diff --git a/news/2 Fixes/17282.md b/news/2 Fixes/17282.md new file mode 100644 index 000000000000..444bdaa0e892 --- /dev/null +++ b/news/2 Fixes/17282.md @@ -0,0 +1 @@ +Fix issue with incomplete `unittest` runs. diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py index 13a34bf6f91e..c22c0d5d2eda 100644 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ b/pythonFiles/visualstudio_py_testlauncher.py @@ -340,9 +340,11 @@ def main(): testId = m.id() if testId.startswith(opts.tests[0]): suite = cls - if testId == opts.tests[0]: - tests = unittest.TestSuite([m]) - break + if testId in opts.tests: + if tests is None: + tests = unittest.TestSuite([m]) + else: + tests.addTest(m) except Exception as err: errorMessage = traceback.format_exc() if tests is None: diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts index 1a7008718edc..9e053893740a 100644 --- a/src/client/testing/testController/unittest/runner.ts +++ b/src/client/testing/testController/unittest/runner.ts @@ -6,7 +6,6 @@ import { Location, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vs import { traceError, traceInfo } from '../../../common/logger'; import * as internalScripts from '../../../common/process/internal/scripts'; import { IOutputChannel } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; import { noop } from '../../../common/utils/misc'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { ITestRunner, ITestDebugLauncher, IUnitTestSocketServer, LaunchOptions, Options } from '../../common/types'; @@ -57,7 +56,23 @@ export class UnittestRunner implements ITestsRunner { ): Promise { runInstance.appendOutput(`Running tests (unittest): ${testNodes.map((t) => t.id).join(' ; ')}\r\n`); const testCaseNodes: TestItem[] = []; - testNodes.forEach((t) => testCaseNodes.push(...getTestCaseNodes(t))); + const fileToTestCases: Map = new Map(); + + testNodes.forEach((t) => { + const nodes = getTestCaseNodes(t); + nodes.forEach((n) => { + if (n.uri) { + const fsRunIds = fileToTestCases.get(n.uri.fsPath); + if (fsRunIds) { + fsRunIds.push(n); + } else { + fileToTestCases.set(n.uri.fsPath, [n]); + } + } + }); + testCaseNodes.push(...nodes); + }); + const tested: string[] = []; const counts = { @@ -70,10 +85,8 @@ export class UnittestRunner implements ITestsRunner { let failFast = false; let stopTesting = false; - let testCasePromise: Deferred; this.server.on('error', (message: string, ...data: string[]) => { traceError(`${message} ${data.join(' ')}`); - testCasePromise.reject(); }); this.server.on('log', (message: string, ...data: string[]) => { traceInfo(`${message} ${data.join(' ')}`); @@ -81,7 +94,6 @@ export class UnittestRunner implements ITestsRunner { this.server.on('connect', noop); this.server.on('start', noop); this.server.on('result', (data: ITestData) => { - testCasePromise.resolve(); const testCase = testCaseNodes.find((node) => idToRawData.get(node.id)?.runId === data.test); const rawTestCase = idToRawData.get(testCase?.id ?? ''); if (testCase && rawTestCase) { @@ -147,18 +159,15 @@ export class UnittestRunner implements ITestsRunner { }); const port = await this.server.start(); - const runTestInternal = async (testFile = '', testId = ''): Promise => { + const runTestInternal = async (testFilePath: string, testRunIds: string[]): Promise => { let testArgs = getTestRunArgs(options.args); failFast = testArgs.indexOf('--uf') >= 0; testArgs = testArgs.filter((arg) => arg !== '--uf'); testArgs.push(`--result-port=${port}`); - if (testId.length > 0) { - testArgs.push(`-t${testId}`); - } - if (testFile.length > 0) { - testArgs.push(`--testFile=${testFile}`); - } + testRunIds.forEach((i) => testArgs.push(`-t${i}`)); + testArgs.push(`--testFile=${testFilePath}`); + if (options.debug === true) { testArgs.push('--debug'); const launchOptions: LaunchOptions = { @@ -179,23 +188,30 @@ export class UnittestRunner implements ITestsRunner { token: options.token, workspaceFolder: options.workspaceFolder, }; - testCasePromise = createDeferred(); await this.runner.run(UNITTEST_PROVIDER, runOptions); - return testCasePromise.promise; + return Promise.resolve(); }; try { - for (const testCaseNode of testCaseNodes) { + for (const testFile of fileToTestCases.keys()) { if (stopTesting || options.token.isCancellationRequested) { break; } - runInstance.appendOutput(`Running tests: ${testCaseNode.id}\r\n`); - const rawTestCaseNode = idToRawData.get(testCaseNode.id); - if (rawTestCaseNode) { - // VS Code API requires that we set the run state on the leaf nodes. The state of the - // parent nodes are computed based on the state of child nodes. - runInstance.started(testCaseNode); - await runTestInternal(testCaseNode.uri?.fsPath, rawTestCaseNode.runId); + + const nodes = fileToTestCases.get(testFile); + if (nodes) { + runInstance.appendOutput(`Running tests: ${nodes.join('\r\n')}\r\n`); + const runIds: string[] = []; + nodes.forEach((n) => { + const rawNode = idToRawData.get(n.id); + if (rawNode) { + // VS Code API requires that we set the run state on the leaf nodes. The state of the + // parent nodes are computed based on the state of child nodes. + runInstance.started(n); + runIds.push(rawNode.runId); + } + }); + await runTestInternal(testFile, runIds); } } } catch (ex) {