Path: blob/main/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts
3296 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { Range } from '../../../../../editor/common/core/range.js';12import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js';13import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';14import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';15import { NullLogService } from '../../../../../platform/log/common/log.js';16import { createDecorationsForStackFrame } from '../../browser/callStackEditorContribution.js';17import { getContext, getContextForContributedActions, getSpecificSourceName } from '../../browser/callStackView.js';18import { debugStackframe, debugStackframeFocused } from '../../browser/debugIcons.js';19import { getStackFrameThreadAndSessionToFocus } from '../../browser/debugService.js';20import { DebugSession } from '../../browser/debugSession.js';21import { IDebugService, IDebugSessionOptions, State } from '../../common/debug.js';22import { DebugModel, StackFrame, Thread } from '../../common/debugModel.js';23import { Source } from '../../common/debugSource.js';24import { createMockDebugModel, mockUriIdentityService } from './mockDebugModel.js';25import { MockRawSession } from '../common/mockDebug.js';2627const mockWorkspaceContextService = {28getWorkspace: () => {29return {30folders: []31};32}33} as any;3435export function createTestSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession {36return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, options, {37getViewModel(): any {38return {39updateViews(): void {40// noop41}42};43}44} 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());45}4647function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame; secondStackFrame: StackFrame } {48const thread = new class extends Thread {49public override getCallStack(): StackFrame[] {50return [firstStackFrame, secondStackFrame];51}52}(session, 'mockthread', 1);5354const firstSource = new Source({55name: 'internalModule.js',56path: 'a/b/c/d/internalModule.js',57sourceReference: 10,58}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());59const secondSource = new Source({60name: 'internalModule.js',61path: 'z/x/c/d/internalModule.js',62sourceReference: 11,63}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());6465const firstStackFrame = new StackFrame(thread, 0, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 0, true);66const secondStackFrame = new StackFrame(thread, 1, secondSource, 'app2.js', 'normal', { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 10 }, 1, true);6768return { firstStackFrame, secondStackFrame };69}7071suite('Debug - CallStack', () => {72let model: DebugModel;73let mockRawSession: MockRawSession;74const disposables = ensureNoDisposablesAreLeakedInTestSuite();7576setup(() => {77model = createMockDebugModel(disposables);78mockRawSession = new MockRawSession();79});8081teardown(() => {82sinon.restore();83});8485// Threads8687test('threads simple', () => {88const threadId = 1;89const threadName = 'firstThread';90const session = createTestSession(model);91disposables.add(session);92model.addSession(session);9394assert.strictEqual(model.getSessions(true).length, 1);95model.rawUpdate({96sessionId: session.getId(),97threads: [{98id: threadId,99name: threadName100}]101});102103assert.strictEqual(session.getThread(threadId)!.name, threadName);104105model.clearThreads(session.getId(), true);106assert.strictEqual(session.getThread(threadId), undefined);107assert.strictEqual(model.getSessions(true).length, 1);108});109110test('threads multiple with allThreadsStopped', async () => {111const threadId1 = 1;112const threadName1 = 'firstThread';113const threadId2 = 2;114const threadName2 = 'secondThread';115const stoppedReason = 'breakpoint';116117// Add the threads118const session = createTestSession(model);119disposables.add(session);120model.addSession(session);121122session['raw'] = <any>mockRawSession;123124model.rawUpdate({125sessionId: session.getId(),126threads: [{127id: threadId1,128name: threadName1129}]130});131132// Stopped event with all threads stopped133model.rawUpdate({134sessionId: session.getId(),135threads: [{136id: threadId1,137name: threadName1138}, {139id: threadId2,140name: threadName2141}],142stoppedDetails: {143reason: stoppedReason,144threadId: 1,145allThreadsStopped: true146},147});148149const thread1 = session.getThread(threadId1)!;150const thread2 = session.getThread(threadId2)!;151152// at the beginning, callstacks are obtainable but not available153assert.strictEqual(session.getAllThreads().length, 2);154assert.strictEqual(thread1.name, threadName1);155assert.strictEqual(thread1.stopped, true);156assert.strictEqual(thread1.getCallStack().length, 0);157assert.strictEqual(thread1.stoppedDetails!.reason, stoppedReason);158assert.strictEqual(thread2.name, threadName2);159assert.strictEqual(thread2.stopped, true);160assert.strictEqual(thread2.getCallStack().length, 0);161assert.strictEqual(thread2.stoppedDetails!.reason, undefined);162163// after calling getCallStack, the callstack becomes available164// and results in a request for the callstack in the debug adapter165await thread1.fetchCallStack();166assert.notStrictEqual(thread1.getCallStack().length, 0);167168await thread2.fetchCallStack();169assert.notStrictEqual(thread2.getCallStack().length, 0);170171// calling multiple times getCallStack doesn't result in multiple calls172// to the debug adapter173await thread1.fetchCallStack();174await thread2.fetchCallStack();175176// clearing the callstack results in the callstack not being available177thread1.clearCallStack();178assert.strictEqual(thread1.stopped, true);179assert.strictEqual(thread1.getCallStack().length, 0);180181thread2.clearCallStack();182assert.strictEqual(thread2.stopped, true);183assert.strictEqual(thread2.getCallStack().length, 0);184185model.clearThreads(session.getId(), true);186assert.strictEqual(session.getThread(threadId1), undefined);187assert.strictEqual(session.getThread(threadId2), undefined);188assert.strictEqual(session.getAllThreads().length, 0);189});190191test('allThreadsStopped in multiple events', async () => {192const threadId1 = 1;193const threadName1 = 'firstThread';194const threadId2 = 2;195const threadName2 = 'secondThread';196const stoppedReason = 'breakpoint';197198// Add the threads199const session = createTestSession(model);200disposables.add(session);201model.addSession(session);202203session['raw'] = <any>mockRawSession;204205// Stopped event with all threads stopped206model.rawUpdate({207sessionId: session.getId(),208threads: [{209id: threadId1,210name: threadName1211}, {212id: threadId2,213name: threadName2214}],215stoppedDetails: {216reason: stoppedReason,217threadId: threadId1,218allThreadsStopped: true219},220});221222model.rawUpdate({223sessionId: session.getId(),224threads: [{225id: threadId1,226name: threadName1227}, {228id: threadId2,229name: threadName2230}],231stoppedDetails: {232reason: stoppedReason,233threadId: threadId2,234allThreadsStopped: true235},236});237238const thread1 = session.getThread(threadId1)!;239const thread2 = session.getThread(threadId2)!;240241assert.strictEqual(thread1.stoppedDetails?.reason, stoppedReason);242assert.strictEqual(thread2.stoppedDetails?.reason, stoppedReason);243});244245test('threads multiple without allThreadsStopped', async () => {246const sessionStub = sinon.spy(mockRawSession, 'stackTrace');247248const stoppedThreadId = 1;249const stoppedThreadName = 'stoppedThread';250const runningThreadId = 2;251const runningThreadName = 'runningThread';252const stoppedReason = 'breakpoint';253const session = createTestSession(model);254disposables.add(session);255model.addSession(session);256257session['raw'] = <any>mockRawSession;258259// Add the threads260model.rawUpdate({261sessionId: session.getId(),262threads: [{263id: stoppedThreadId,264name: stoppedThreadName265}]266});267268// Stopped event with only one thread stopped269model.rawUpdate({270sessionId: session.getId(),271threads: [{272id: 1,273name: stoppedThreadName274}, {275id: runningThreadId,276name: runningThreadName277}],278stoppedDetails: {279reason: stoppedReason,280threadId: 1,281allThreadsStopped: false282}283});284285const stoppedThread = session.getThread(stoppedThreadId)!;286const runningThread = session.getThread(runningThreadId)!;287288// the callstack for the stopped thread is obtainable but not available289// the callstack for the running thread is not obtainable nor available290assert.strictEqual(stoppedThread.name, stoppedThreadName);291assert.strictEqual(stoppedThread.stopped, true);292assert.strictEqual(session.getAllThreads().length, 2);293assert.strictEqual(stoppedThread.getCallStack().length, 0);294assert.strictEqual(stoppedThread.stoppedDetails!.reason, stoppedReason);295assert.strictEqual(runningThread.name, runningThreadName);296assert.strictEqual(runningThread.stopped, false);297assert.strictEqual(runningThread.getCallStack().length, 0);298assert.strictEqual(runningThread.stoppedDetails, undefined);299300// after calling getCallStack, the callstack becomes available301// and results in a request for the callstack in the debug adapter302await stoppedThread.fetchCallStack();303assert.notStrictEqual(stoppedThread.getCallStack().length, 0);304assert.strictEqual(runningThread.getCallStack().length, 0);305assert.strictEqual(sessionStub.callCount, 1);306307// calling getCallStack on the running thread returns empty array308// and does not return in a request for the callstack in the debug309// adapter310await runningThread.fetchCallStack();311assert.strictEqual(runningThread.getCallStack().length, 0);312assert.strictEqual(sessionStub.callCount, 1);313314// clearing the callstack results in the callstack not being available315stoppedThread.clearCallStack();316assert.strictEqual(stoppedThread.stopped, true);317assert.strictEqual(stoppedThread.getCallStack().length, 0);318319model.clearThreads(session.getId(), true);320assert.strictEqual(session.getThread(stoppedThreadId), undefined);321assert.strictEqual(session.getThread(runningThreadId), undefined);322assert.strictEqual(session.getAllThreads().length, 0);323});324325test('stack frame get specific source name', () => {326const session = createTestSession(model);327disposables.add(session);328model.addSession(session);329const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);330331assert.strictEqual(getSpecificSourceName(firstStackFrame), '.../b/c/d/internalModule.js');332assert.strictEqual(getSpecificSourceName(secondStackFrame), '.../x/c/d/internalModule.js');333});334335test('stack frame toString()', () => {336const session = createTestSession(model);337disposables.add(session);338const thread = new Thread(session, 'mockthread', 1);339const firstSource = new Source({340name: 'internalModule.js',341path: 'a/b/c/d/internalModule.js',342sourceReference: 10,343}, 'aDebugSessionId', mockUriIdentityService, new NullLogService());344const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1, true);345assert.strictEqual(stackFrame.toString(), 'app (internalModule.js:1)');346347const secondSource = new Source(undefined, 'aDebugSessionId', mockUriIdentityService, new NullLogService());348const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2, true);349assert.strictEqual(stackFrame2.toString(), 'module');350});351352test('debug child sessions are added in correct order', () => {353const session = disposables.add(createTestSession(model));354model.addSession(session);355const secondSession = disposables.add(createTestSession(model, 'mockSession2'));356model.addSession(secondSession);357const firstChild = disposables.add(createTestSession(model, 'firstChild', { parentSession: session }));358model.addSession(firstChild);359const secondChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: session }));360model.addSession(secondChild);361const thirdSession = disposables.add(createTestSession(model, 'mockSession3'));362model.addSession(thirdSession);363const anotherChild = disposables.add(createTestSession(model, 'secondChild', { parentSession: secondSession }));364model.addSession(anotherChild);365366const sessions = model.getSessions();367assert.strictEqual(sessions[0].getId(), session.getId());368assert.strictEqual(sessions[1].getId(), firstChild.getId());369assert.strictEqual(sessions[2].getId(), secondChild.getId());370assert.strictEqual(sessions[3].getId(), secondSession.getId());371assert.strictEqual(sessions[4].getId(), anotherChild.getId());372assert.strictEqual(sessions[5].getId(), thirdSession.getId());373});374375test('decorations', () => {376const session = createTestSession(model);377disposables.add(session);378model.addSession(session);379const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);380let decorations = createDecorationsForStackFrame(firstStackFrame, true, false);381assert.strictEqual(decorations.length, 3);382assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));383assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));384assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));385assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');386assert.strictEqual(decorations[1].options.isWholeLine, true);387388decorations = createDecorationsForStackFrame(secondStackFrame, true, false);389assert.strictEqual(decorations.length, 2);390assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));391assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframeFocused));392assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));393assert.strictEqual(decorations[1].options.className, 'debug-focused-stack-frame-line');394assert.strictEqual(decorations[1].options.isWholeLine, true);395396decorations = createDecorationsForStackFrame(firstStackFrame, true, false);397assert.strictEqual(decorations.length, 3);398assert.deepStrictEqual(decorations[0].range, new Range(1, 2, 1, 3));399assert.strictEqual(decorations[0].options.glyphMarginClassName, ThemeIcon.asClassName(debugStackframe));400assert.deepStrictEqual(decorations[1].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));401assert.strictEqual(decorations[1].options.className, 'debug-top-stack-frame-line');402assert.strictEqual(decorations[1].options.isWholeLine, true);403// Inline decoration gets rendered in this case404assert.strictEqual(decorations[2].options.before?.inlineClassName, 'debug-top-stack-frame-column');405assert.deepStrictEqual(decorations[2].range, new Range(1, 2, 1, Constants.MAX_SAFE_SMALL_INTEGER));406});407408test('contexts', () => {409const session = createTestSession(model);410disposables.add(session);411model.addSession(session);412const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);413let context = getContext(firstStackFrame);414assert.strictEqual(context.sessionId, firstStackFrame.thread.session.getId());415assert.strictEqual(context.threadId, firstStackFrame.thread.getId());416assert.strictEqual(context.frameId, firstStackFrame.getId());417418context = getContext(secondStackFrame.thread);419assert.strictEqual(context.sessionId, secondStackFrame.thread.session.getId());420assert.strictEqual(context.threadId, secondStackFrame.thread.getId());421assert.strictEqual(context.frameId, undefined);422423context = getContext(session);424assert.strictEqual(context.sessionId, session.getId());425assert.strictEqual(context.threadId, undefined);426assert.strictEqual(context.frameId, undefined);427428let contributedContext = getContextForContributedActions(firstStackFrame);429assert.strictEqual(contributedContext, firstStackFrame.source.raw.path);430contributedContext = getContextForContributedActions(firstStackFrame.thread);431assert.strictEqual(contributedContext, firstStackFrame.thread.threadId);432contributedContext = getContextForContributedActions(session);433assert.strictEqual(contributedContext, session.getId());434});435436test('focusStackFrameThreadAndSession', () => {437const threadId1 = 1;438const threadName1 = 'firstThread';439const threadId2 = 2;440const threadName2 = 'secondThread';441const stoppedReason = 'breakpoint';442443// Add the threads444const session = new class extends DebugSession {445override get state(): State {446return State.Stopped;447}448}(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());449disposables.add(session);450451const runningSession = createTestSession(model);452disposables.add(runningSession);453model.addSession(runningSession);454model.addSession(session);455456session['raw'] = <any>mockRawSession;457458model.rawUpdate({459sessionId: session.getId(),460threads: [{461id: threadId1,462name: threadName1463}]464});465466// Stopped event with all threads stopped467model.rawUpdate({468sessionId: session.getId(),469threads: [{470id: threadId1,471name: threadName1472}, {473id: threadId2,474name: threadName2475}],476stoppedDetails: {477reason: stoppedReason,478threadId: 1,479allThreadsStopped: true480},481});482483const thread = session.getThread(threadId1)!;484const runningThread = session.getThread(threadId2);485486let toFocus = getStackFrameThreadAndSessionToFocus(model, undefined);487// Verify stopped session and stopped thread get focused488assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });489490toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, undefined, runningSession);491assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: undefined, session: runningSession });492493toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, thread);494assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: thread, session: session });495496toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, runningThread);497assert.deepStrictEqual(toFocus, { stackFrame: undefined, thread: runningThread, session: session });498499const stackFrame = new StackFrame(thread, 5, undefined!, 'stackframename2', undefined, undefined!, 1, true);500toFocus = getStackFrameThreadAndSessionToFocus(model, stackFrame);501assert.deepStrictEqual(toFocus, { stackFrame: stackFrame, thread: thread, session: session });502});503});504505506