diff --git a/src/adapter/smartStepping.test.ts b/src/adapter/smartStepping.test.ts new file mode 100644 index 000000000..23cefb1fd --- /dev/null +++ b/src/adapter/smartStepping.test.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { expect } from 'chai'; +import { createStubInstance, stub } from 'sinon'; +import { Logger } from '../common/logging/logger'; +import { upcastPartial } from '../common/objUtils'; +import { nodeLaunchConfigDefaults } from '../configuration'; +import { IPausedDetails, StepDirection } from './pause'; +import { SmartStepper } from './smartStepping'; +import { Source, SourceLocationProvider } from './source'; +import { IPreferredUiLocation, UnmappedReason } from './sourceContainer'; +import { StackFrame, StackTrace } from './stackTrace'; + +describe('SmartStepper', () => { + for (const kind of ['skipped', 'unmapped'] as const) { + describe(`${kind} frame`, () => { + let stepper: SmartStepper; + let paused: IPausedDetails; + + beforeEach(() => { + stepper = new SmartStepper({ ...nodeLaunchConfigDefaults, smartStep: true }, Logger.null); + const source = createStubInstance(Source); + source.blackboxed.returns(kind === 'skipped'); + if (kind === 'unmapped') { + source.sourceMap = upcastPartial({}); + } + + const frame = Object.assign(createStubInstance(StackFrame), { + uiLocation: stub().resolves(upcastPartial({ + source, + isMapped: false, + unmappedReason: UnmappedReason.MapPositionMissing, + })), + }); + const stackTrace = createStubInstance(StackTrace); + stackTrace.loadFrames.resolves([frame]); + paused = upcastPartial({ reason: 'pause', stackTrace }); + }); + + it('honors an explicit pause request', async () => { + expect(await stepper.getSmartStepDirection(paused, { reason: 'pause' })).to.be.undefined; + }); + + it('still smart steps a pause without explicit pause intent', async () => { + expect(await stepper.getSmartStepDirection(paused)).to.equal(StepDirection.In); + }); + + for (const direction of [StepDirection.In, StepDirection.Over, StepDirection.Out]) { + it(`preserves step direction ${direction}`, async () => { + expect(await stepper.getSmartStepDirection(paused, { reason: 'step', direction })) + .to.equal(direction); + }); + } + + for (const reason of ['breakpoint', 'exception', 'entry'] as const) { + it(`preserves ${reason} stops`, async () => { + expect(await stepper.getSmartStepDirection({ ...paused, reason })).to.be.undefined; + }); + } + + it('resets the automatic stepping limit when explicitly paused', async () => { + for (let i = 0; i < 258; i++) { + await stepper.getSmartStepDirection(paused); + } + expect(await stepper.getSmartStepDirection(paused)).to.equal(StepDirection.Out); + + await stepper.getSmartStepDirection(paused, { reason: 'pause' }); + + expect( + await stepper.getSmartStepDirection(paused, { + reason: 'step', + direction: StepDirection.In, + }), + ).to.equal(StepDirection.In); + }); + }); + } +}); diff --git a/src/adapter/smartStepping.ts b/src/adapter/smartStepping.ts index 87295c4c2..8e4c352fc 100644 --- a/src/adapter/smartStepping.ts +++ b/src/adapter/smartStepping.ts @@ -76,6 +76,13 @@ export class SmartStepper { return; } + // An explicit pause must leave the debuggee stopped, even in skipped code. + // Other pauses (such as debugger statements) can still be smart stepped. + if (reason?.reason === 'pause') { + this.resetSmartStepCount(); + return; + } + if (neverStepReasons.has(pausedDetails.reason)) { return; } diff --git a/src/test/node/node-runtime.test.ts b/src/test/node/node-runtime.test.ts index b00a6d317..3f12cd951 100644 --- a/src/test/node/node-runtime.test.ts +++ b/src/test/node/node-runtime.test.ts @@ -54,6 +54,35 @@ describe('node runtime', () => { } describe('skipFiles', () => { + itIntegrates('honors an explicit pause in skipped code', async ({ r }) => { + createFileTree(testFixturesDir, { + 'skipped.js': ['setInterval(() => {}, 10);', 'console.log("ready");'], + }); + const handle = await r.runScript('skipped.js', { + smartStep: true, + skipFiles: ['/**', '**/skipped.js'], + }); + const ready = handle.dap.once('output', event => event.output.includes('ready')); + await handle.load(); + await ready; + + const { threads } = await handle.dap.threads({}); + const threadId = threads[0].id; + const stopped = handle.dap.once('stopped'); + await handle.dap.pause({ threadId }); + expect((await stopped).reason).to.equal('pause'); + + const { stackFrames } = await handle.dap.stackTrace({ threadId }); + expect(stackFrames).to.not.be.empty; + const evaluated = await handle.dap.evaluate({ + expression: '1 + 1', + frameId: stackFrames[0].id, + context: 'watch', + }); + expect(evaluated.result).to.equal('2'); + await handle.dap.continue({ threadId }); + }); + itIntegrates('skipFiles skip node internals', async ({ r }) => { await r.initialize; const cwd = join(testWorkspace, 'simpleNode');