Skip to content

Commit 278cfc0

Browse files
committed
Add context-aware test commands to extension
1 parent c342b64 commit 278cfc0

8 files changed

Lines changed: 177 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Once installed, the extension automatically activates when you open a \`.vb\` fi
130130
If you have multiple solutions, use the Command Palette action **"VB.NET: Select Workspace Solution"** to choose the active \`.sln/.slnf/.slnx\`.
131131
For diagnostics, use **"VB.NET: Show Logs"** and **"VB.NET: Toggle LSP Trace"**.
132132
If assets are missing, run **"VB.NET: Restore Workspace"** or **"VB.NET: Restore Project"**.
133+
To run tests scoped to the active file, solution, or project, use **"VB.NET: Run Tests"** (or **"VB.NET: Debug Tests (Preview)"**).
133134
To force a reanalysis without restarting VS Code, use **"VB.NET: Reload Workspace"**.
134135
For attaching to a running .NET process, use **"VB.NET: Attach Debugger to Process"**.
135136

docs/configuration.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
**`VB.NET` Language Support - User Configuration**
44

55
Version: 1.1
6-
Last Updated: 2026-01-20
6+
Last Updated: 2026-01-22
77

88
## Table of Contents
99

@@ -30,6 +30,8 @@ Last Updated: 2026-01-20
3030
- **`VB.NET: Toggle LSP Trace`** — toggles `vbnet.trace.server` between `off` and `verbose`.
3131
- **`VB.NET: Restore Workspace`** — runs `dotnet restore` using the selected solution (or workspace root).
3232
- **`VB.NET: Restore Project`** — picks a `.vbproj` file and runs `dotnet restore` for it.
33+
- **`VB.NET: Run Tests`** — runs `dotnet test` using the active `.vb` context, selected solution, or project.
34+
- **`VB.NET: Debug Tests (Preview)`** — runs `dotnet test` and notes that debug attach is not yet implemented.
3335
- **`VB.NET: Reload Workspace`** — requests a workspace reload without restarting the extension.
3436
- **`VB.NET: Attach Debugger to Process`** — picks a running process and starts an attach session.
3537

@@ -740,7 +742,7 @@ If configuration issues persist:
740742

741743
---
742744

743-
**Last Updated**: 2026-01-18
745+
**Last Updated**: 2026-01-22
744746

745747
**Maintained by**: `VB.NET` Language Support Contributors
746748

src/extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,16 @@
242242
"title": "Restore VB.NET Project",
243243
"category": "VB.NET"
244244
},
245+
{
246+
"command": "vbnet.runTestsInContext",
247+
"title": "Run VB.NET Tests",
248+
"category": "VB.NET"
249+
},
250+
{
251+
"command": "vbnet.debugTestsInContext",
252+
"title": "Debug VB.NET Tests (Preview)",
253+
"category": "VB.NET"
254+
},
245255
{
246256
"command": "vbnet.reloadWorkspace",
247257
"title": "Reload VB.NET Workspace",

src/extension/src/extension.ts

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,31 @@ function registerCommands(context: vscode.ExtensionContext): void {
271271
}
272272
})
273273
);
274+
275+
context.subscriptions.push(
276+
vscode.commands.registerCommand('vbnet.runTestsInContext', async () => {
277+
try {
278+
await runTestsInContext();
279+
} catch (error) {
280+
const message = error instanceof Error ? error.message : String(error);
281+
outputChannel?.appendLine(`Failed to run tests: ${message}`);
282+
vscode.window.showErrorMessage(`Failed to run tests: ${message}`);
283+
}
284+
})
285+
);
286+
287+
context.subscriptions.push(
288+
vscode.commands.registerCommand('vbnet.debugTestsInContext', async () => {
289+
try {
290+
vscode.window.showInformationMessage('Debug test support is in preview; running dotnet test for now.');
291+
await runTestsInContext(true);
292+
} catch (error) {
293+
const message = error instanceof Error ? error.message : String(error);
294+
outputChannel?.appendLine(`Failed to debug tests: ${message}`);
295+
vscode.window.showErrorMessage(`Failed to debug tests: ${message}`);
296+
}
297+
})
298+
);
274299
}
275300

276301
interface SolutionPickItem extends vscode.QuickPickItem {
@@ -292,6 +317,10 @@ interface ProcessPickItem extends vscode.QuickPickItem {
292317
pid: number;
293318
}
294319

320+
interface TestPickItem extends vscode.QuickPickItem {
321+
targetPath?: string;
322+
}
323+
295324
async function selectWorkspaceSolution(): Promise<void> {
296325
const workspaceFolders = vscode.workspace.workspaceFolders;
297326
if (!workspaceFolders || workspaceFolders.length === 0) {
@@ -429,7 +458,7 @@ async function restoreWorkspace(): Promise<void> {
429458
}
430459

431460
const label = candidateSolution ? `Restoring ${path.basename(candidateSolution)}` : 'Restoring workspace';
432-
await runDotnetCommand(args, workspaceRoot, label);
461+
await runDotnetCommand(args, workspaceRoot, label, 'Restore completed.');
433462
}
434463

435464
async function restoreProject(): Promise<void> {
@@ -465,7 +494,30 @@ async function restoreProject(): Promise<void> {
465494
return;
466495
}
467496

468-
await runDotnetCommand(['restore', pick.projectPath], workspaceRoot, `Restoring ${pick.label}`);
497+
await runDotnetCommand(['restore', pick.projectPath], workspaceRoot, `Restoring ${pick.label}`, 'Restore completed.');
498+
}
499+
500+
async function runTestsInContext(debug: boolean = false): Promise<void> {
501+
const workspaceRoot = getWorkspaceRoot();
502+
if (!workspaceRoot) {
503+
return;
504+
}
505+
506+
const target = await resolveTestTarget(workspaceRoot);
507+
const args = ['test'];
508+
if (target) {
509+
args.push(target);
510+
}
511+
512+
const title = target
513+
? `${debug ? 'Debugging' : 'Running'} tests: ${path.basename(target)}`
514+
: `${debug ? 'Debugging' : 'Running'} tests`;
515+
516+
const successMessage = debug
517+
? 'Test run completed (debug attach not yet implemented).'
518+
: 'Test run completed.';
519+
520+
await runDotnetCommand(args, workspaceRoot, title, successMessage);
469521
}
470522

471523
function getWorkspaceRoot(): string | undefined {
@@ -517,6 +569,59 @@ async function pickWorkspaceSolutionCandidate(workspaceRoot: string): Promise<st
517569
return candidates[0];
518570
}
519571

572+
async function resolveTestTarget(workspaceRoot: string): Promise<string | undefined> {
573+
const activeFile = vscode.window.activeTextEditor?.document?.uri?.fsPath;
574+
if (activeFile && activeFile.startsWith(workspaceRoot) && activeFile.toLowerCase().endsWith('.vb')) {
575+
const nearestProject = findNearestProjectForFile(activeFile, workspaceRoot);
576+
if (nearestProject) {
577+
return nearestProject;
578+
}
579+
}
580+
581+
const configuredSolution = getConfiguredSolutionPath(workspaceRoot);
582+
if (configuredSolution) {
583+
return configuredSolution;
584+
}
585+
586+
const candidateSolution = await pickWorkspaceSolutionCandidate(workspaceRoot);
587+
if (candidateSolution) {
588+
return candidateSolution;
589+
}
590+
591+
const projects = await findWorkspaceProjects();
592+
if (projects.length === 1) {
593+
return projects[0];
594+
}
595+
596+
if (projects.length > 1) {
597+
const items: TestPickItem[] = projects.map((projectPath) => {
598+
const relative = path.relative(workspaceRoot, projectPath);
599+
const label = relative && !relative.startsWith('..') && !path.isAbsolute(relative)
600+
? relative
601+
: path.basename(projectPath);
602+
return {
603+
label,
604+
detail: projectPath,
605+
targetPath: projectPath
606+
};
607+
});
608+
609+
items.unshift({
610+
label: 'Workspace (dotnet test)',
611+
description: 'Run tests without an explicit project/solution'
612+
});
613+
614+
const pick = await vscode.window.showQuickPick(items, {
615+
placeHolder: 'Select a project/solution to test',
616+
matchOnDescription: true
617+
});
618+
619+
return pick?.targetPath;
620+
}
621+
622+
return undefined;
623+
}
624+
520625
async function findWorkspaceSolutions(): Promise<string[]> {
521626
const config = vscode.workspace.getConfiguration('vbnet');
522627
const defaultExclude = '**/node_modules/**,**/.git/**,**/bower_components/**';
@@ -543,7 +648,7 @@ async function findWorkspaceProjects(): Promise<string[]> {
543648
return resources.map((resource) => resource.fsPath);
544649
}
545650

546-
async function runDotnetCommand(args: string[], cwd: string, title: string): Promise<void> {
651+
async function runDotnetCommand(args: string[], cwd: string, title: string, successMessage?: string): Promise<void> {
547652
outputChannel?.show();
548653
outputChannel?.appendLine(`Running: dotnet ${args.join(' ')}`);
549654

@@ -578,7 +683,9 @@ async function runDotnetCommand(args: string[], cwd: string, title: string): Pro
578683
})
579684
);
580685

581-
vscode.window.showInformationMessage('Restore completed.');
686+
if (successMessage) {
687+
vscode.window.showInformationMessage(successMessage);
688+
}
582689
}
583690

584691
function fsPathExists(filePath: string): boolean {
@@ -589,6 +696,34 @@ function fsPathExists(filePath: string): boolean {
589696
}
590697
}
591698

699+
function findNearestProjectForFile(filePath: string, workspaceRoot: string): string | undefined {
700+
let current = path.dirname(filePath);
701+
const root = path.resolve(workspaceRoot);
702+
703+
while (current.startsWith(root)) {
704+
try {
705+
const entries = fs.readdirSync(current);
706+
const candidates = entries.filter((entry) => entry.toLowerCase().endsWith('.vbproj'));
707+
if (candidates.length > 0) {
708+
return path.join(current, candidates[0]);
709+
}
710+
} catch {
711+
return undefined;
712+
}
713+
714+
if (current === root) {
715+
break;
716+
}
717+
const parent = path.dirname(current);
718+
if (parent === current) {
719+
break;
720+
}
721+
current = parent;
722+
}
723+
724+
return undefined;
725+
}
726+
592727
async function attachToProcess(): Promise<void> {
593728
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
594729
const processes = await listProcesses();

test-explore/TEST_RESULTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ Optional flags:
4747

4848
## Recent runs
4949

50+
### 2026-01-22 — CI-safe tests (VB.NET only)
51+
52+
Commands:
53+
- `dotnet test test\VbNet.LanguageServer.Tests.Vb\VbNet.LanguageServer.Tests.Vb.vbproj -c Release`
54+
- `dotnet test test\VbNet.Extension.Tests.Vb\VbNet.Extension.Tests.Vb.vbproj -c Release`
55+
56+
Outcome: PASS (150/150, 6/6)
57+
58+
### 2026-01-22 — VS Code harness (VB.NET server, test commands)
59+
60+
Command:
61+
- `cd test-explore\\clients\\vscode; npm test`
62+
63+
Outcome: PASS (14 passing, 5 pending; non-fatal DAP warnings)
64+
Notes:
65+
- DAP warnings: `Failed command 'threads' : 0x80004005`.
66+
- DAP trace: `test-explore/clients/vscode/logs/dap-trace-2026-01-22T203652019Z.log`.
67+
5068
### 2026-01-22 — VS Code harness (VB.NET server, attach command)
5169

5270
Command:

test-explore/TEST_SUITE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Purpose: Validate extension activation, configuration, and editor integrations.
9292
Targets:
9393
- Activation events (.vb file open, workspace containing .sln/.vbproj).
9494
- Output channels and log level propagation.
95-
- Commands (restart server, select solution, show output).
95+
- Commands (restart server, select solution, show output, restore, reload, attach, test runs).
9696
- Configuration changes (debounce and diagnostics mode).
9797
- Workspace trust behavior (limited activation if applicable).
9898

test-explore/clients/vscode/src/suite/vbnet-extension.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ if (skipVbnetSmoke) {
279279
assert.ok(commands.includes("vbnet.toggleLspTrace"), "Toggle LSP trace command not registered.");
280280
assert.ok(commands.includes("vbnet.restoreWorkspace"), "Restore workspace command not registered.");
281281
assert.ok(commands.includes("vbnet.restoreProject"), "Restore project command not registered.");
282+
assert.ok(commands.includes("vbnet.runTestsInContext"), "Run tests command not registered.");
283+
assert.ok(commands.includes("vbnet.debugTestsInContext"), "Debug tests command not registered.");
282284
assert.ok(commands.includes("vbnet.reloadWorkspace"), "Reload workspace command not registered.");
283285
assert.ok(commands.includes("vbnet.attachToProcess"), "Attach to process command not registered.");
284286
await config.update("trace.server", "verbose", vscode.ConfigurationTarget.Workspace);

test/VbNet.Extension.Tests.Vb/ExtensionManifestTests.vb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Namespace VbNet.Extension.Tests
7171
"vbnet.toggleLspTrace",
7272
"vbnet.restoreWorkspace",
7373
"vbnet.restoreProject",
74+
"vbnet.runTestsInContext",
75+
"vbnet.debugTestsInContext",
7476
"vbnet.reloadWorkspace",
7577
"vbnet.attachToProcess"
7678
}

0 commit comments

Comments
 (0)