Skip to content

Commit c8bb730

Browse files
OmotolaCopilot
andcommitted
Add "${testName}" variable support for ctestArgs and ctestDefaultArgs
Enables test-specific argument expansion in cmake.ctestArgs and cmake.ctestDefaultArgs using the ${testName} placeholder. This allows users to generate per-test output (e.g., --output-log ${testName}.log) instead of having all tests overwrite the same file. The variable is expanded when a single test is being run: - Non-parallel mode: expanded per-test in the sequential loop - Parallel mode: expanded when exactly one test is targeted - Direct ctest path: expanded when testsToRun has one entry When multiple tests run in a batch, a warning is logged and the variable is not expanded. Args containing ${testName} (and their preceding flags) are filtered out when no test name is available, preventing literal placeholders from being passed to ctest. Resolves #4416 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 55b2a85 commit c8bb730

4 files changed

Lines changed: 76 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Features:
77
- 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)
88

99
Improvements:
10+
- 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)
1011
- Improve responsiveness to CMake path changes made by vendor extensions during configure-on-open retry. [#4908](https://github.com/microsoft/vscode-cmake-tools/pull/4908) Contributed by STMicroelectronics
1112
- Improve ergonomics of the test explorer UI by removing the project source directory, improving horizontal scrolling experience. [#4562](https://github.com/microsoft/vscode-cmake-tools/issues/4562) [@miss-programgamer](https://github.com/miss-programgamer)
1213

docs/cmake-settings.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ Options that support substitution, in the table below, allow variable references
4141
| `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 |
4242
| `cmake.ctest.testExplorerIntegrationEnabled` | If `true`, configure CMake to generate information needed by the test explorer. | `true` | no |
4343
| `cmake.ctest.testSuiteDelimiter` | Character(s) that separate test suite name components. | `null` | no |
44-
| `cmake.ctestArgs` | An array of additional arguments to pass to CTest. | `[]` | yes |
45-
| `cmake.ctestDefaultArgs` | Default arguments to pass to CTest. | `["-T", "test", "--output-on-failure"]` | no |
44+
| `cmake.ctestArgs` | An array of additional arguments to pass to CTest. Supports `${testName}` for per-test expansion (see [Variable substitution](#variable-substitution)). | `[]` | yes |
45+
| `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 |
4646
| `cmake.ctestPath` | Path to CTest executable. | `null` | no |
4747
| `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 |
4848
| `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
141141
|`${buildKitVersionMajor}`| The current CMake kit major version. For example: `7`|
142142
|`${buildKitVersionMinor}`| The current CMake kit minor version. For example: `3`|
143143
|`${generator}`| The name of the CMake generator. For example: `Ninja`|
144+
|`${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.|
144145
|`${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.|
145146
|`${userHome}`| The full path to the current user's home directory. |
146147

package.nls.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@
257257
"cmake-tools.configuration.cmake.defaultVariants.buildType.release.long": "Optimize for speed - exclude debug information.",
258258
"cmake-tools.configuration.cmake.defaultVariants.buildType.minsize.long": "Optimize for smallest binary size - exclude debug information.",
259259
"cmake-tools.configuration.cmake.defaultVariants.buildType.reldeb.long": "Optimize for speed - include debug information.",
260-
"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.",
261-
"cmake-tools.configuration.cmake.ctestDefaultArgs.description": "Arguments passed by default to CTest.",
260+
"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).",
261+
"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).",
262262
"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.",
263263
"cmake-tools.configuration.cmake.environment.additionalProperties.description": "Value for the environment variable.",
264264
"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.",

src/ctest.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { fs } from '@cmt/pr';
1111
import * as util from '@cmt/util';
1212
import * as nls from 'vscode-nls';
1313
import { testArgs, TestPreset } from '@cmt/presets/preset';
14-
import { expandString } from '@cmt/expand';
14+
import { expandString, ExpansionOptions } from '@cmt/expand';
1515
import * as proc from '@cmt/proc';
1616
import { ProjectController } from '@cmt/projectController';
1717
import { extensionManager } from '@cmt/extension';
@@ -446,11 +446,44 @@ export class CTestDriver implements vscode.Disposable {
446446
return items;
447447
};
448448

449-
private async getCTestArgs(driver: CMakeDriver, customizedTask: boolean = false, testPreset?: TestPreset): Promise<string[] | undefined> {
449+
/**
450+
* Check whether any of the user's ctestArgs or ctestDefaultArgs contain ${testName}.
451+
*/
452+
private hasTestNameVariable(): boolean {
453+
const allArgs = [...this.ws.config.ctestDefaultArgs, ...this.ws.config.ctestArgs];
454+
return allArgs.some(arg => arg.includes('${testName}'));
455+
}
456+
457+
private async getCTestArgs(driver: CMakeDriver, customizedTask: boolean = false, testPreset?: TestPreset, testName?: string): Promise<string[] | undefined> {
450458
let ctestArgs: string[];
451-
const opts = driver.expansionOptions;
452-
const initialArgs = await Promise.all(this.ws.config.ctestDefaultArgs.map(async (value) => expandString(value, driver.expansionOptions)));
453-
const additionalArgs = await Promise.all(this.ws.config.ctestArgs.map(async (value) => expandString(value, driver.expansionOptions)));
459+
const opts: ExpansionOptions = testName !== undefined
460+
? { ...driver.expansionOptions, vars: { ...driver.expansionOptions.vars, testName } }
461+
: driver.expansionOptions;
462+
463+
// When testName is not provided, filter out args containing ${testName} to avoid
464+
// passing literal "${testName}" to ctest (e.g., during test discovery or batch runs).
465+
// Also removes the preceding flag arg (e.g., "--output-log") to avoid dangling flags.
466+
const testNamePlaceholder = '${testName}';
467+
const filterTestNameArgs = (args: string[]): string[] => {
468+
if (testName !== undefined) {
469+
return args;
470+
}
471+
const filtered: string[] = [];
472+
for (let i = 0; i < args.length; i++) {
473+
if (args[i].includes(testNamePlaceholder)) {
474+
// Also remove preceding flag arg if it starts with '-'
475+
if (filtered.length > 0 && filtered[filtered.length - 1].startsWith('-')) {
476+
filtered.pop();
477+
}
478+
} else {
479+
filtered.push(args[i]);
480+
}
481+
}
482+
return filtered;
483+
};
484+
485+
const initialArgs = await Promise.all(filterTestNameArgs(this.ws.config.ctestDefaultArgs).map(async (value) => expandString(value, opts)));
486+
const additionalArgs = await Promise.all(filterTestNameArgs(this.ws.config.ctestArgs).map(async (value) => expandString(value, opts)));
454487

455488
ctestArgs = initialArgs.slice(0);
456489

@@ -529,7 +562,12 @@ export class CTestDriver implements vscode.Disposable {
529562
return { exitCode: -2 };
530563
}
531564

532-
const ctestArgs = await this.getCTestArgs(driver, customizedTask, testPreset) || [];
565+
// Pass testName when exactly one test is targeted
566+
const singleTestName = testsToRun && testsToRun.length === 1 ? testsToRun[0] : undefined;
567+
if (singleTestName === undefined && this.hasTestNameVariable() && testsToRun && testsToRun.length > 1) {
568+
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.'));
569+
}
570+
const ctestArgs = await this.getCTestArgs(driver, customizedTask, testPreset, singleTestName) || [];
533571
if (testsToRun && testsToRun.length > 0) {
534572
ctestArgs.push("-R");
535573
const superset = this.getTestNames() || [];
@@ -695,6 +733,7 @@ export class CTestDriver implements vscode.Disposable {
695733
await this.fillDriverMap(tests, run, cancellation, driverMap, driver, ctestPath, ctestArgs, testsToRun, customizedTask);
696734

697735
if (!this.ws.config.ctestAllowParallelJobs) {
736+
const usesTestName = this.hasTestNameVariable();
698737
for (const driver of driverMap.values()) {
699738
// Sort tests alphabetically by label to match the Test Explorer display order.
700739
driver.tests.sort((a, b) => (a.label).localeCompare(b.label));
@@ -706,8 +745,12 @@ export class CTestDriver implements vscode.Disposable {
706745

707746
run.started(test);
708747

748+
// Re-expand args with ${testName} when the variable is used
749+
const baseArgs = usesTestName
750+
? (await this.getCTestArgs(driver.driver, customizedTask, undefined, test.id) || [])
751+
: driver.ctestArgs;
709752
const superset = this.getTestNames() || [];
710-
const _ctestArgs = driver.ctestArgs.concat('-R', getMinimalRegexFragments(superset, [test.id]).join('|'));
753+
const _ctestArgs = baseArgs.concat('-R', getMinimalRegexFragments(superset, [test.id]).join('|'));
711754

712755
const testResults = await this.runCTestImpl(driver.driver, driver.ctestPath, _ctestArgs, cancellation, customizedTask, consumer);
713756

@@ -731,15 +774,10 @@ export class CTestDriver implements vscode.Disposable {
731774
/**
732775
* For each unique driver (i.e., driver.sourceDir), run the tests.
733776
*/
777+
const usesTestName = this.hasTestNameVariable();
734778
for (const driver of driverMap.values()) {
735779
const uniqueDriver: CMakeDriver = driver.driver;
736780
const uniqueCtestPath: string = driver.ctestPath;
737-
const uniqueCtestArgs: string[] = driver.ctestArgs;
738-
739-
// Check if the user (or us programmatically) have already added a -j flag. If not, add it by default for parallel jobs.
740-
if (uniqueCtestArgs.filter(arg => arg.startsWith("-j")).length === 0) {
741-
uniqueCtestArgs.push(`-j${this.ws.config.numCTestJobs}`);
742-
}
743781

744782
// If we have the test explorer enabled and this method was called from a test explorer entry point,
745783
// then there may be a scenario when the user requested only a subset of tests to be ran.
@@ -752,6 +790,25 @@ export class CTestDriver implements vscode.Disposable {
752790
targetTests = driver.tests.filter(t => testsToRun.includes(t.id));
753791
}
754792

793+
const effectiveTestCount = targetTests ? targetTests.length : driver.tests.length;
794+
795+
// When ${testName} is used and exactly one test is targeted, expand with its name
796+
let uniqueCtestArgs: string[];
797+
if (usesTestName && effectiveTestCount === 1) {
798+
const singleTestName = targetTests ? targetTests[0].id : driver.tests[0].id;
799+
uniqueCtestArgs = await this.getCTestArgs(uniqueDriver, customizedTask, undefined, singleTestName) || [];
800+
} else if (usesTestName && effectiveTestCount > 1) {
801+
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.'));
802+
uniqueCtestArgs = driver.ctestArgs.slice();
803+
} else {
804+
uniqueCtestArgs = driver.ctestArgs.slice();
805+
}
806+
807+
// Check if the user (or us programmatically) have already added a -j flag. If not, add it by default for parallel jobs.
808+
if (uniqueCtestArgs.filter(arg => arg.startsWith("-j")).length === 0) {
809+
uniqueCtestArgs.push(`-j${this.ws.config.numCTestJobs}`);
810+
}
811+
755812
if (targetTests) {
756813
uniqueCtestArgs.push("-R");
757814
const superset = this.getTestNames() || [];

0 commit comments

Comments
 (0)