Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import * as sinon from 'sinon';
8
import { ThemeIcon } from '../../../../../base/common/themables.js';
9
import { Constants } from '../../../../../base/common/uint.js';
10
import { generateUuid } from '../../../../../base/common/uuid.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
12
import { Range } from '../../../../../editor/common/core/range.js';
13
import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js';
14
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
15
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
16
import { NullLogService } from '../../../../../platform/log/common/log.js';
17
import { createDecorationsForStackFrame } from '../../browser/callStackEditorContribution.js';
18
import { getContext, getContextForContributedActions, getSpecificSourceName } from '../../browser/callStackView.js';
19
import { debugStackframe, debugStackframeFocused } from '../../browser/debugIcons.js';
20
import { getStackFrameThreadAndSessionToFocus } from '../../browser/debugService.js';
21
import { DebugSession } from '../../browser/debugSession.js';
22
import { IDebugService, IDebugSessionOptions, State } from '../../common/debug.js';
23
import { DebugModel, StackFrame, Thread } from '../../common/debugModel.js';
24
import { Source } from '../../common/debugSource.js';
25
import { createMockDebugModel, mockUriIdentityService } from './mockDebugModel.js';
26
import { MockRawSession } from '../common/mockDebug.js';
27
28
const mockWorkspaceContextService = {
29
getWorkspace: () => {
30
return {
31
folders: []
32
};
33
}
34
} as any;
35
36
export function createTestSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession {
37
return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, options, {
38
getViewModel(): any {
39
return {
40
updateViews(): void {
41
// noop
42
}
43
};
44
}
45
} as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!, new TestAccessibilityService());
46
}
47
48
function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } {
49
const thread = new class extends Thread {
50
public override getCallStack(): StackFrame[] {
51
return [firstStackFrame, secondStackFrame];
52
}
53
}(session, 'mockthread', 1);
54
55
const firstSource = new Source({
56
name: 'internalModule.js',
57
path: 'a/b/c/d/internalModule.js',
58
sourceReference: 10,
59
}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());
60
const secondSource = new Source({
61
name: 'internalModule.js',
62
path: 'z/x/c/d/internalModule.js',
63
sourceReference: 11,
64
}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());
65
66
const firstStackFrame = new StackFrame(thread, 0, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 0, true);
67
const secondStackFrame = new StackFrame(thread, 1, secondSource, 'app2.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 1, true);
68
69
return { firstStackFrame, secondStackFrame };
70
}
71
72
suite('Debug - CallStack', () => {
73
let model: DebugModel;
74
let mockRawSession: MockRawSession;
75
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
76
77
setup(() => {
78
model = createMockDebugModel(disposables);
79
mockRawSession = new MockRawSession();
80
});
81
82
teardown(() => {
83
sinon.restore();
84
});
85
86
// Threads
87
88
test('threads simple', () => {
89
const threadId = 1;
90
const threadName = 'firstThread';
91
const session = createTestSession(model);
92
disposables.add(session);
93
model.addSession(session);
94
95
assert.strictEqual(model.getSessions(true).length, 1);
96
model.rawUpdate({
97
sessionId: session.getId(),
98
threads: [{
99
id: threadId,
100
name: threadName
101
}]
102
});
103
104
assert.strictEqual(session.getThread(threadId)!.name, threadName);
105
106
model.clearThreads(session.getId(), true);
107
assert.strictEqual(session.getThread(threadId), undefined);
108
assert.strictEqual(model.getSessions(true).length, 1);
109
});
110
111
test('threads multiple with allThreadsStopped', async () => {
112
const threadId1 = 1;
113
const threadName1 = 'firstThread';
114
const threadId2 = 2;
115
const threadName2 = 'secondThread';
116
const stoppedReason = 'breakpoint';
117
118
// Add the threads
119
const session = createTestSession(model);
120
disposables.add(session);
121
model.addSession(session);
122
123
session['raw'] = <any>mockRawSession;
124
125
model.rawUpdate({
126
sessionId: session.getId(),
127
threads: [{
128
id: threadId1,
129
name: threadName1
130
}]
131
});
132
133
// Stopped event with all threads stopped
134
model.rawUpdate({
135
sessionId: session.getId(),
136
threads: [{
137
id: threadId1,
138
name: threadName1
139
}, {
140
id: threadId2,
141
name: threadName2
142
}],
143
stoppedDetails: {
144
reason: stoppedReason,
145
threadId: 1,
146
allThreadsStopped: true
147
},
148
});
149
150
const thread1 = session.getThread(threadId1)!;
151
const thread2 = session.getThread(threadId2)!;
152
153
// at the beginning, callstacks are obtainable but not available
154
assert.strictEqual(session.getAllThreads().length, 2);
155
assert.strictEqual(thread1.name, threadName1);
156
assert.strictEqual(thread1.stopped, true);
157
assert.strictEqual(thread1.getCallStack().length, 0);
158
assert.strictEqual(thread1.stoppedDetails!.reason, stoppedReason);
159
assert.strictEqual(thread2.name, threadName2);
160
assert.strictEqual(thread2.stopped, true);
161
assert.strictEqual(thread2.getCallStack().length, 0);
162
assert.strictEqual(thread2.stoppedDetails!.reason, undefined);
163
164
// after calling getCallStack, the callstack becomes available
165
// and results in a request for the callstack in the debug adapter
166
await thread1.fetchCallStack();
167
assert.notStrictEqual(thread1.getCallStack().length, 0);
168
169
await thread2.fetchCallStack();
170
assert.notStrictEqual(thread2.getCallStack().length, 0);
171
172
// calling multiple times getCallStack doesn't result in multiple calls
173
// to the debug adapter
174
await thread1.fetchCallStack();
175
await thread2.fetchCallStack();
176
177
// clearing the callstack results in the callstack not being available
178
thread1.clearCallStack();
179
assert.strictEqual(thread1.stopped, true);
180
assert.strictEqual(thread1.getCallStack().length, 0);
181
182
thread2.clearCallStack();
183
assert.strictEqual(thread2.stopped, true);
184
assert.strictEqual(thread2.getCallStack().length, 0);
185
186
model.clearThreads(session.getId(), true);
187
assert.strictEqual(session.getThread(threadId1), undefined);
188
assert.strictEqual(session.getThread(threadId2), undefined);
189
assert.strictEqual(session.getAllThreads().length, 0);
190
});
191
192
test('allThreadsStopped in multiple events', async () => {
193
const threadId1 = 1;
194
const threadName1 = 'firstThread';
195
const threadId2 = 2;
196
const threadName2 = 'secondThread';
197
const stoppedReason = 'breakpoint';
198
199
// Add the threads
200
const session = createTestSession(model);
201
disposables.add(session);
202
model.addSession(session);
203
204
session['raw'] = <any>mockRawSession;
205
206
// Stopped event with all threads stopped
207
model.rawUpdate({
208
sessionId: session.getId(),
209
threads: [{
210
id: threadId1,
211
name: threadName1
212
}, {
213
id: threadId2,
214
name: threadName2
215
}],
216
stoppedDetails: {
217
reason: stoppedReason,
218
threadId: threadId1,
219
allThreadsStopped: true
220
},
221
});
222
223
model.rawUpdate({
224
sessionId: session.getId(),
225
threads: [{
226
id: threadId1,
227
name: threadName1
228
}, {
229
id: threadId2,
230
name: threadName2
231
}],
232
stoppedDetails: {
233
reason: stoppedReason,
234
threadId: threadId2,
235
allThreadsStopped: true
236
},
237
});
238
239
const thread1 = session.getThread(threadId1)!;
240
const thread2 = session.getThread(threadId2)!;
241
242
assert.strictEqual(thread1.stoppedDetails?.reason, stoppedReason);
243
assert.strictEqual(thread2.stoppedDetails?.reason, stoppedReason);
244
});
245
246
test('threads multiple without allThreadsStopped', async () => {
247
const sessionStub = sinon.spy(mockRawSession, 'stackTrace');
248
249
const stoppedThreadId = 1;
250
const stoppedThreadName = 'stoppedThread';
251
const runningThreadId = 2;
252
const runningThreadName = 'runningThread';
253
const stoppedReason = 'breakpoint';
254
const session = createTestSession(model);
255
disposables.add(session);
256
model.addSession(session);
257
258
session['raw'] = <any>mockRawSession;
259
260
// Add the threads
261
model.rawUpdate({
262
sessionId: session.getId(),
263
threads: [{
264
id: stoppedThreadId,
265
name: stoppedThreadName
266
}]
267
});
268
269
// Stopped event with only one thread stopped
270
model.rawUpdate({
271
sessionId: session.getId(),
272
threads: [{
273
id: 1,
274
name: stoppedThreadName
275
}, {
276
id: runningThreadId,
277
name: runningThreadName
278
}],
279
stoppedDetails: {
280
reason: stoppedReason,
281
threadId: 1,
282
allThreadsStopped: false
283
}
284
});
285
286
const stoppedThread = session.getThread(stoppedThreadId)!;
287
const runningThread = session.getThread(runningThreadId)!;
288
289
// the callstack for the stopped thread is obtainable but not available
290
// the callstack for the running thread is not obtainable nor available
291
assert.strictEqual(stoppedThread.name, stoppedThreadName);
292
assert.strictEqual(stoppedThread.stopped, true);
293
assert.strictEqual(session.getAllThreads().length, 2);
294
assert.strictEqual(stoppedThread.getCallStack().length, 0);
295
assert.strictEqual(stoppedThread.stoppedDetails!.reason, stoppedReason);
296
assert.strictEqual(runningThread.name, runningThreadName);
297
assert.strictEqual(runningThread.stopped, false);
298
assert.strictEqual(runningThread.getCallStack().length, 0);
299
assert.strictEqual(runningThread.stoppedDetails, undefined);
300
301
// after calling getCallStack, the callstack becomes available
302
// and results in a request for the callstack in the debug adapter
303
await stoppedThread.fetchCallStack();
304
assert.notStrictEqual(stoppedThread.getCallStack().length, 0);
305
assert.strictEqual(runningThread.getCallStack().length, 0);
306
assert.strictEqual(sessionStub.callCount, 1);
307
308
// calling getCallStack on the running thread returns empty array
309
// and does not return in a request for the callstack in the debug
310
// adapter
311
await runningThread.fetchCallStack();
312
assert.strictEqual(runningThread.getCallStack().length, 0);
313
assert.strictEqual(sessionStub.callCount, 1);
314
315
// clearing the callstack results in the callstack not being available
316
stoppedThread.clearCallStack();
317
assert.strictEqual(stoppedThread.stopped, true);
318
assert.strictEqual(stoppedThread.getCallStack().length, 0);
319
320
model.clearThreads(session.getId(), true);
321
assert.strictEqual(session.getThread(stoppedThreadId), undefined);
322
assert.strictEqual(session.getThread(runningThreadId), undefined);
323
assert.strictEqual(session.getAllThreads().length, 0);
324
});
325
326
test('stack frame get specific source name', () => {
327
const session = createTestSession(model);
328
disposables.add(session);
329
model.addSession(session);
330
const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);
331
332
assert.strictEqual(getSpecificSourceName(firstStackFrame), '.../b/c/d/internalModule.js');
333
assert.strictEqual(getSpecificSourceName(secondStackFrame), '.../x/c/d/internalModule.js');
334
});
335
336
test('stack frame toString()', () => {
337
const session = createTestSession(model);
338
disposables.add(session);
339
const thread = new Thread(session, 'mockthread', 1);
340
const firstSource = new Source({
341
name: 'internalModule.js',
342
path: 'a/b/c/d/internalModule.js',
343
sourceReference: 10,
344
}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());
345
const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1, true);
346
assert.strictEqual(stackFrame.toString(), 'app (internalModule.js:1)');
347
348
const secondSource = new Source(undefined, 'aDebugSessionId', mockUriIdentityService, new NullLogService());
349
const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2, true);
350
assert.strictEqual(stackFrame2.toString(), 'module');
351
});
352
353
test('debug child sessions are added in correct order', () => {
354
const session = disposables.add(createTestSession(model));
355
model.addSession(session);
356
const secondSession = disposables.add(createTestSession(model, 'mockSession2'));
357
model.addSession(secondSession);
358
const firstChild = disposables.add(createTestSession(model, 'firstChild', { parentSession: session }));
359
model.addSession(firstChild);
360
const secondChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: session }));
361
model.addSession(secondChild);
362
const thirdSession = disposables.add(createTestSession(model, 'mockSession3'));
363
model.addSession(thirdSession);
364
const anotherChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: secondSession }));
365
model.addSession(anotherChild);
366
367
const sessions = model.getSessions();
368
assert.strictEqual(sessions[0].getId(), session.getId());
369
assert.strictEqual(sessions[1].getId(), firstChild.getId());
370
assert.strictEqual(sessions[2].getId(), secondChild.getId());
371
assert.strictEqual(sessions[3].getId(), secondSession.getId());
372
assert.strictEqual(sessions[4].getId(), anotherChild.getId());
373
assert.strictEqual(sessions[5].getId(), thirdSession.getId());
374
});
375
376
test('decorations', () => {
377
const session = createTestSession(model);
378
disposables.add(session);
379
model.addSession(session);
380
const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);
381
let decorations = createDecorationsForStackFrame(firstStackFrame, true, false);
382
assert.strictEqual(decorations.length, 3);
383
assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));
384
assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));
385
assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));
386
assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');
387
assert.strictEqual(decorations[1].options.isWholeLine, true);
388
389
decorations = createDecorationsForStackFrame(secondStackFrame, true, false);
390
assert.strictEqual(decorations.length, 2);
391
assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));
392
assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframeFocused));
393
assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));
394
assert.strictEqual(decorations[1].options.className, 'debug-focused-stack-frame-line');
395
assert.strictEqual(decorations[1].options.isWholeLine, true);
396
397
decorations = createDecorationsForStackFrame(firstStackFrame, true, false);
398
assert.strictEqual(decorations.length, 3);
399
assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));
400
assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));
401
assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));
402
assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');
403
assert.strictEqual(decorations[1].options.isWholeLine, true);
404
// Inline decoration gets rendered in this case
405
assert.strictEqual(decorations[2].options.before?.inlineClassName, 'debug-top-stack-frame-column');
406
assert.deepStrictEqual(decorations[2].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));
407
});
408
409
test('contexts', () => {
410
const session = createTestSession(model);
411
disposables.add(session);
412
model.addSession(session);
413
const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);
414
let context = getContext(firstStackFrame);
415
assert.strictEqual(context.sessionId, firstStackFrame.thread.session.getId());
416
assert.strictEqual(context.threadId, firstStackFrame.thread.getId());
417
assert.strictEqual(context.frameId, firstStackFrame.getId());
418
419
context = getContext(secondStackFrame.thread);
420
assert.strictEqual(context.sessionId, secondStackFrame.thread.session.getId());
421
assert.strictEqual(context.threadId, secondStackFrame.thread.getId());
422
assert.strictEqual(context.frameId, undefined);
423
424
context = getContext(session);
425
assert.strictEqual(context.sessionId, session.getId());
426
assert.strictEqual(context.threadId, undefined);
427
assert.strictEqual(context.frameId, undefined);
428
429
let contributedContext = getContextForContributedActions(firstStackFrame);
430
assert.strictEqual(contributedContext, firstStackFrame.source.raw.path);
431
contributedContext = getContextForContributedActions(firstStackFrame.thread);
432
assert.strictEqual(contributedContext, firstStackFrame.thread.threadId);
433
contributedContext = getContextForContributedActions(session);
434
assert.strictEqual(contributedContext, session.getId());
435
});
436
437
test('focusStackFrameThreadAndSession', () => {
438
const threadId1 = 1;
439
const threadName1 = 'firstThread';
440
const threadId2 = 2;
441
const threadName2 = 'secondThread';
442
const stoppedReason = 'breakpoint';
443
444
// Add the threads
445
const session = new class extends DebugSession {
446
override get state(): State {
447
return State.Stopped;
448
}
449
}(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!, undefined!, new NullLogService(), undefined!, undefined!, new TestAccessibilityService());
450
disposables.add(session);
451
452
const runningSession = createTestSession(model);
453
disposables.add(runningSession);
454
model.addSession(runningSession);
455
model.addSession(session);
456
457
session['raw'] = <any>mockRawSession;
458
459
model.rawUpdate({
460
sessionId: session.getId(),
461
threads: [{
462
id: threadId1,
463
name: threadName1
464
}]
465
});
466
467
// Stopped event with all threads stopped
468
model.rawUpdate({
469
sessionId: session.getId(),
470
threads: [{
471
id: threadId1,
472
name: threadName1
473
}, {
474
id: threadId2,
475
name: threadName2
476
}],
477
stoppedDetails: {
478
reason: stoppedReason,
479
threadId: 1,
480
allThreadsStopped: true
481
},
482
});
483
484
const thread = session.getThread(threadId1)!;
485
const runningThread = session.getThread(threadId2);
486
487
let toFocus = getStackFrameThreadAndSessionToFocus(model, undefined);
488
// Verify stopped session and stopped thread get focused
489
assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });
490
491
toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, undefined, runningSession);
492
assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: undefined, session: runningSession });
493
494
toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, thread);
495
assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });
496
497
toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, runningThread);
498
assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: runningThread, session: session });
499
500
const stackFrame = new StackFrame(thread, 5, undefined!, 'stackframename2', undefined, undefined!, 1, true);
501
toFocus = getStackFrameThreadAndSessionToFocus(model, stackFrame);
502
assert.deepStrictEqual(toFocus, { stackFrame: stackFrame, thread: thread, session: session });
503
});
504
});
505
506