Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions docs/cmake-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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. |

Expand Down
4 changes: 2 additions & 2 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
83 changes: 70 additions & 13 deletions src/ctest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -447,11 +447,44 @@ export class CTestDriver implements vscode.Disposable {
return items;
};

private async getCTestArgs(driver: CMakeDriver, customizedTask: boolean = false, testPreset?: TestPreset): Promise<string[] | undefined> {
/**
* 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<string[] | undefined> {
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);

Expand Down Expand Up @@ -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) || [];
Expand Down Expand Up @@ -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));
Expand All @@ -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);

Expand All @@ -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.
Expand All @@ -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 || [];
Expand Down
Loading