diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8439ba5..cff3cb9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Improvements: - No longer convert paths on lowercase on MacOS to enable cpp tools to resolve them. [#4325](https://github.com/microsoft/vscode-cmake-tools/pull/4325) [@tringenbach](https://github.com/tringenbach) - Speedup & reduce heap allocations in shlex split module function. Significant gains for mid-large compile_commands.json - CompilationDatabase construction. [#4458](https://github.com/microsoft/vscode-cmake-tools/pull/4458) [@borjamunozf](https://github.com/borjamunozf) - In the Test Explorer, associate CTest tests with outermost function or macro invocation that calls `add_test()` instead of with the `add_test()` call itself. [#4490](https://github.com/microsoft/vscode-cmake-tools/issues/4490) [@malsyned](https://github.com/malsyned) +- When CMake is invoked prior to running tests, build targets required for the test rather than everything. [#4515](https://github.com/microsoft/vscode-cmake-tools/issues/4515) [@epistax](https://github.com/epistax) Bug Fixes: diff --git a/src/ctest.ts b/src/ctest.ts index 4f41ffcdb..26f76816d 100644 --- a/src/ctest.ts +++ b/src/ctest.ts @@ -1275,44 +1275,93 @@ export class CTestDriver implements vscode.Disposable { return currentTestItem.id; } + /** + * Determine and build all targets needed to rebuild the selected TestItems. Returns a false if anything goes + * wrong, and returns an early success if build-before-run is disabled. + */ private async buildTests(tests: vscode.TestItem[], run: vscode.TestRun): Promise { // If buildBeforeRun is set to false, we skip the build step if (!this.ws.config.buildBeforeRun) { return true; } - // Folder => status - const builtFolder = new Map(); - let status: number = 0; + const foundTarget = new Map>(); for (const test of tests) { + if (!await this.getTestTargets(test, foundTarget, run)) { + return false; + } + } + + return this.buildTestTargets(foundTarget, run); + } + + /** + * Given the TestItem, determine the owning CMakeProject and build targets to build the tests. If the given + * TestItem is a suite, then recurse. Information is passed back through the foundTarget parameter. A failure to + * identify the project of the test will result in a ctest error and a false value being returned. + */ + private async getTestTargets(test: vscode.TestItem, foundTarget: Map>, run: vscode.TestRun): Promise { + if (test.children.size > 0) { + const children = this.testItemCollectionToArray(test.children); + for (const child of children) { + await this.getTestTargets(child, foundTarget, run); + } + } else { + const testProgram = this.testProgram(test.id); const folder = this.getTestRootFolder(test); - if (!builtFolder.has(folder)) { - const project = await this.projectController?.getProjectForFolder(folder); - if (!project) { - status = 1; + const project = await this.projectController?.getProjectForFolder(folder); + if (!project) { + this.ctestErrored(test, run, { message: localize('no.project.found', 'No project found for folder {0}', folder) }); + return false; + } + if (!foundTarget.has(project)) { + foundTarget.set(project, new Map([[testProgram, [test]]])); + } else { + const prj = foundTarget.get(project)!; + if (!prj.has(testProgram)) { + prj.set(testProgram, [test]); } else { - try { - if (extensionManager !== undefined && extensionManager !== null) { - extensionManager.cleanOutputChannel(); - } - const buildResult = await project.build(undefined, false, false); - if (buildResult !== 0) { - status = 2; - } - } catch (e) { - status = 2; - } + prj.get(testProgram)!.push(test); } } - builtFolder.set(folder, status); - if (status === 1) { - this.ctestErrored(test, run, { message: localize('no.project.found', 'No project found for folder {0}', folder) }); - } else if (status === 2) { - this.ctestErrored(test, run, { message: localize('build.failed', 'Build failed') }); - } } + return true; + } - return Array.from(builtFolder.values()).filter(v => v !== 0).length === 0; + /** + * Build the targets provided in foundTarget. CMake will be invoked once per CMakeProject for efficiency. On error, + * the associated tests will be flagged with a ctest error and a false value will be returned. + */ + private async buildTestTargets(foundTarget: Map>, run: vscode.TestRun): Promise { + let overallSuccess = true; + for (const [project, targets] of foundTarget) { + const binaryDir = (await project.binaryDir).toString(); + const accmulatedTestList: vscode.TestItem[] = []; + const accumulatedTargets: string[] = []; + let success: boolean = true; + for (const [targetName, testList] of targets) { + accumulatedTargets.push(path.relative(binaryDir, targetName)); + accmulatedTestList.concat(testList); + } + try { + if (extensionManager !== undefined && extensionManager !== null) { + extensionManager.cleanOutputChannel(); + } + const buildResult = await project.build(accumulatedTargets, false, false); + if (buildResult !== 0) { + success = false; + } + } catch (e) { + success = false; + } + if (!success) { + overallSuccess = false; + accmulatedTestList.forEach(test => { + this.ctestErrored(test, run, { message: localize('build.failed', 'Build failed') }); + }); + } + }; + return overallSuccess; } /**