diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f6c2e22..0d3a38a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Features: - Add `onConfigureResult` event to the CMake Tools API that fires after every configure attempt (success or failure), allowing dependent extensions to detect and react to configure failures. [#4021](https://github.com/microsoft/vscode-cmake-tools/issues/4021) Improvements: +- Add `${testName}` variable support for `cmake.ctestArgs` and `cmake.ctestDefaultArgs`, enabling per-test argument expansion (e.g., unique log file paths per test). [#4416](https://github.com/microsoft/vscode-cmake-tools/issues/4416) - Reduce CI pipeline time by parallelizing E2E test jobs and adding build artifact caching. - Further reduce CI pipeline time by splitting each platform pipeline into a build job and a parallel test matrix (backend, smoke, unit, integration, four E2E suites), caching `node_modules` between runs, sharing a single Xvfb instance on Linux, dropping a duplicate backend-test load inside the unit-tests Electron suite, and adding per-step timeouts to prevent silent hangs. - Add `cmake.showTimestampsInOutput` setting to display timestamps and log levels in the CMake output channel, useful for tracking build durations. [#4057](https://github.com/microsoft/vscode-cmake-tools/issues/4057) diff --git a/docs/cmake-settings.md b/docs/cmake-settings.md index 3330a13f5..27744a135 100644 --- a/docs/cmake-settings.md +++ b/docs/cmake-settings.md @@ -41,8 +41,8 @@ Options that support substitution, in the table below, allow variable references | `cmake.ctest.parallelJobs` | Specify the number of jobs to run in parallel for ctest. Using the value `0` will detect and use the number of CPUs. Using the value `1` will disable test parallelism. | `0` | no | | `cmake.ctest.testExplorerIntegrationEnabled` | If `true`, configure CMake to generate information needed by the test explorer. | `true` | no | | `cmake.ctest.testSuiteDelimiter` | Character(s) that separate test suite name components. | `null` | no | -| `cmake.ctestArgs` | An array of additional arguments to pass to CTest. | `[]` | yes | -| `cmake.ctestDefaultArgs` | Default arguments to pass to CTest. | `["-T", "test", "--output-on-failure"]` | no | +| `cmake.ctestArgs` | An array of additional arguments to pass to CTest. Supports `${testName}` for per-test expansion (see [Variable substitution](#variable-substitution)). | `[]` | yes | +| `cmake.ctestDefaultArgs` | Default arguments to pass to CTest. Supports `${testName}` for per-test expansion (see [Variable substitution](#variable-substitution)). | `["-T", "test", "--output-on-failure"]` | no | | `cmake.ctestPath` | Path to CTest executable. | `null` | no | | `cmake.debugConfig`| The debug configuration to use when debugging a target. When `type` is specified, automatic debugger detection is skipped and a custom debug adapter can be used. Additional properties required by the debug adapter can be added freely. See [Debug and launch](debug-launch.md#customize-the-debug-adapter) for examples, including Natvis via `visualizerFile` without a `launch.json`. | `null` (no values) | yes | | `cmake.defaultActiveFolder`| The name of active folder, which be used as default (Only works when `cmake.autoSelectActiveFolder` is disabled). | `""` | no | @@ -141,6 +141,7 @@ The following built-in variables are expanded in supported `cmake.*` settings on |`${buildKitVersionMajor}`| The current CMake kit major version. For example: `7`| |`${buildKitVersionMinor}`| The current CMake kit minor version. For example: `3`| |`${generator}`| The name of the CMake generator. For example: `Ninja`| +|`${testName}`| The name of the current CTest test. Only expanded in `cmake.ctestArgs` and `cmake.ctestDefaultArgs` when running a single test (non-parallel mode or single-test selection). When multiple tests run in a batch, the variable is not expanded and a warning is logged.| |`${projectName}`|**DEPRECATED**. Expands to the constant string `"ProjectName"` CMake does not consider there to be just one project name to use. The concept of a single project does not work in CMake. Use `${workspaceRootFolderName}`, instead.| |`${userHome}`| The full path to the current user's home directory. | diff --git a/package.nls.json b/package.nls.json index ef0cfe69e..d3f254594 100644 --- a/package.nls.json +++ b/package.nls.json @@ -257,8 +257,8 @@ "cmake-tools.configuration.cmake.defaultVariants.buildType.release.long": "Optimize for speed - exclude debug information.", "cmake-tools.configuration.cmake.defaultVariants.buildType.minsize.long": "Optimize for smallest binary size - exclude debug information.", "cmake-tools.configuration.cmake.defaultVariants.buildType.reldeb.long": "Optimize for speed - include debug information.", - "cmake-tools.configuration.cmake.ctestArgs.description": "Additional arguments to pass to CTest. When using CMake Presets, these arguments are temporarily added to the arguments provided by the active test preset.", - "cmake-tools.configuration.cmake.ctestDefaultArgs.description": "Arguments passed by default to CTest.", + "cmake-tools.configuration.cmake.ctestArgs.description": "Additional arguments to pass to CTest. When using CMake Presets, these arguments are temporarily added to the arguments provided by the active test preset. Use ${testName} to reference the current test name (only expanded when running a single test).", + "cmake-tools.configuration.cmake.ctestDefaultArgs.description": "Arguments passed by default to CTest. Use ${testName} to reference the current test name (only expanded when running a single test).", "cmake-tools.configuration.cmake.environment.description": "Environment variables to set when running CMake commands. When using CMake Presets, these are temporarily added to the environment used for CMake commands.", "cmake-tools.configuration.cmake.environment.additionalProperties.description": "Value for the environment variable.", "cmake-tools.configuration.cmake.configureEnvironment.description": "Environment variables to pass to CMake during configure. When using CMake Presets, these are temporarily added to the environment provided by the active configure preset.", diff --git a/src/ctest.ts b/src/ctest.ts index 28e5955c7..3ccee0e7a 100644 --- a/src/ctest.ts +++ b/src/ctest.ts @@ -12,7 +12,7 @@ import { fs } from '@cmt/pr'; import * as util from '@cmt/util'; import * as nls from 'vscode-nls'; import { testArgs, TestPreset } from '@cmt/presets/preset'; -import { expandString } from '@cmt/expand'; +import { expandString, ExpansionOptions } from '@cmt/expand'; import * as proc from '@cmt/proc'; import { ProjectController } from '@cmt/projectController'; import { extensionManager } from '@cmt/extension'; @@ -447,11 +447,44 @@ export class CTestDriver implements vscode.Disposable { return items; }; - private async getCTestArgs(driver: CMakeDriver, customizedTask: boolean = false, testPreset?: TestPreset): Promise { + /** + * Check whether any of the user's ctestArgs or ctestDefaultArgs contain ${testName}. + */ + private hasTestNameVariable(): boolean { + const allArgs = [...this.ws.config.ctestDefaultArgs, ...this.ws.config.ctestArgs]; + return allArgs.some(arg => arg.includes('${testName}')); + } + + private async getCTestArgs(driver: CMakeDriver, customizedTask: boolean = false, testPreset?: TestPreset, testName?: string): Promise { let ctestArgs: string[]; - const opts = driver.expansionOptions; - const initialArgs = await Promise.all(this.ws.config.ctestDefaultArgs.map(async (value) => expandString(value, driver.expansionOptions))); - const additionalArgs = await Promise.all(this.ws.config.ctestArgs.map(async (value) => expandString(value, driver.expansionOptions))); + const opts: ExpansionOptions = testName !== undefined + ? { ...driver.expansionOptions, vars: { ...driver.expansionOptions.vars, testName } } + : driver.expansionOptions; + + // When testName is not provided, filter out args containing ${testName} to avoid + // passing literal "${testName}" to ctest (e.g., during test discovery or batch runs). + // Also removes the preceding flag arg (e.g., "--output-log") to avoid dangling flags. + const testNamePlaceholder = '${testName}'; + const filterTestNameArgs = (args: string[]): string[] => { + if (testName !== undefined) { + return args; + } + const filtered: string[] = []; + for (let i = 0; i < args.length; i++) { + if (args[i].includes(testNamePlaceholder)) { + // Also remove preceding flag arg if it starts with '-' + if (filtered.length > 0 && filtered[filtered.length - 1].startsWith('-')) { + filtered.pop(); + } + } else { + filtered.push(args[i]); + } + } + return filtered; + }; + + const initialArgs = await Promise.all(filterTestNameArgs(this.ws.config.ctestDefaultArgs).map(async (value) => expandString(value, opts))); + const additionalArgs = await Promise.all(filterTestNameArgs(this.ws.config.ctestArgs).map(async (value) => expandString(value, opts))); ctestArgs = initialArgs.slice(0); @@ -530,7 +563,12 @@ export class CTestDriver implements vscode.Disposable { return { exitCode: -2 }; } - const ctestArgs = await this.getCTestArgs(driver, customizedTask, testPreset) || []; + // Pass testName when exactly one test is targeted + const singleTestName = testsToRun && testsToRun.length === 1 ? testsToRun[0] : undefined; + if (singleTestName === undefined && this.hasTestNameVariable() && testsToRun && testsToRun.length > 1) { + log.warning(localize('testName.not.supported.multiple', '${testName} variable in ctest args is not supported when running multiple tests. The variable will not be expanded.')); + } + const ctestArgs = await this.getCTestArgs(driver, customizedTask, testPreset, singleTestName) || []; if (testsToRun && testsToRun.length > 0) { ctestArgs.push("-R"); const superset = this.getTestNamesForSourceDir(driver.sourceDir) || []; @@ -696,6 +734,7 @@ export class CTestDriver implements vscode.Disposable { await this.fillDriverMap(tests, run, cancellation, driverMap, driver, ctestPath, ctestArgs, testsToRun, customizedTask); if (!this.ws.config.ctestAllowParallelJobs) { + const usesTestName = this.hasTestNameVariable(); for (const driver of driverMap.values()) { // Sort tests alphabetically by label to match the Test Explorer display order. driver.tests.sort((a, b) => (a.label).localeCompare(b.label)); @@ -707,8 +746,12 @@ export class CTestDriver implements vscode.Disposable { run.started(test); + // Re-expand args with ${testName} when the variable is used + const baseArgs = usesTestName + ? (await this.getCTestArgs(driver.driver, customizedTask, undefined, test.id) || []) + : driver.ctestArgs; const superset = this.getTestNamesForSourceDir(driver.driver.sourceDir) || []; - const _ctestArgs = driver.ctestArgs.concat('-R', getMinimalRegexFragments(superset, [test.id]).join('|')); + const _ctestArgs = baseArgs.concat('-R', getMinimalRegexFragments(superset, [test.id]).join('|')); const testResults = await this.runCTestImpl(driver.driver, driver.ctestPath, _ctestArgs, cancellation, customizedTask, consumer); @@ -732,15 +775,10 @@ export class CTestDriver implements vscode.Disposable { /** * For each unique driver (i.e., driver.sourceDir), run the tests. */ + const usesTestName = this.hasTestNameVariable(); for (const driver of driverMap.values()) { const uniqueDriver: CMakeDriver = driver.driver; const uniqueCtestPath: string = driver.ctestPath; - const uniqueCtestArgs: string[] = driver.ctestArgs; - - // Check if the user (or us programmatically) have already added a -j flag. If not, add it by default for parallel jobs. - if (uniqueCtestArgs.filter(arg => arg.startsWith("-j")).length === 0) { - uniqueCtestArgs.push(`-j${this.ws.config.numCTestJobs}`); - } // If we have the test explorer enabled and this method was called from a test explorer entry point, // then there may be a scenario when the user requested only a subset of tests to be ran. @@ -754,6 +792,25 @@ export class CTestDriver implements vscode.Disposable { targetTests = driver.tests.filter(t => testsToRun.includes(t.id)); } + const effectiveTestCount = targetTests ? targetTests.length : driver.tests.length; + + // When ${testName} is used and exactly one test is targeted, expand with its name + let uniqueCtestArgs: string[]; + if (usesTestName && effectiveTestCount === 1) { + const singleTestName = targetTests ? targetTests[0].id : driver.tests[0].id; + uniqueCtestArgs = await this.getCTestArgs(uniqueDriver, customizedTask, undefined, singleTestName) || []; + } else if (usesTestName && effectiveTestCount > 1) { + log.warning(localize('testName.not.supported.parallel', '${testName} variable in ctest args is not supported when running multiple tests in parallel. The variable will not be expanded.')); + uniqueCtestArgs = driver.ctestArgs.slice(); + } else { + uniqueCtestArgs = driver.ctestArgs.slice(); + } + + // Check if the user (or us programmatically) have already added a -j flag. If not, add it by default for parallel jobs. + if (uniqueCtestArgs.filter(arg => arg.startsWith("-j")).length === 0) { + uniqueCtestArgs.push(`-j${this.ws.config.numCTestJobs}`); + } + if (targetTests) { uniqueCtestArgs.push("-R"); const superset = allTestNamesForDriver || [];