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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 1.24

Features:
- Add a `refresh` command to the `cmake` task type so builds and automation driven through tasks (for example a build launched from another extension, where the test view is not visible during the build) can refresh the CMake test information / Test Explorer, equivalent to the "CMake: Refresh Tests" command. [#5007](https://github.com/microsoft/vscode-cmake-tools/issues/5007)
- 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)
- 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)
Expand All @@ -26,6 +27,7 @@ Improvements:
- Embed the source commit SHA as a `commitId` field in the packaged extension's `package.json` so the exact commit a (pre-)release was built from can be identified, since the marketplace version number alone cannot be traced back to a commit. [#4801](https://github.com/microsoft/vscode-cmake-tools/issues/4801)

Bug Fixes:
- Fix the Test Explorer keeping previous pass/fail results after they became stale. After "CMake: Refresh Tests", the `refresh` task, and a successful build, build all, or clean rebuild (from a command or the `type: cmake` build task), previously-run tests are now marked as outdated (their prior pass/fail is muted) instead of misleadingly appearing current, since the rebuilt binaries make earlier results stale. [#5007](https://github.com/microsoft/vscode-cmake-tools/issues/5007)
- Stop showing "Adding a file without a valid code model" / "Deleting a file without a valid code model" warning notifications when a source file is automatically added or deleted and there is no valid CMake code model (for example when using clangd with the C/C++ IntelliSense engine disabled, or before the project has been configured). The automatic list-file update now stays silent in this case; the warning is only shown when the update is explicitly invoked via the "CMake: Add new source file" / "CMake: Remove deleted source file" commands. [#5009](https://github.com/microsoft/vscode-cmake-tools/issues/5009)
- Fix running a single test from the inline Test CodeLens or the project outline building the default (or all) target instead of the test's own executable; single-test runs now build only that test's target.
- Fix test results showing a bare "The test case did not report any output." when a test did not actually run (for example when its executable was not built, so CTest reported it as "Not Run" or matched nothing). The test is now marked with an actionable message explaining that the executable may not have been built. CTest result parsing was also hardened so that compressed test output is decoded and a single test with missing or empty measurements no longer discards the results of the whole run. [#4451](https://github.com/microsoft/vscode-cmake-tools/issues/4451)
Expand Down
22 changes: 22 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,28 @@ However, if you are using presets, this task will be generated in `tasks.json` f

**Note**: When running this task, the test settings defined in `CMakeUserPresets.json`/`CMakePresets.json` will be used.

# Refresh with CMake Tools tasks
You can create a refresh task the same way, by running the **Tasks: Configure task** command and selecting the "CMake: refresh" template. This task refreshes the CMake test information and the Test Explorer, the same as the **CMake: Refresh Tests** command.

```json
{
"type": "cmake",
"label": "CMake: refresh",
"command": "refresh",
"detail": "CMake template refresh task"
}
```

**Note**: A `type: cmake`, `command: build` task already refreshes the test information and marks prior test results as outdated when it finishes, so you do not need to chain a refresh task after it. The refresh task is useful after a build that does *not* go through a `cmake` task (for example an external/shell build task, or a build driven by another extension), where the test view would otherwise not refresh. You can chain it with `dependsOn`:

```json
"dependsOn": [
"Some external build task"
]
```

**Note**: The refresh task builds the default target first (like the **CMake: Refresh Tests** command) and refreshes tests using the active test preset.

# Install/Clean/Clean-rebuild with CMake Tools tasks
Similarly, you can create a Install/Clean/Clean-rebuild task from the VS Code command pallette by running the **Tasks: Configure task** command.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,8 @@
"package",
"workflow",
"clean",
"cleanRebuild"
"cleanRebuild",
"refresh"
],
"description": "%cmake-tools.taskDefinitions.properties.command.description%"
},
Expand Down
45 changes: 41 additions & 4 deletions src/cmakeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2596,9 +2596,19 @@ export class CMakeProject {
/**
* Implementation of `cmake.build`
*/
async build(targets?: string[], showCommandOnly?: boolean, isBuildCommand: boolean = true, cancellationToken?: vscode.CancellationToken, isAutomatic: boolean = false): Promise<CommandResult> {
async build(targets?: string[], showCommandOnly?: boolean, isBuildCommand: boolean = true, cancellationToken?: vscode.CancellationToken, isAutomatic: boolean = false, markOutdated: boolean = false): Promise<CommandResult> {
this.activeBuild = this.runBuild(targets, showCommandOnly, undefined, isBuildCommand, cancellationToken, isAutomatic);
return this.activeBuild;
const result = await this.activeBuild;
// After a successful, user-facing build the test binaries may have changed, so any prior
// pass/fail results in the Test Explorer are stale. Mark them outdated (muted) rather than
// leaving them misleadingly current. markOutdated defaults to false so internal builds
// (preTest, per-test/coverage builds during a Test Explorer run, API/programmatic builds)
// never invalidate results - in particular a build that runs inside an active test run must
// not retire that run's own results.
if (markOutdated && !showCommandOnly && result.exitCode === 0) {
this.cTestController.markTestResultsOutdated(this.sourceDir);
}
return result;
}

/**
Expand Down Expand Up @@ -2804,7 +2814,7 @@ export class CMakeProject {
if (cleanResult !== 0) {
return cleanResult;
}
return (await this.build()).exitCode;
return (await this.build(undefined, undefined, undefined, undefined, undefined, true)).exitCode;
}

async cleanConfigureAndBuild(trigger: ConfigureTrigger = ConfigureTrigger.api): Promise<number> {
Expand Down Expand Up @@ -2883,7 +2893,34 @@ export class CMakeProject {

async refreshTests(): Promise<number> {
const drv = await this.preTest();
return this.cTestController.refreshTests(drv);
try {
return await this.cTestController.refreshTests(drv);
} finally {
// Retire prior pass/fail so the Test Explorer shows results as outdated (muted) after the
// refresh, rather than misleadingly current. Runs in finally so results are still marked
// outdated even when discovery returns a non-zero status (e.g. no CTestTestfile) after a
// successful preTest build. If preTest throws (build failure), we never get here.
this.cTestController.markTestResultsOutdated(drv.sourceDir);
}
}

/**
* Post-build Test Explorer sync for the direct `type: cmake, command: build` task, which builds
* through the task terminal (not build()/preTest). Refreshes the CTest list into the Test
* Explorer WITHOUT triggering another build and marks prior results outdated. Best-effort and
* gated on test explorer integration so it never rebuilds or affects the task's exit code.
*/
async refreshTestsAfterExternalBuild(): Promise<void> {
const drv = await this.getCMakeDriverInstance();
if (!drv || !drv.config.testExplorerIntegrationEnabled) {
return;
}
try {
await this.cTestController.refreshTests(drv);
} finally {
// Retire prior results even if discovery throws, so stale pass/fail is never left looking current.
this.cTestController.markTestResultsOutdated(drv.sourceDir);
}
}

async runTest(testName: string): Promise<CommandResult> {
Expand Down
101 changes: 77 additions & 24 deletions src/cmakeTaskProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export enum CommandType {
package = "package",
workflow = "workflow",
clean = "clean",
cleanRebuild = "cleanRebuild"
cleanRebuild = "cleanRebuild",
refresh = "refresh"
}

const localizeCommandType = (cmd: CommandType): string => {
Expand Down Expand Up @@ -82,6 +83,9 @@ const localizeCommandType = (cmd: CommandType): string => {
case CommandType.cleanRebuild: {
return localize("clean.rebuild", "clean rebuild");
}
case CommandType.refresh: {
return localize("refresh", "refresh");
}
default: {
return "";
}
Expand Down Expand Up @@ -181,6 +185,7 @@ export class CMakeTaskProvider implements vscode.TaskProvider {
result.push(await CMakeTaskProvider.provideTask(CommandType.build, project.workspaceFolder, project.useCMakePresets, targets));
result.push(await CMakeTaskProvider.provideTask(CommandType.install, project.workspaceFolder, project.useCMakePresets));
result.push(await CMakeTaskProvider.provideTask(CommandType.test, project.workspaceFolder, project.useCMakePresets));
result.push(await CMakeTaskProvider.provideTask(CommandType.refresh, project.workspaceFolder, project.useCMakePresets));
result.push(await CMakeTaskProvider.provideTask(CommandType.package, project.workspaceFolder, project.useCMakePresets));
result.push(await CMakeTaskProvider.provideTask(CommandType.workflow, project.workspaceFolder, project.useCMakePresets));
result.push(await CMakeTaskProvider.provideTask(CommandType.clean, project.workspaceFolder, project.useCMakePresets));
Expand Down Expand Up @@ -244,28 +249,31 @@ export class CMakeTaskProvider implements vscode.TaskProvider {
}

public static async resolveInternalTask(task: CMakeTask): Promise<{ task: CMakeTask; exitCodePromise?: Promise<number | null> } | undefined> {
const execution: any = task.execution;
if (!execution) {
const definition: CMakeTaskDefinition = <any>task.definition;
// task.scope can be a WorkspaceFolder, TaskScope.Global, or TaskScope.Workspace.
// Only use it as a WorkspaceFolder if it's an object (not a number or null).
const workspaceFolder: vscode.WorkspaceFolder | undefined = (task.scope && typeof task.scope === 'object') ? task.scope as vscode.WorkspaceFolder : undefined;
let exitCodeResolve!: (exitCode: number | null) => void;
const exitCodePromise = new Promise<number | null>(resolve => {
exitCodeResolve = resolve;
});
const resolvedTask: CMakeTask = new vscode.Task(definition, workspaceFolder ?? vscode.TaskScope.Workspace, definition.label, CMakeTaskProvider.CMakeSourceStr,
new vscode.CustomExecution(async (resolvedDefinition: vscode.TaskDefinition): Promise<vscode.Pseudoterminal> => {
const terminal = new CustomBuildTaskTerminal(resolvedDefinition.command, resolvedDefinition.targets, workspaceFolder, resolvedDefinition.preset, resolvedDefinition.options);
const listener = terminal.onDidClose((exitCode) => {
listener.dispose();
exitCodeResolve(exitCode);
});
return terminal;
}), []);
return { task: resolvedTask, exitCodePromise };
}
return { task };
const definition: CMakeTaskDefinition = <any>task.definition;
// task.scope can be a WorkspaceFolder, TaskScope.Global, or TaskScope.Workspace.
// Only use it as a WorkspaceFolder if it's an object (not a number or null).
const workspaceFolder: vscode.WorkspaceFolder | undefined = (task.scope && typeof task.scope === 'object') ? task.scope as vscode.WorkspaceFolder : undefined;
let exitCodeResolve!: (exitCode: number | null) => void;
const exitCodePromise = new Promise<number | null>(resolve => {
exitCodeResolve = resolve;
});
// resolveInternalTask is called ONLY from CMakeDriver.build() for internal cmake.buildTask
// executions (e.g. preTest and coverage pre/post builds that may run during an active test
// run), so ALWAYS construct our own internal terminal (isInternalBuild=true) regardless of
// whether the incoming task already carried a CustomExecution. In particular findBuildTask's
// ambiguous/no-match fallback returns a provideTask-built task whose terminal defaults to
// isInternalBuild=false; returning it unchanged would let an internal build trip Hook C and
// retire the in-flight run's own results. Rebuilding it also gives us a real exitCodePromise.
const resolvedTask: CMakeTask = new vscode.Task(definition, workspaceFolder ?? vscode.TaskScope.Workspace, definition.label, CMakeTaskProvider.CMakeSourceStr,
new vscode.CustomExecution(async (resolvedDefinition: vscode.TaskDefinition): Promise<vscode.Pseudoterminal> => {
const terminal = new CustomBuildTaskTerminal(resolvedDefinition.command, resolvedDefinition.targets, workspaceFolder, resolvedDefinition.preset, resolvedDefinition.options, true);
const listener = terminal.onDidClose((exitCode) => {
listener.dispose();
exitCodeResolve(exitCode);
});
return terminal;
}), []);
return { task: resolvedTask, exitCodePromise };
}

public static async findBuildTask(workspaceFolder: string, presetName?: string, targets?: string[], expansionOptions?: expand.ExpansionOptions): Promise<CMakeTask | undefined> {
Expand Down Expand Up @@ -375,7 +383,7 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
return this.closeEmitter.event;
}

constructor(private command: string, private targets: string[], private workspaceFolder?: vscode.WorkspaceFolder, private preset?: string, private options?: { cwd?: string; environment?: Environment }) {
constructor(private command: string, private targets: string[], private workspaceFolder?: vscode.WorkspaceFolder, private preset?: string, private options?: { cwd?: string; environment?: Environment }, private isInternalBuild: boolean = false) {
super();
}

Expand Down Expand Up @@ -406,6 +414,9 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
case CommandType.test:
await this.runTestTask();
break;
case CommandType.refresh:
await this.runRefreshTask();
break;
case CommandType.package:
await this.runPackageTask();
break;
Expand Down Expand Up @@ -660,6 +671,20 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
} else {
this.writeEmitter.fire(localize("build.finished.successfully", "{0} finished successfully.", taskName) + endOfLine);
}
// On a successful `type: cmake, command: build` task (this path also backs the cleanRebuild
// task, which runs runBuildTask(CommandType.build)), refresh the test list and mark prior
// results outdated so the Test Explorer reflects the rebuilt binaries even when the view was
// not visible during the build (issue #5007). Best-effort: never fail the task or change its
// exit code, and never trigger another build. Skipped for internal builds (cmake.buildTask
// executions launched by CMakeDriver.build(), e.g. preTest/coverage builds during a test
// run) so they never retire an in-flight run's results.
if (!result.retc && commandType === CommandType.build && !this.isInternalBuild) {
try {
await project.refreshTestsAfterExternalBuild();
} catch (e) {
log.debug(localize("refresh.tests.after.build.failed", 'Failed to refresh tests after build: {0}', String(e)));
}
}
if (doCloseEmitter) {
this.closeEmitter.fire(result.retc ?? 0);
}
Expand Down Expand Up @@ -714,6 +739,34 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
}
}

private async runRefreshTask(): Promise<any> {
this.writeEmitter.fire(localize("refresh.started", "Refresh task started...") + endOfLine);

const project: CMakeProject | undefined = await this.getProject();
if (!project || !await this.isTaskCompatibleWithPresets(project)) {
return;
}
telemetry.logEvent("task", { taskType: "refresh", useCMakePresets: String(project.useCMakePresets) });

try {
// Mirrors the "CMake: Refresh Tests" command: it builds the default target if needed,
// then re-reads the CTest information and refreshes the Test Explorer. Exposing this as a
// task lets automation (e.g. builds launched from another extension, where the test view
// isn't visible during the build) refresh the test information afterwards. Uses the active
// test preset, the same as the command.
const result: number = await project.refreshTests();
this.writeEmitter.fire(localize('refresh.finished.with.code', 'Refresh finished with return code {0}', result) + endOfLine);
// Report success as long as the refresh itself did not throw. A negative return code is
// a non-fatal state (e.g. no CTestTestfile.cmake because the project defines no tests, or
// CTest not found) rather than a task failure; a failed build throws and is handled below.
// This matches the "CMake: Refresh Tests" command, which also ignores the numeric result.
this.closeEmitter.fire(0);
} catch (e) {
this.writeEmitter.fire(localize('refresh.failed', 'Refresh failed: {0}', String(e)) + endOfLine);
this.closeEmitter.fire(-1);
}
}

private async runPackageTask(): Promise<any> {
this.writeEmitter.fire(localize("package.started", "Package task started...") + endOfLine);

Expand Down
Loading
Loading