diff --git a/CHANGELOG.md b/CHANGELOG.md index fa059eb03..2b2337393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ Features: - Add support for the FASTBuild generator (CMake 4.2+). [#4690](https://github.com/microsoft/vscode-cmake-tools/pull/4690) - Add support for `${workspaceFolder}`, `${workspaceFolder:name}` variables and relative paths in `cmake.exclude` setting for multi-root workspaces. [#4689](https://github.com/microsoft/vscode-cmake-tools/pull/4689) +Improvements: +- 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: - Fix kit detection returning "unknown vendor" when using clang-cl compiler. [#4638](https://github.com/microsoft/vscode-cmake-tools/issues/4638) - Update testing framework to fix bugs when running tests of CMake Tools without a reliable internet connection. [#4891](https://github.com/microsoft/vscode-cmake-tools/pull/4891) [@cwalther](https://github.com/cwalther) diff --git a/src/ctest.ts b/src/ctest.ts index b6b4476e5..3fd3fda51 100644 --- a/src/ctest.ts +++ b/src/ctest.ts @@ -1832,6 +1832,10 @@ 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) { @@ -1841,7 +1845,11 @@ export class CTestDriver implements vscode.Disposable { // 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; + } const folder = this.getTestRootFolder(test); if (!builtFolder.has(folder)) { const project = await this.projectController?.getProjectForFolder(folder); @@ -1869,7 +1877,90 @@ export class CTestDriver implements vscode.Disposable { } } - return Array.from(builtFolder.values()).filter(v => v !== 0).length === 0; + 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) { + if (!await this.getTestTargets(child, foundTarget, run)) { + return false; + } + } + } else { + const testProgram = this.testProgram(test.id); + if (!testProgram) { + this.ctestErrored(test, run, { message: localize('test.program.not.found', 'Could not determine the test program for test {0}', test.id) }); + return false; + } + const folder = this.getTestRootFolder(test); + 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 { + prj.get(testProgram)!.push(test); + } + } + } + return true; + } + + /** + * 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 execTargets = await project.executableTargets; + // Precompute a lookup map from normalized executable path to target name + const execPathToName = new Map(); + for (const t of execTargets) { + execPathToName.set(util.platformNormalizePath(t.path), t.name); + } + const accumulatedTestList: vscode.TestItem[] = []; + const accumulatedTargets: string[] = []; + let success: boolean = true; + for (const [targetPath, testList] of targets) { + const normalizedTargetPath = util.platformNormalizePath(targetPath); + accumulatedTargets.push(execPathToName.get(normalizedTargetPath) ?? path.parse(targetPath).name); + accumulatedTestList.push(...testList); + } + try { + if (extensionManager !== undefined && extensionManager !== null) { + extensionManager.cleanOutputChannel(); + } + const buildResult = await project.build(accumulatedTargets, false, false); + if (buildResult.exitCode !== 0) { + log.error(localize('build.targets.failed.with.code', 'Building targets [{0}] failed with exit code {1}.', accumulatedTargets.join(', '), buildResult.exitCode)); + success = false; + } + } catch (e) { + log.error(localize('build.targets.threw', 'Building targets [{0}] threw an error: {1}', accumulatedTargets.join(', '), (e as Error)?.message ?? String(e))); + success = false; + } + if (!success) { + overallSuccess = false; + accumulatedTestList.forEach(test => { + this.ctestErrored(test, run, { message: localize('build.failed', 'Build failed') }); + }); + } + }; + return overallSuccess; } /**