Path: blob/main/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import * as sinon from 'sinon';7import { ThemeIcon } from '../../../../../base/common/themables.js';8import { Constants } from '../../../../../base/common/uint.js';9import { generateUuid } from '../../../../../base/common/uuid.js';10import { upcastDeepPartial, upcastPartial } from '../../../../../base/test/common/mock.js';11import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';12import { Range } from '../../../../../editor/common/core/range.js';13import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js';14import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';15import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';16import { NullLogService } from '../../../../../platform/log/common/log.js';17import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';18import { createDecorationsForStackFrame } from '../../browser/callStackEditorContribution.js';19import { getContext, getContextForContributedActions, getSpecificSourceName } from '../../browser/callStackView.js';20import { debugStackframe, debugStackframeFocused } from '../../browser/debugIcons.js';21import { getStackFrameThreadAndSessionToFocus } from '../../browser/debugService.js';22import { DebugSession } from '../../browser/debugSession.js';23import { IDebugService, IDebugSessionOptions, State } from '../../common/debug.js';24import { DebugModel, StackFrame, Thread } from '../../common/debugModel.js';25import { Source } from '../../common/debugSource.js';26import { MockRawSession } from '../common/mockDebug.js';27import { createMockDebugModel, mockUriIdentityService } from './mockDebugModel.js';28import { RawDebugSession } from '../../browser/rawDebugSession.js';2930const mockWorkspaceContextService = upcastDeepPartial<IWorkspaceContextService>({31getWorkspace: () => {32return {33folders: []34};35}36});3738export function createTestSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession {39return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, options, {40getViewModel(): any {41return {42updateViews(): void {43// noop44}45};46}47} 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());48}4950function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } {51const thread = new class extends Thread {52public override getCallStack(): StackFrame[] {53return [firstStackFrame, secondStackFrame];54}55}(session, 'mockthread', 1);5657const firstSource = new Source({58name: 'internalModule.js',59path: 'a/b/c/d/internalModule.js',60sourceReference: 10,61}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());62const secondSource = new Source({63name: 'internalModule.js',64path: 'z/x/c/d/internalModule.js',65sourceReference: 11,66}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());6768const firstStackFrame = new StackFrame(thread, 0, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 0, true);69const secondStackFrame = new StackFrame(thread, 1, secondSource, 'app2.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 1, true);7071return { firstStackFrame, secondStackFrame };72}7374suite('Debug - CallStack', () => {75let model: DebugModel;76let mockRawSession: MockRawSession;77const disposables = ensureNoDisposablesAreLeakedInTestSuite();7879setup(() => {80model = createMockDebugModel(disposables);81mockRawSession = new MockRawSession();82});8384teardown(() => {85sinon.restore();86});8788// Threads8990test('threads simple', () => {91const threadId = 1;92const threadName = 'firstThread';93const session = createTestSession(model);94disposables.add(session);95model.addSession(session);9697assert.strictEqual(model.getSessions(true).length, 1);98model.rawUpdate({99sessionId: session.getId(),100threads: [{101id: threadId,102name: threadName103}]104});105106assert.strictEqual(session.getThread(threadId)!.name, threadName);107108model.clearThreads(session.getId(), true);109assert.strictEqual(session.getThread(threadId), undefined);110assert.strictEqual(model.getSessions(true).length, 1);111});112113test('threads multiple with allThreadsStopped', async () => {114const threadId1 = 1;115const threadName1 = 'firstThread';116const threadId2 = 2;117const threadName2 = 'secondThread';118const stoppedReason = 'breakpoint';119120// Add the threads121const session = createTestSession(model);122disposables.add(session);123model.addSession(session);124125session.raw = upcastPartial<RawDebugSession>(mockRawSession);126127model.rawUpdate({128sessionId: session.getId(),129threads: [{130id: threadId1,131name: threadName1132}]133});134135// Stopped event with all threads stopped136model.rawUpdate({137sessionId: session.getId(),138threads: [{139id: threadId1,140name: threadName1141}, {142id: threadId2,143name: threadName2144}],145stoppedDetails: {146reason: stoppedReason,147threadId: 1,148allThreadsStopped: true149},150});151152const thread1 = session.getThread(threadId1)!;153const thread2 = session.getThread(threadId2)!;154155// at the beginning, callstacks are obtainable but not available156assert.strictEqual(session.getAllThreads().length, 2);157assert.strictEqual(thread1.name, threadName1);158assert.strictEqual(thread1.stopped, true);159assert.strictEqual(thread1.getCallStack().length, 0);160assert.strictEqual(thread1.stoppedDetails!.reason, stoppedReason);161assert.strictEqual(thread2.name, threadName2);162assert.strictEqual(thread2.stopped, true);163assert.strictEqual(thread2.getCallStack().length, 0);164assert.strictEqual(thread2.stoppedDetails!.reason, undefined);165166// after calling getCallStack, the callstack becomes available167// and results in a request for the callstack in the debug adapter168await thread1.fetchCallStack();169assert.notStrictEqual(thread1.getCallStack().length, 0);170171await thread2.fetchCallStack();172assert.notStrictEqual(thread2.getCallStack().length, 0);173174// calling multiple times getCallStack doesn't result in multiple calls175// to the debug adapter176await thread1.fetchCallStack();177await thread2.fetchCallStack();178179// clearing the callstack results in the callstack not being available180thread1.clearCallStack();181assert.strictEqual(thread1.stopped, true);182assert.strictEqual(thread1.getCallStack().length, 0);183184thread2.clearCallStack();185assert.strictEqual(thread2.stopped, true);186assert.strictEqual(thread2.getCallStack().length, 0);187188model.clearThreads(session.getId(), true);189assert.strictEqual(session.getThread(threadId1), undefined);190assert.strictEqual(session.getThread(threadId2), undefined);191assert.strictEqual(session.getAllThreads().length, 0);192});193194test('allThreadsStopped in multiple events', async () => {195const threadId1 = 1;196const threadName1 = 'firstThread';197const threadId2 = 2;198const threadName2 = 'secondThread';199const stoppedReason = 'breakpoint';200201// Add the threads202const session = createTestSession(model);203disposables.add(session);204model.addSession(session);205206session.raw = upcastPartial<RawDebugSession>(mockRawSession);207208// Stopped event with all threads stopped209model.rawUpdate({210sessionId: session.getId(),211threads: [{212id: threadId1,213name: threadName1214}, {215id: threadId2,216name: threadName2217}],218stoppedDetails: {219reason: stoppedReason,220threadId: threadId1,221allThreadsStopped: true222},223});224225model.rawUpdate({226sessionId: session.getId(),227threads: [{228id: threadId1,229name: threadName1230}, {231id: threadId2,232name: threadName2233}],234stoppedDetails: {235reason: stoppedReason,236threadId: threadId2,237allThreadsStopped: true238},239});240241const thread1 = session.getThread(threadId1)!;242const thread2 = session.getThread(threadId2)!;243244assert.strictEqual(thread1.stoppedDetails?.reason, stoppedReason);245assert.strictEqual(thread2.stoppedDetails?.reason, stoppedReason);246});247248test('threads multiple without allThreadsStopped', async () => {249const sessionStub = sinon.spy(mockRawSession, 'stackTrace');250251const stoppedThreadId = 1;252const stoppedThreadName = 'stoppedThread';253const runningThreadId = 2;254const runningThreadName = 'runningThread';255const stoppedReason = 'breakpoint';256const session = createTestSession(model);257disposables.add(session);258model.addSession(session);259260session.raw = upcastPartial<RawDebugSession>(mockRawSession);261262// Add the threads263model.rawUpdate({264sessionId: session.getId(),265threads: [{266id: stoppedThreadId,267name: stoppedThreadName268}]269});270271// Stopped event with only one thread stopped272model.rawUpdate({273sessionId: session.getId(),274threads: [{275id: 1,276name: stoppedThreadName277}, {278id: runningThreadId,279name: runningThreadName280}],281stoppedDetails: {282reason: stoppedReason,283threadId: 1,284allThreadsStopped: false285}286});287288const stoppedThread = session.getThread(stoppedThreadId)!;289const runningThread = session.getThread(runningThreadId)!;290291// the callstack for the stopped thread is obtainable but not available292// the callstack for the running thread is not obtainable nor available293assert.strictEqual(stoppedThread.name, stoppedThreadName);294assert.strictEqual(stoppedThread.stopped, true);295assert.strictEqual(session.getAllThreads().length, 2);296assert.strictEqual(stoppedThread.getCallStack().length, 0);297assert.strictEqual(stoppedThread.stoppedDetails!.reason, stoppedReason);298assert.strictEqual(runningThread.name, runningThreadName);299assert.strictEqual(runningThread.stopped, false);300assert.strictEqual(runningThread.getCallStack().length, 0);301assert.strictEqual(runningThread.stoppedDetails, undefined);302303// after calling getCallStack, the callstack becomes available304// and results in a request for the callstack in the debug adapter305await stoppedThread.fetchCallStack();306assert.notStrictEqual(stoppedThread.getCallStack().length, 0);307assert.strictEqual(runningThread.getCallStack().length, 0);308assert.strictEqual(sessionStub.callCount, 1);309310// calling getCallStack on the running thread returns empty array311// and does not return in a request for the callstack in the debug312// adapter313await runningThread.fetchCallStack();314assert.strictEqual(runningThread.getCallStack().length, 0);315assert.strictEqual(sessionStub.callCount, 1);316317// clearing the callstack results in the callstack not being available318stoppedThread.clearCallStack();319assert.strictEqual(stoppedThread.stopped, true);320assert.strictEqual(stoppedThread.getCallStack().length, 0);321322model.clearThreads(session.getId(), true);323assert.strictEqual(session.getThread(stoppedThreadId), undefined);324assert.strictEqual(session.getThread(runningThreadId), undefined);325assert.strictEqual(session.getAllThreads().length, 0);326});327328test('stack frame get specific source name', () => {329const session = createTestSession(model);330disposables.add(session);331model.addSession(session);332const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);333334assert.strictEqual(getSpecificSourceName(firstStackFrame), '.../b/c/d/internalModule.js');335assert.strictEqual(getSpecificSourceName(secondStackFrame), '.../x/c/d/internalModule.js');336});337338test('stack frame toString()', () => {339const session = createTestSession(model);340disposables.add(session);341const thread = new Thread(session, 'mockthread', 1);342const firstSource = new Source({343name: 'internalModule.js',344path: 'a/b/c/d/internalModule.js',345sourceReference: 10,346}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());347const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1, true);348assert.strictEqual(stackFrame.toString(), 'app (internalModule.js:1)');349350const secondSource = new Source(undefined, 'aDebugSessionId', mockUriIdentityService, new NullLogService());351const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2, true);352assert.strictEqual(stackFrame2.toString(), 'module');353});354355test('debug child sessions are added in correct order', () => {356const session = disposables.add(createTestSession(model));357model.addSession(session);358const secondSession = disposables.add(createTestSession(model, 'mockSession2'));359model.addSession(secondSession);360const firstChild = disposables.add(createTestSession(model, 'firstChild', { parentSession: session }));361model.addSession(firstChild);362const secondChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: session }));363model.addSession(secondChild);364const thirdSession = disposables.add(createTestSession(model, 'mockSession3'));365model.addSession(thirdSession);366const anotherChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: secondSession }));367model.addSession(anotherChild);368369const sessions = model.getSessions();370assert.strictEqual(sessions[0].getId(), session.getId());371assert.strictEqual(sessions[1].getId(), firstChild.getId());372assert.strictEqual(sessions[2].getId(), secondChild.getId());373assert.strictEqual(sessions[3].getId(), secondSession.getId());374assert.strictEqual(sessions[4].getId(), anotherChild.getId());375assert.strictEqual(sessions[5].getId(), thirdSession.getId());376});377378test('decorations', () => {379const session = createTestSession(model);380disposables.add(session);381model.addSession(session);382const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);383let decorations = createDecorationsForStackFrame(firstStackFrame, true, false);384assert.strictEqual(decorations.length, 3);385assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));386assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));387assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));388assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');389assert.strictEqual(decorations[1].options.isWholeLine, true);390391decorations = createDecorationsForStackFrame(secondStackFrame, true, false);392assert.strictEqual(decorations.length, 2);393assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));394assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframeFocused));395assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));396assert.strictEqual(decorations[1].options.className, 'debug-focused-stack-frame-line');397assert.strictEqual(decorations[1].options.isWholeLine, true);398399decorations = createDecorationsForStackFrame(firstStackFrame, true, false);400assert.strictEqual(decorations.length, 3);401assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));402assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));403assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));404assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');405assert.strictEqual(decorations[1].options.isWholeLine, true);406// Inline decoration gets rendered in this case407assert.strictEqual(decorations[2].options.before?.inlineClassName, 'debug-top-stack-frame-column');408assert.deepStrictEqual(decorations[2].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));409});410411test('contexts', () => {412const session = createTestSession(model);413disposables.add(session);414model.addSession(session);415const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);416let context = getContext(firstStackFrame);417assert.strictEqual(context?.sessionId, firstStackFrame.thread.session.getId());418assert.strictEqual(context?.threadId, firstStackFrame.thread.getId());419assert.strictEqual(context?.frameId, firstStackFrame.getId());420421context = getContext(secondStackFrame.thread);422assert.strictEqual(context?.sessionId, secondStackFrame.thread.session.getId());423assert.strictEqual(context?.threadId, secondStackFrame.thread.getId());424assert.strictEqual(context?.frameId, undefined);425426context = getContext(session);427assert.strictEqual(context?.sessionId, session.getId());428assert.strictEqual(context?.threadId, undefined);429assert.strictEqual(context?.frameId, undefined);430431let contributedContext = getContextForContributedActions(firstStackFrame);432assert.strictEqual(contributedContext, firstStackFrame.source.raw.path);433contributedContext = getContextForContributedActions(firstStackFrame.thread);434assert.strictEqual(contributedContext, firstStackFrame.thread.threadId);435contributedContext = getContextForContributedActions(session);436assert.strictEqual(contributedContext, session.getId());437});438439test('focusStackFrameThreadAndSession', () => {440const threadId1 = 1;441const threadName1 = 'firstThread';442const threadId2 = 2;443const threadName2 = 'secondThread';444const stoppedReason = 'breakpoint';445446// Add the threads447const session = new class extends DebugSession {448override get state(): State {449return State.Stopped;450}451}(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());452disposables.add(session);453454const runningSession = createTestSession(model);455disposables.add(runningSession);456model.addSession(runningSession);457model.addSession(session);458459session.raw = upcastPartial<RawDebugSession>(mockRawSession);460461model.rawUpdate({462sessionId: session.getId(),463threads: [{464id: threadId1,465name: threadName1466}]467});468469// Stopped event with all threads stopped470model.rawUpdate({471sessionId: session.getId(),472threads: [{473id: threadId1,474name: threadName1475}, {476id: threadId2,477name: threadName2478}],479stoppedDetails: {480reason: stoppedReason,481threadId: 1,482allThreadsStopped: true483},484});485486const thread = session.getThread(threadId1)!;487const runningThread = session.getThread(threadId2);488489let toFocus = getStackFrameThreadAndSessionToFocus(model, undefined);490// Verify stopped session and stopped thread get focused491assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });492493toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, undefined, runningSession);494assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: undefined, session: runningSession });495496toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, thread);497assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });498499toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, runningThread);500assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: runningThread, session: session });501502const stackFrame = new StackFrame(thread, 5, undefined!, 'stackframename2', undefined, undefined!, 1, true);503toFocus = getStackFrameThreadAndSessionToFocus(model, stackFrame);504assert.deepStrictEqual(toFocus, { stackFrame: stackFrame, thread: thread, session: session });505});506});507508509