Skip to content

Commit 15cd4a0

Browse files
committed
[DebugAdapter] Add test coverage for launch requests
This required breaking out the VS Code UI effects and external process/fs methods into wrappers that can be stubbed when executing the tests. While I was creating the ProcessRunner, I modified it to move away from the platform dependent `wc -l` and into a more node native fs.readFileSync to count the lines of the log file.
1 parent fba4697 commit 15cd4a0

File tree

2 files changed

+189
-68
lines changed

2 files changed

+189
-68
lines changed

editors/code/src/debugAdapter.ts

Lines changed: 115 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,96 @@ import {
1111
Handles,
1212
} from '@vscode/debugadapter';
1313
import { DebugProtocol } from '@vscode/debugprotocol';
14+
import { execFileSync as nodeExecFileSync } from 'child_process';
15+
import * as fs from 'fs';
1416
import * as vscode from 'vscode';
1517
import * as path from 'path';
1618

1719
import { outputChannel } from './extension';
1820
import { LogDebugger } from './logDebugger';
1921

22+
export interface ProcessRunner {
23+
execFileSync(file: string, args: string[]): Buffer;
24+
readFile(path: string): Buffer;
25+
}
26+
27+
const defaultProcessRunner: ProcessRunner = {
28+
execFileSync: (file: string, args: string[]): Buffer =>
29+
nodeExecFileSync(file, args) as Buffer,
30+
readFile: (path: string): Buffer =>
31+
fs.readFileSync(path)
32+
};
33+
34+
export interface EditorEffects {
35+
openAndFocus(log: string, line: number): void;
36+
highlightLine(log: string, line: number): void;
37+
clearHighlights(): void;
38+
}
39+
40+
class DefaultEditorEffects implements EditorEffects {
41+
private readonly _highlightDecoration: vscode.TextEditorDecorationType;
42+
43+
constructor() {
44+
const focusColor = new vscode.ThemeColor('editor.focusedStackFrameHighlightBackground');
45+
this._highlightDecoration = vscode.window.createTextEditorDecorationType({
46+
backgroundColor: focusColor
47+
});
48+
}
49+
50+
public openAndFocus(log: string, line: number): void {
51+
const editors = this.findEditors(log);
52+
if (editors.length >= 1) {
53+
this.focusEditor(editors[0], line);
54+
} else {
55+
Promise.resolve(vscode.workspace.openTextDocument(log))
56+
.then(doc => {
57+
return vscode.window.showTextDocument(doc, {
58+
viewColumn: vscode.ViewColumn.Beside,
59+
preserveFocus: false
60+
});
61+
})
62+
.then(editor => {
63+
this.focusEditor(editor, line);
64+
return editor;
65+
})
66+
.catch(error => {
67+
const message = `Failed to open log file: ${error.message}`;
68+
outputChannel.appendLine(message);
69+
console.error(message);
70+
});
71+
}
72+
}
73+
74+
public highlightLine(log: string, line: number): void {
75+
const editor = this.findEditors(log);
76+
if (editor.length > 0) {
77+
this.focusEditor(editor[0], line);
78+
}
79+
}
80+
81+
public clearHighlights(): void {
82+
vscode.window.visibleTextEditors.forEach((editor) => editor.setDecorations(this._highlightDecoration, []));
83+
}
84+
85+
private findEditors(log: string): vscode.TextEditor[] {
86+
const target = path.resolve(log);
87+
return vscode.window.visibleTextEditors.filter((editor) => path.resolve(editor.document.fileName) === target);
88+
}
89+
90+
private focusEditor(editor: vscode.TextEditor, line: number): void {
91+
const start = Math.max(0, line - 1);
92+
let range = new vscode.Range(
93+
new vscode.Position(start, 0),
94+
new vscode.Position(start, Number.MAX_VALUE)
95+
);
96+
editor.setDecorations(this._highlightDecoration, [range]);
97+
editor.revealRange(
98+
range,
99+
vscode.TextEditorRevealType.InCenter
100+
);
101+
}
102+
}
103+
20104
interface CallSite {
21105
name: string,
22106
sourcePath: string,
@@ -77,20 +161,29 @@ export class BinaryNotFoundError extends Error {
77161

78162
export class DebugSession extends LoggingDebugSession {
79163

80-
private static _threadID = 1;
164+
// prefer constant to be all caps
165+
// eslint-disable-next-line
166+
private static readonly NEWLINE = '\n'.charCodeAt(0);
167+
168+
private static readonly _threadID = 1;
81169
private _binaryPath: string;
82170
private readonly _variableHandles = new Handles<'locals'>();
83171
private _launchArgs: ILaunchRequestArguments = { source: "", log: "", log_format: "" };
84-
private readonly _highlightDecoration: vscode.TextEditorDecorationType;
85172
private _mapping?: LogMapping = undefined;
86173
private readonly _logDebugger: LogDebugger;
174+
private readonly _processRunner: ProcessRunner;
175+
private readonly _editorEffects: EditorEffects;
87176

88177
/**
89178
* Create a new debug adapter to use with a debug session.
90179
*/
91-
public constructor(logDebugger: LogDebugger) {
180+
public constructor(
181+
logDebugger: LogDebugger,
182+
processRunner: ProcessRunner = defaultProcessRunner,
183+
editorEffects: EditorEffects = new DefaultEditorEffects()
184+
) {
92185
super("log2src-dap.txt");
93-
186+
this._editorEffects = editorEffects;
94187
this._binaryPath = PLATFORM_TO_BINARY.get(`${process.platform}-${process.arch}`)!;
95188

96189
if (!this._binaryPath) {
@@ -100,17 +193,15 @@ export class DebugSession extends LoggingDebugSession {
100193
}
101194

102195
this._logDebugger = logDebugger;
196+
this._processRunner = processRunner;
103197
this.setDebuggerLinesStartAt1(true);
104198
this.setDebuggerColumnsStartAt1(true);
105-
106-
const focusColor = new vscode.ThemeColor('editor.focusedStackFrameHighlightBackground');
107-
this._highlightDecoration = vscode.window.createTextEditorDecorationType({ "backgroundColor": focusColor });
108199
outputChannel.appendLine("Starting up...");
109200
}
110201

111202
protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request): void {
112203
console.log(`disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`);
113-
vscode.window.visibleTextEditors.forEach((editor) => editor.setDecorations(this._highlightDecoration, []));
204+
this._editorEffects.clearHighlights();
114205
this.sendResponse(response);
115206
}
116207

@@ -133,7 +224,12 @@ export class DebugSession extends LoggingDebugSession {
133224
protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments) {
134225
console.log(`setBreakPointsRequest ${JSON.stringify(args)}`);
135226

136-
const source = args.source.path as string;
227+
const source = args.source.path;
228+
if (!source) {
229+
response.body = { breakpoints: [] };
230+
this.sendResponse(response);
231+
return;
232+
}
137233
const bps = args.breakpoints || [];
138234
const breakpoints = this._logDebugger.setBreakpoints(source, bps);
139235

@@ -159,42 +255,18 @@ export class DebugSession extends LoggingDebugSession {
159255
logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false);
160256

161257
this._launchArgs = args;
162-
this.openLogAndFocus();
163-
const execFile = require('child_process').execFileSync;
164-
let stdout = execFile('wc', ['-l', this._launchArgs.log]);
165-
const logLines = +stdout.toString().trim().split(" ")[0] || Number.MAX_VALUE
166-
this._logDebugger.setToLog(this._launchArgs.log, logLines);
258+
const log = this._launchArgs.log;
259+
const logContent = this._processRunner.readFile(log);
260+
const logLines = logContent.reduce((count, byte) => byte === DebugSession.NEWLINE ? count + 1 : count, 0) || Number.MAX_VALUE;
261+
this._logDebugger.setToLog(log, logLines);
262+
this._editorEffects.openAndFocus(log, this._logDebugger.linenum());
167263

168264
if (!this._logDebugger.hasBreakpoints()) {
169265
this.sendEvent(new StoppedEvent('entry', DebugSession._threadID));
170266
}
171267
this.sendResponse(response);
172268
}
173269

174-
private openLogAndFocus() {
175-
const editors = this.findEditors();
176-
if (editors.length >= 1) {
177-
this.focusEditor(editors[0]);
178-
} else {
179-
Promise.resolve(vscode.workspace.openTextDocument(this._launchArgs.log))
180-
.then(doc => {
181-
return vscode.window.showTextDocument(doc, {
182-
viewColumn: vscode.ViewColumn.Beside,
183-
preserveFocus: false
184-
});
185-
})
186-
.then(editor => {
187-
this.focusEditor(editor);
188-
return editor;
189-
})
190-
.catch(error => {
191-
const message = `Failed to open log file: ${error.message}`;
192-
outputChannel.appendLine(message);
193-
console.error(message);
194-
});
195-
}
196-
}
197-
198270
protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
199271
console.log(`threadsRequest`);
200272

@@ -241,24 +313,20 @@ export class DebugSession extends LoggingDebugSession {
241313
console.log(`stackTraceRequest ${JSON.stringify(args)}`);
242314

243315
const log2srcPath = path.resolve(__dirname, this._binaryPath);
244-
const execFile = require('child_process').execFileSync;
245316
const start = this._logDebugger.linenum() - 1;
246317

247-
const editors = this.findEditors();
248-
if (editors.length > 0) {
249-
this.focusEditor(editors[0]);
250-
}
318+
this._editorEffects.openAndFocus(this._launchArgs.log, this._logDebugger.linenum());
251319

252-
let l2sArgs = ['-d', this._launchArgs.source,
320+
const l2sArgs: string[] = ['-d', this._launchArgs.source,
253321
'--log', this._launchArgs.log,
254-
'--start', start,
255-
'--count', 1]
322+
'--start', String(start),
323+
'--count', '1'];
256324
if (this._launchArgs.log_format !== undefined && this._launchArgs.log_format !== "") {
257325
l2sArgs.push("-f");
258326
l2sArgs.push(this._launchArgs.log_format);
259327
}
260328
outputChannel.appendLine(`args ${l2sArgs.join(" ")}`);
261-
let stdout = execFile(log2srcPath, l2sArgs);
329+
const stdout = this._processRunner.execFileSync(log2srcPath, l2sArgs);
262330
this._mapping = JSON.parse(stdout.toString('utf8'));
263331
outputChannel.appendLine(`mapped ${JSON.stringify(this._mapping)}`);
264332

@@ -275,24 +343,6 @@ export class DebugSession extends LoggingDebugSession {
275343
this.sendResponse(response);
276344
}
277345

278-
private findEditors(): vscode.TextEditor[] {
279-
const target = path.resolve(this._launchArgs.log);
280-
return vscode.window.visibleTextEditors.filter((editor) => editor.document.fileName === target);
281-
}
282-
283-
private focusEditor(editor: vscode.TextEditor) {
284-
const start = this._logDebugger.linenum() - 1;
285-
let range = new vscode.Range(
286-
new vscode.Position(start, 0),
287-
new vscode.Position(start, Number.MAX_VALUE)
288-
);
289-
editor.setDecorations(this._highlightDecoration, [range]);
290-
editor.revealRange(
291-
range,
292-
vscode.TextEditorRevealType.InCenter
293-
);
294-
}
295-
296346
private buildStackFrame(index: number, srcRef?: SourceRef): StackFrame {
297347
let name = "???";
298348
let lineNumber = -1;

editors/code/src/test/suite/debugAdapter.test.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as assert from 'assert';
22
import { DebugProtocol } from '@vscode/debugprotocol';
33

4-
import { DebugSession, BinaryNotFoundError } from '../../debugAdapter';
4+
import {
5+
BinaryNotFoundError,
6+
DebugSession,
7+
EditorEffects,
8+
ILaunchRequestArguments,
9+
ProcessRunner
10+
} from '../../debugAdapter';
511
import { LogDebugger } from '../../logDebugger';
612

713
type PatchedSession = DebugSession & {
@@ -50,9 +56,22 @@ function setPlatformArch(platform: NodeJS.Platform | string, arch: string): void
5056
});
5157
}
5258

53-
function createSession(logDebugger: LogDebugger): DebugSession {
59+
function createSession(
60+
logDebugger: LogDebugger,
61+
processRunner?: ProcessRunner,
62+
editorEffects?: EditorEffects
63+
): DebugSession {
5464
setPlatformArch('darwin', 'arm64');
55-
return new DebugSession(logDebugger);
65+
const runner = processRunner ?? {
66+
execFileSync: (_file: string, _args: string[]): Buffer => Buffer.alloc(0),
67+
readFile: (_path: string): Buffer => Buffer.alloc(0)
68+
};
69+
const effects = editorEffects ?? {
70+
openAndFocus: (_log: string, _line: number): void => { },
71+
highlightLine: (_log: string, _line: number): void => { },
72+
clearHighlights: (): void => { }
73+
};
74+
return new DebugSession(logDebugger, runner, effects);
5675
}
5776

5877

@@ -204,4 +223,56 @@ suite('DebugAdapter Test Suite', () => {
204223
assert.strictEqual(logDebugger.hasBreakpoints(), false, 'LogDebugger should report no breakpoints');
205224
});
206225
});
226+
227+
suite('Launch Request Tests', () => {
228+
test('Launch request should set log state and send entry event', () => {
229+
const logPath = '/test/source/file.log';
230+
const args: ILaunchRequestArguments = {
231+
source: '/test/source/file.rs',
232+
log: logPath,
233+
log_format: '',
234+
trace: false,
235+
noDebug: false
236+
};
237+
238+
const response: DebugProtocol.LaunchResponse = {
239+
request_seq: 1,
240+
success: true,
241+
command: 'launch',
242+
seq: 1,
243+
type: 'response'
244+
};
245+
246+
const processRunner: ProcessRunner = {
247+
execFileSync: (_file: string, _args: string[]): Buffer => Buffer.alloc(0),
248+
readFile: (_path: string): Buffer => Buffer.from('line1\nline2\n')
249+
};
250+
251+
let openAndFocusCalled = 0;
252+
let focusedLog: string | undefined;
253+
let focusedLine: number | undefined;
254+
const editorEffects: EditorEffects = {
255+
openAndFocus: (log: string, line: number): void => {
256+
openAndFocusCalled++;
257+
focusedLog = log;
258+
focusedLine = line;
259+
},
260+
highlightLine: (_log: string, _line: number): void => { },
261+
clearHighlights: (): void => { }
262+
};
263+
264+
debugSession = createSession(logDebugger, processRunner, editorEffects);
265+
const session = debugSession as PatchedSession;
266+
const { response: captured, eventCount } = captureRequest<DebugProtocol.LaunchResponse>(
267+
session,
268+
() => (session as any).launchRequest(response, args)
269+
);
270+
271+
assert.ok(captured, 'Launch response should be sent');
272+
assert.strictEqual(eventCount, 1, 'Should send entry stopped event when no breakpoints are set');
273+
assert.strictEqual(openAndFocusCalled, 1, 'Should open and focus log once');
274+
assert.strictEqual(focusedLog, logPath, 'Should focus the launched log file');
275+
assert.strictEqual(focusedLine, logDebugger.linenum(), 'Should focus current debugger line');
276+
});
277+
});
207278
});

0 commit comments

Comments
 (0)