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