Skip to content

Commit 9590fc9

Browse files
committed
Add VS Code integration test project scaffolding
Adds an AppHost-aware Aspire CLI test template workflow and exposes it through a capability-gated VS Code command. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a7314ab-4780-4472-bee8-f265bab3de94
1 parent debd81b commit 9590fc9

28 files changed

Lines changed: 720 additions & 102 deletions

extension/loc/xlf/aspire-vscode.xlf

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@
300300
"title": "%command.new%",
301301
"category": "Aspire"
302302
},
303+
{
304+
"command": "aspire-vscode.addIntegrationTestProject",
305+
"title": "%command.addIntegrationTestProject%",
306+
"category": "Aspire",
307+
"enablement": "aspire.addIntegrationTestProjectSupported"
308+
},
303309
{
304310
"command": "aspire-vscode.init",
305311
"title": "%command.init%",
@@ -656,6 +662,10 @@
656662
}
657663
],
658664
"commandPalette": [
665+
{
666+
"command": "aspire-vscode.addIntegrationTestProject",
667+
"when": "aspire.addIntegrationTestProjectSupported"
668+
},
659669
{
660670
"command": "aspire-vscode.createWithAspire",
661671
"when": "false"

extension/package.nls.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"extension.debug.defaultConfiguration.name": "Aspire: Launch default AppHost",
1313
"extension.debug.defaultConfiguration.description": "Launch the effective Aspire AppHost in your workspace",
1414
"command.add": "Add an integration",
15+
"command.addIntegrationTestProject": "Add integration test project",
1516
"command.new": "New Aspire project",
1617
"command.init": "Initialize Aspire in an existing codebase",
1718
"command.createWithAspire": "Set up Aspire",
@@ -59,6 +60,8 @@
5960
"command.runPipelineStepAppHost": "Run pipeline step",
6061
"command.debugPipelineStepAppHost": "Debug pipeline step",
6162
"aspire-vscode.strings.noCsprojFound": "No AppHost found in the current workspace.",
63+
"aspire-vscode.strings.addIntegrationTestProjectRequiresCSharpAppHost": "Integration test project scaffolding requires a C# AppHost.",
64+
"aspire-vscode.strings.addIntegrationTestProjectUnsupported": "The selected Aspire CLI does not advertise integration test project scaffolding. Update or verify the Aspire CLI installation and try again.",
6265
"aspire-vscode.strings.error": "Error: {0}",
6366
"aspire-vscode.strings.yes": "Yes",
6467
"aspire-vscode.strings.no": "No",

extension/src/activation/registerCliCommands.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { isE2eBridgeEnabled } from '../testing/e2eStateFileBridge';
2424
import { registerInstrumentedCommand } from './instrumentedCommand';
2525
import { AppHostCommandTarget, getAppHostArgs } from '../utils/appHostArgs';
2626
import { getCliPathTargetForUri } from '../utils/cliPathVariables';
27+
import { addIntegrationTestProject } from '../commands/addIntegrationTestProject';
2728

2829
interface CommandInvocation {
2930
readonly target: CliPathResolutionTarget;
@@ -56,6 +57,20 @@ export function registerCliCommands(
5657
const openGlobalSettingsCommandRegistration = vscode.commands.registerCommand('aspire-vscode.openGlobalSettings', () => tryExecuteCommand('aspire-vscode.openGlobalSettings', terminalProvider, openGlobalSettingsCommand));
5758
const runAppHostCommandRegistration = registerInstrumentedCommand('aspire-vscode.runAppHostCommand', 'editor', () => editorCommandProvider.tryExecuteRunAppHost(true));
5859
const debugAppHostCommandRegistration = registerInstrumentedCommand('aspire-vscode.debugAppHostCommand', 'editor', () => editorCommandProvider.tryExecuteRunAppHost(false));
60+
const addIntegrationTestProjectRegistration = vscode.commands.registerCommand(
61+
'aspire-vscode.addIntegrationTestProject',
62+
() => tryExecuteCommand(
63+
'aspire-vscode.addIntegrationTestProject',
64+
terminalProvider,
65+
(tp, invocation, cliPath) => {
66+
const appHostPath = invocation.appHost?.appHostPath;
67+
if (!appHostPath) {
68+
throw new vscode.CancellationError();
69+
}
70+
71+
return addIntegrationTestProject(tp, configInfoProvider, appHostPath, invocation.target, cliPath);
72+
},
73+
() => selectAppHostCommandInvocation(editorCommandProvider, true)));
5974

6075
// Walkthrough commands (no CLI check - the CLI may not be installed yet).
6176
const installCliRegistration = registerInstrumentedCommand('aspire-vscode.installCli', 'walkthrough', installCliCommand);
@@ -78,6 +93,7 @@ export function registerCliCommands(
7893
openGlobalSettingsCommandRegistration,
7994
runAppHostCommandRegistration,
8095
debugAppHostCommandRegistration,
96+
addIntegrationTestProjectRegistration,
8197
installCliRegistration,
8298
verifyCliInstalledRegistration,
8399
];
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import {
4+
addIntegrationTestProjectRequiresCSharpAppHost,
5+
addIntegrationTestProjectUnsupported,
6+
} from '../loc/strings';
7+
import { aspireTestAppHostCapability } from '../types/configInfo';
8+
import { AspireTerminalProvider } from '../utils/AspireTerminalProvider';
9+
import { ConfigInfoProvider } from '../utils/configInfoProvider';
10+
import {
11+
CliPathResolutionTarget,
12+
windowCliPathTarget,
13+
workspaceFolderCliPathTarget,
14+
} from '../utils/cliPathVariables';
15+
import { extensionLogOutputChannel } from '../utils/logging';
16+
17+
export const addIntegrationTestProjectSupportedContext = 'aspire.addIntegrationTestProjectSupported';
18+
19+
export class AddIntegrationTestProjectAvailability implements vscode.Disposable {
20+
private _refreshGeneration = 0;
21+
private _disposed = false;
22+
23+
constructor(private readonly _configInfoProvider: ConfigInfoProvider) {
24+
}
25+
26+
async refresh(forceRefresh = false): Promise<void> {
27+
const generation = ++this._refreshGeneration;
28+
await this._publish(false, generation);
29+
if (this._disposed || generation !== this._refreshGeneration) {
30+
return;
31+
}
32+
33+
try {
34+
const supported = await this._configInfoProvider.hasCapability(
35+
aspireTestAppHostCapability,
36+
{
37+
target: getAvailabilityTarget(),
38+
forceRefresh,
39+
suppressErrors: true,
40+
});
41+
await this._publish(supported, generation);
42+
}
43+
catch (error) {
44+
if (!this._disposed && generation === this._refreshGeneration) {
45+
extensionLogOutputChannel.warn(`Unable to determine integration test scaffolding availability: ${String(error)}`);
46+
}
47+
}
48+
}
49+
50+
dispose(): void {
51+
this._disposed = true;
52+
this._refreshGeneration++;
53+
void vscode.commands.executeCommand('setContext', addIntegrationTestProjectSupportedContext, false);
54+
}
55+
56+
private async _publish(supported: boolean, generation: number): Promise<void> {
57+
if (!this._disposed && generation === this._refreshGeneration) {
58+
await vscode.commands.executeCommand(
59+
'setContext',
60+
addIntegrationTestProjectSupportedContext,
61+
supported);
62+
}
63+
}
64+
}
65+
66+
export async function addIntegrationTestProject(
67+
terminalProvider: AspireTerminalProvider,
68+
configInfoProvider: ConfigInfoProvider,
69+
appHostPath: string,
70+
target: CliPathResolutionTarget,
71+
cliPath: string,
72+
): Promise<void> {
73+
if (path.extname(appHostPath).toLowerCase() !== '.csproj') {
74+
await vscode.window.showErrorMessage(addIntegrationTestProjectRequiresCSharpAppHost);
75+
return;
76+
}
77+
78+
const supported = await configInfoProvider.hasCapability(
79+
aspireTestAppHostCapability,
80+
{
81+
cliPath,
82+
target,
83+
forceRefresh: true,
84+
suppressErrors: true,
85+
});
86+
if (!supported) {
87+
await vscode.window.showErrorMessage(addIntegrationTestProjectUnsupported);
88+
return;
89+
}
90+
91+
await terminalProvider.sendAspireCommandToAspireTerminal(
92+
['new', 'aspire-test'],
93+
true,
94+
['--apphost', appHostPath],
95+
{ cliPath, target });
96+
}
97+
98+
function getAvailabilityTarget(): CliPathResolutionTarget {
99+
const activeUri = vscode.window.activeTextEditor?.document.uri;
100+
const workspaceFolder = activeUri
101+
? vscode.workspace.getWorkspaceFolder(activeUri)
102+
: vscode.workspace.workspaceFolders?.[0];
103+
return workspaceFolder
104+
? workspaceFolderCliPathTarget(workspaceFolder)
105+
: windowCliPathTarget;
106+
}

extension/src/extension.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { registerInstrumentedCommand } from './activation/instrumentedCommand';
3737
import { registerCliCommands } from './activation/registerCliCommands';
3838
import { registerTreeViewCommands } from './activation/registerTreeViewCommands';
3939
import { registerCodeLensCommands } from './activation/registerCodeLensCommands';
40+
import { AddIntegrationTestProjectAvailability } from './commands/addIntegrationTestProject';
4041

4142
let aspireExtensionContext = new AspireExtensionContext();
4243

@@ -159,6 +160,19 @@ export async function activate(context: vscode.ExtensionContext) {
159160
vscode.commands.executeCommand('setContext', 'aspire.noRunningAppHosts', true);
160161
vscode.commands.executeCommand('setContext', 'aspire.loading', true);
161162

163+
const addIntegrationTestProjectAvailability = new AddIntegrationTestProjectAvailability(configInfoProvider);
164+
context.subscriptions.push(
165+
addIntegrationTestProjectAvailability,
166+
vscode.window.onDidChangeActiveTextEditor(() => void addIntegrationTestProjectAvailability.refresh()),
167+
vscode.workspace.onDidChangeWorkspaceFolders(() => void addIntegrationTestProjectAvailability.refresh()),
168+
cliPathResolver.onDidChangeForwarding(() => void addIntegrationTestProjectAvailability.refresh(true)),
169+
vscode.workspace.onDidChangeConfiguration(event => {
170+
if (event.affectsConfiguration('aspire.aspireCliExecutablePath')) {
171+
void addIntegrationTestProjectAvailability.refresh(true);
172+
}
173+
}));
174+
void addIntegrationTestProjectAvailability.refresh();
175+
162176
// Activate the data repository. Workspace describe watching and global polling begin when the panel is visible.
163177
dataRepository.activate();
164178

extension/src/loc/strings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as vscode from 'vscode';
22

33
export const noCsprojFound = vscode.l10n.t('No AppHost found in the current workspace.');
4+
export const addIntegrationTestProjectRequiresCSharpAppHost = vscode.l10n.t('Integration test project scaffolding requires a C# AppHost.');
5+
export const addIntegrationTestProjectUnsupported = vscode.l10n.t('The selected Aspire CLI does not advertise integration test project scaffolding. Update or verify the Aspire CLI installation and try again.');
46
// l10n.t only substitutes primitives, so passing an Error left the message as the literal "Error: {0}".
57
export const errorMessage = (error: unknown) => vscode.l10n.t('Error: {0}', error instanceof Error ? error.message : String(error));
68
export const yesLabel = vscode.l10n.t('Yes');

0 commit comments

Comments
 (0)