Skip to content

Commit a86afd6

Browse files
committed
Add tests
1 parent c315d20 commit a86afd6

4 files changed

Lines changed: 313 additions & 1 deletion

File tree

Extension/.vscode/launch.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@
9797
"label": "MultirootDeadlockTest ",
9898
"value": "${workspaceFolder}/test/scenarios/MultirootDeadlockTest/assets/test.code-workspace"
9999
},
100+
{
101+
"label": "RunWithoutDebugging ",
102+
"value": "${workspaceFolder}/test/scenarios/RunWithoutDebugging/assets/"
103+
},
100104
{
101105
"label": "SimpleCppProject ",
102106
"value": "${workspaceFolder}/test/scenarios/SimpleCppProject/assets/simpleCppProject.code-workspace"

Extension/src/Debugger/runWithoutDebuggingAdapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
134134
const bashArgs = ['bash', '-c', bashCmd];
135135

136136
// Terminal emulators in order of preference, with the correct flag style for each.
137-
const candidates: { cmd: string; buildArgs: () => string[] }[] = [
137+
const candidates: { cmd: string; buildArgs(): string[] }[] = [
138138
{ cmd: 'x-terminal-emulator', buildArgs: () => ['-e', ...bashArgs] },
139139
{ cmd: 'gnome-terminal', buildArgs: () => ['-e', ...bashArgs] },
140140
{ cmd: 'konsole', buildArgs: () => ['-e', ...bashArgs] },
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int main() {
2+
return 37;
3+
}
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
/* eslint-disable @typescript-eslint/triple-slash-reference */
6+
/// <reference path="../../../../vscode.d.ts" />
7+
import * as assert from 'assert';
8+
import * as cp from 'child_process';
9+
import { suite } from 'mocha';
10+
import * as path from 'path';
11+
import * as vscode from 'vscode';
12+
import * as util from '../../../../src/common';
13+
import { isLinux, isMacOS, isWindows } from '../../../../src/constants';
14+
import { getEffectiveEnvironment } from '../../../../src/LanguageServer/devcmd';
15+
16+
interface ProcessResult {
17+
code: number | null;
18+
stdout: string;
19+
stderr: string;
20+
}
21+
22+
interface TrackerState {
23+
setBreakpointsRequestReceived: boolean;
24+
stoppedEventReceived: boolean;
25+
exitedEventReceived: boolean;
26+
exitedBeforeStop: boolean;
27+
actualExitCode?: number;
28+
}
29+
30+
interface TrackerController {
31+
state: TrackerState;
32+
lastEvent: Promise<'stopped' | 'exited'>;
33+
dispose(): void;
34+
}
35+
36+
function runProcess(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<ProcessResult> {
37+
return new Promise((resolve, reject) => {
38+
const child = cp.spawn(command, args, { cwd, env });
39+
let stdout = '';
40+
let stderr = '';
41+
42+
child.stdout.on('data', (data: Buffer) => {
43+
stdout += data.toString();
44+
});
45+
46+
child.stderr.on('data', (data: Buffer) => {
47+
stderr += data.toString();
48+
});
49+
50+
child.on('error', reject);
51+
child.on('close', (code) => resolve({ code, stdout, stderr }));
52+
});
53+
}
54+
55+
async function setWindowsBuildEnvironment(): Promise<void> {
56+
const promise = vscode.commands.executeCommand('C_Cpp.SetVsDeveloperEnvironment', 'test');
57+
const timer = setInterval(() => {
58+
void vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
59+
}, 1000);
60+
await promise;
61+
clearInterval(timer);
62+
assert.strictEqual(util.hasMsvcEnvironment(), true, 'MSVC environment not set correctly.');
63+
}
64+
65+
async function compileProgram(workspacePath: string, sourcePath: string, outputPath: string): Promise<void> {
66+
if (isWindows) {
67+
await setWindowsBuildEnvironment();
68+
const env = getEffectiveEnvironment();
69+
const result = await runProcess('cl.exe', ['/nologo', '/EHsc', '/Zi', '/std:c++17', `/Fe:${outputPath}`, sourcePath], workspacePath, env);
70+
assert.strictEqual(result.code, 0, `MSVC compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`);
71+
return;
72+
}
73+
74+
if (isMacOS) {
75+
const result = await runProcess('clang++', ['-std=c++17', '-g', sourcePath, '-o', outputPath], workspacePath);
76+
assert.strictEqual(result.code, 0, `clang++ compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`);
77+
return;
78+
}
79+
80+
if (isLinux) {
81+
const result = await runProcess('g++', ['-std=c++17', '-g', sourcePath, '-o', outputPath], workspacePath);
82+
assert.strictEqual(result.code, 0, `g++ compilation failed. stdout: ${result.stdout}\nstderr: ${result.stderr}`);
83+
return;
84+
}
85+
86+
assert.fail(`Unsupported test platform: ${process.platform}`);
87+
}
88+
89+
async function createBreakpointAtReturnStatement(sourceUri: vscode.Uri): Promise<vscode.SourceBreakpoint> {
90+
const document = await vscode.workspace.openTextDocument(sourceUri);
91+
const returnLine = document.getText().split(/\r?\n/).findIndex((line) => line.includes('return 37;'));
92+
assert.notStrictEqual(returnLine, -1, 'Unable to find expected return statement for breakpoint placement.');
93+
const breakpoint = new vscode.SourceBreakpoint(new vscode.Location(sourceUri, new vscode.Position(returnLine, 0)), true);
94+
vscode.debug.addBreakpoints([breakpoint]);
95+
return breakpoint;
96+
}
97+
98+
function createSessionTerminatedPromise(sessionName: string): Promise<void> {
99+
return new Promise<void>((resolve) => {
100+
const terminateSubscription = vscode.debug.onDidTerminateDebugSession((session) => {
101+
if (session.name === sessionName) {
102+
terminateSubscription.dispose();
103+
resolve();
104+
}
105+
});
106+
});
107+
}
108+
109+
function createTracker(debugType: string, sessionName: string, timeoutMs: number, timeoutMessage: string): TrackerController {
110+
const state: TrackerState = {
111+
setBreakpointsRequestReceived: false,
112+
stoppedEventReceived: false,
113+
exitedEventReceived: false,
114+
exitedBeforeStop: false
115+
};
116+
117+
let trackerRegistration: vscode.Disposable | undefined;
118+
let timeoutHandle: NodeJS.Timeout | undefined;
119+
120+
const lastEvent = new Promise<'stopped' | 'exited'>((resolve, reject) => {
121+
timeoutHandle = setTimeout(() => {
122+
trackerRegistration?.dispose();
123+
trackerRegistration = undefined;
124+
reject(new Error(timeoutMessage));
125+
}, timeoutMs);
126+
127+
trackerRegistration = vscode.debug.registerDebugAdapterTrackerFactory(debugType, {
128+
createDebugAdapterTracker: (session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined => {
129+
if (session.name !== sessionName) {
130+
return undefined;
131+
}
132+
133+
return {
134+
onWillReceiveMessage: (message: any): void => {
135+
if (message?.type === 'request' && message?.command === 'setBreakpoints') {
136+
state.setBreakpointsRequestReceived = true;
137+
}
138+
},
139+
onDidSendMessage: (message: any): void => {
140+
if (message?.type !== 'event') {
141+
return;
142+
}
143+
144+
if (message.event === 'stopped') {
145+
state.stoppedEventReceived = true;
146+
if (timeoutHandle) {
147+
clearTimeout(timeoutHandle);
148+
timeoutHandle = undefined;
149+
}
150+
resolve('stopped');
151+
}
152+
153+
if (message.event === 'exited') {
154+
state.exitedEventReceived = true;
155+
state.actualExitCode = message.body?.exitCode;
156+
if (!state.stoppedEventReceived) {
157+
state.exitedBeforeStop = true;
158+
}
159+
if (timeoutHandle) {
160+
clearTimeout(timeoutHandle);
161+
timeoutHandle = undefined;
162+
}
163+
resolve('exited');
164+
}
165+
}
166+
};
167+
}
168+
});
169+
});
170+
171+
return {
172+
state,
173+
lastEvent,
174+
dispose(): void {
175+
if (timeoutHandle) {
176+
clearTimeout(timeoutHandle);
177+
timeoutHandle = undefined;
178+
}
179+
trackerRegistration?.dispose();
180+
trackerRegistration = undefined;
181+
}
182+
};
183+
}
184+
185+
suite('Run Without Debugging Integration Test', function (): void {
186+
187+
suiteSetup(async function (): Promise<void> {
188+
const extension: vscode.Extension<any> = vscode.extensions.getExtension('ms-vscode.cpptools') || assert.fail('Extension not found');
189+
if (!extension.isActive) {
190+
await extension.activate();
191+
}
192+
});
193+
194+
suiteTeardown(async function (): Promise<void> {
195+
if (isWindows) {
196+
await vscode.commands.executeCommand('C_Cpp.ClearVsDeveloperEnvironment');
197+
}
198+
});
199+
200+
test('Run Without Debugging should not break on breakpoints and emit expected exit code', async () => {
201+
const expectedExitCode = 37;
202+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available');
203+
const workspacePath = workspaceFolder.uri.fsPath;
204+
const sourceFile = path.join(workspacePath, 'exitCode.cpp');
205+
const sourceUri = vscode.Uri.file(sourceFile);
206+
const executableName = isWindows ? 'exitCodeProgram.exe' : 'exitCodeProgram';
207+
const executablePath = path.join(workspacePath, executableName);
208+
const sessionName = 'Run Without Debugging Exit Code';
209+
const debugType = isWindows ? 'cppvsdbg' : 'cppdbg';
210+
211+
await compileProgram(workspacePath, sourceFile, executablePath);
212+
213+
const breakpoint = await createBreakpointAtReturnStatement(sourceUri);
214+
const tracker = createTracker(debugType, sessionName, 30000, 'Timed out waiting for debugger event.');
215+
const debugSessionTerminated = createSessionTerminatedPromise(sessionName);
216+
217+
try {
218+
const started = await vscode.debug.startDebugging(
219+
workspaceFolder,
220+
{
221+
name: sessionName,
222+
type: debugType,
223+
request: 'launch',
224+
program: executablePath,
225+
args: [],
226+
cwd: workspacePath,
227+
console: 'internalConsole'
228+
},
229+
{ noDebug: true });
230+
231+
assert.strictEqual(started, true, 'The noDebug launch did not start successfully.');
232+
233+
const lastEvent = await tracker.lastEvent;
234+
await debugSessionTerminated;
235+
236+
assert.strictEqual(lastEvent, 'exited', 'No-debug launch should exit rather than stop on a breakpoint.');
237+
assert.strictEqual(tracker.state.setBreakpointsRequestReceived, false, 'a "no debug" session should not send setBreakpoints requests.');
238+
assert.strictEqual(tracker.state.stoppedEventReceived, false, 'a "no debug" session should not emit stopped events.');
239+
assert.strictEqual(tracker.state.actualExitCode, expectedExitCode, 'Unexpected exit code from run without debugging launch.');
240+
} finally {
241+
tracker.dispose();
242+
vscode.debug.removeBreakpoints([breakpoint]);
243+
}
244+
});
245+
246+
test('Debug launch should bind and stop at the breakpoint', async () => {
247+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available');
248+
const workspacePath = workspaceFolder.uri.fsPath;
249+
const sourceFile = path.join(workspacePath, 'exitCode.cpp');
250+
const sourceUri = vscode.Uri.file(sourceFile);
251+
const executableName = isWindows ? 'exitCodeProgram.exe' : 'exitCodeProgram';
252+
const executablePath = path.join(workspacePath, executableName);
253+
const sessionName = 'Debug Launch Breakpoint Stop';
254+
const debugType = isWindows ? 'cppvsdbg' : 'cppdbg';
255+
256+
await compileProgram(workspacePath, sourceFile, executablePath);
257+
258+
const breakpoint = await createBreakpointAtReturnStatement(sourceUri);
259+
260+
let launchedSession: vscode.DebugSession | undefined;
261+
const tracker = createTracker(debugType, sessionName, 45000, 'Timed out waiting for debugger event in normal debug mode.');
262+
263+
const startedSubscription = vscode.debug.onDidStartDebugSession((session) => {
264+
if (session.name === sessionName) {
265+
launchedSession = session;
266+
}
267+
});
268+
269+
const debugSessionTerminated = createSessionTerminatedPromise(sessionName);
270+
271+
try {
272+
const started = await vscode.debug.startDebugging(
273+
workspaceFolder,
274+
{
275+
name: sessionName,
276+
type: debugType,
277+
request: 'launch',
278+
program: executablePath,
279+
args: [],
280+
cwd: workspacePath,
281+
console: 'internalConsole'
282+
},
283+
{ noDebug: false });
284+
285+
assert.strictEqual(started, true, 'The debug launch did not start successfully.');
286+
287+
const lastEvent = await tracker.lastEvent;
288+
289+
assert.strictEqual(lastEvent, 'stopped', 'Debug launch should stop at the breakpoint before exit.');
290+
assert.strictEqual(tracker.state.setBreakpointsRequestReceived, true, 'Debug mode should send setBreakpoints requests.');
291+
assert.strictEqual(tracker.state.stoppedEventReceived, true, 'Debug mode should emit a stopped event at the breakpoint.');
292+
assert.strictEqual(tracker.state.exitedBeforeStop, false, 'Program exited before stopping at breakpoint in debug mode.');
293+
assert.strictEqual(vscode.debug.activeDebugSession?.name, sessionName, 'Debug session should still be active at breakpoint.');
294+
295+
const stoppedSession = launchedSession ?? vscode.debug.activeDebugSession;
296+
assert.ok(stoppedSession, 'Unable to identify the running debug session for termination.');
297+
await vscode.debug.stopDebugging(stoppedSession);
298+
await debugSessionTerminated;
299+
} finally {
300+
startedSubscription.dispose();
301+
tracker.dispose();
302+
vscode.debug.removeBreakpoints([breakpoint]);
303+
}
304+
});
305+
});

0 commit comments

Comments
 (0)