diff --git a/src/GDBDebugSession.ts b/src/GDBDebugSession.ts index 081a2660..5967869f 100644 --- a/src/GDBDebugSession.ts +++ b/src/GDBDebugSession.ts @@ -335,6 +335,7 @@ export class GDBDebugSession extends LoggingDebugSession { response.body.supportsDisassembleRequest = true; response.body.supportsReadMemoryRequest = true; response.body.supportsWriteMemoryRequest = true; + response.body.supportsSteppingGranularity = true; this.sendResponse(response); } @@ -1003,7 +1004,9 @@ export class GDBDebugSession extends LoggingDebugSession { args: DebugProtocol.NextArguments ): Promise { try { - await mi.sendExecNext(this.gdb, args.threadId); + await (args.granularity === 'instruction' + ? mi.sendExecNextInstruction(this.gdb, args.threadId) + : mi.sendExecNext(this.gdb, args.threadId)); this.sendResponse(response); } catch (err) { this.sendErrorResponse( @@ -1019,7 +1022,9 @@ export class GDBDebugSession extends LoggingDebugSession { args: DebugProtocol.StepInArguments ): Promise { try { - await mi.sendExecStep(this.gdb, args.threadId); + await (args.granularity === 'instruction' + ? mi.sendExecStepInstruction(this.gdb, args.threadId) + : mi.sendExecStep(this.gdb, args.threadId)); this.sendResponse(response); } catch (err) { this.sendErrorResponse( diff --git a/src/MIParser.ts b/src/MIParser.ts index 4e2f060d..c45b64ad 100644 --- a/src/MIParser.ts +++ b/src/MIParser.ts @@ -177,27 +177,15 @@ export class MIParser { const result: any = {}; if (c === '{') { c = this.next(); - if (c !== '"') { - // oject contains name-value pairs - while (c !== '}') { - if (c !== ',') { - this.back(); - } - const name = this.handleString(); - if (this.next() === '=') { - result[name] = this.handleValue(); - } - c = this.next(); + while (c !== '}') { + if (c !== ',') { + this.back(); } - } else { - // "object" contains just values - this.back(); - let key = 0; - while (c !== '}') { - let value = this.handleCString(); - if (value) result[key++] = value; - c = this.next(); + const name = this.handleString(); + if (this.next() === '=') { + result[name] = this.handleValue(); } + c = this.next(); } } diff --git a/src/integration-tests/miparser.spec.ts b/src/integration-tests/miparser.spec.ts index 2a9f2679..07d705c8 100644 --- a/src/integration-tests/miparser.spec.ts +++ b/src/integration-tests/miparser.spec.ts @@ -154,23 +154,4 @@ describe('MI Parser Test Suite', function () { 'stdout' ); }); - - it('structure that starts with a curly bracket and contains values but not keys', async function () { - parser.parseLine( - '+message,bkpt={number="1",type="breakpoint",thread-groups=["i1"],script={"p }123","p 321","p 789"}}' - ); - sinon.assert.calledOnceWithExactly( - gdbBackendMock.emit as sinon.SinonStub, - 'statusAsync', - 'message', - { - bkpt: { - number: '1', - type: 'breakpoint', - 'thread-groups': ['i1'], - script: { '0': 'p }123', '1': 'p 321', '2': 'p 789' }, - }, - } - ); - }); }); diff --git a/src/integration-tests/stepping-granularity.spec.ts b/src/integration-tests/stepping-granularity.spec.ts new file mode 100644 index 00000000..96627241 --- /dev/null +++ b/src/integration-tests/stepping-granularity.spec.ts @@ -0,0 +1,177 @@ +/********************************************************************* + * Copyright (c) 2023 Ericsson and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +import * as path from 'path'; +import { expect } from 'chai'; +import { CdtDebugClient } from './debugClient'; +import { standardBeforeEach, testProgramsDir, fillDefaults } from './utils'; +import { DebugProtocol } from '@vscode/debugprotocol'; + +interface StackState { + main: DebugProtocol.StackFrame | undefined; + elsewhere: DebugProtocol.StackFrame | undefined; +} + +interface StackStateCheck { + elsewhereDefined: boolean; + line: number; +} + +describe('Stepping', async function () { + let dc: CdtDebugClient; + const steppingProgram = path.join(testProgramsDir, 'stepping'); + const steppingSource = path.join(testProgramsDir, 'stepping.c'); + + beforeEach(async function () { + dc = await standardBeforeEach(); + + await dc.hitBreakpoint( + fillDefaults(this.currentTest, { program: steppingProgram }), + { + path: steppingSource, + line: 8, + } + ); + }); + + afterEach(async () => { + await dc.stop(); + }); + + async function getFrameState(threadId: number) { + const stack = await dc.stackTraceRequest({ threadId }); + const main = stack.body.stackFrames.find( + (frame) => frame.name === 'main' + ); + const elsewhere = stack.body.stackFrames.find( + (frame) => frame.name === 'getFromElsewhere' + ); + return { main, elsewhere }; + } + + function expectStackState(state: StackState, check: StackStateCheck) { + if (check.elsewhereDefined) { + expect(state.elsewhere).not.to.be.undefined; + } else { + expect(state.elsewhere).to.be.undefined; + } + const target = check.elsewhereDefined ? 'elsewhere' : 'main'; + expect(state[target]).not.to.be.undefined; + expect(state[target]?.line).equal( + check.line, + `It should have stopped at line ${check.line}` + ); + } + + it('steps in by line', async () => { + const threads = await dc.threadsRequest(); + const threadId = threads.body.threads[0].id; + expectStackState(await getFrameState(threadId), { + elsewhereDefined: false, + line: 8, + }); + await Promise.all([ + dc.stepInRequest({ threadId, granularity: 'statement' }), + dc.waitForEvent('stopped'), + ]); + expectStackState(await getFrameState(threadId), { + elsewhereDefined: true, + line: 15, + }); + await Promise.all([ + dc.stepInRequest({ threadId, granularity: 'statement' }), + dc.waitForEvent('stopped'), + ]); + expectStackState(await getFrameState(threadId), { + elsewhereDefined: true, + line: 16, + }); + }); + + it('steps in by instruction', async () => { + const threads = await dc.threadsRequest(); + const threadId = threads.body.threads[0].id; + let state = await getFrameState(threadId); + expectStackState(state, { + elsewhereDefined: false, + line: 8, + }); + await Promise.all([ + dc.stepInRequest({ threadId, granularity: 'instruction' }), + dc.waitForEvent('stopped'), + ]); + // First step should not take us straight to the function. + expectStackState((state = await getFrameState(threadId)), { + elsewhereDefined: false, + line: 8, + }); + // Step until we leave that line. + while (state.main?.line === 8 && !state.elsewhere) { + await Promise.all([ + dc.stepInRequest({ threadId, granularity: 'instruction' }), + dc.waitForEvent('stopped'), + ]); + state = await getFrameState(threadId); + } + // First line we see should be inside `getFromElsewhere` + expectStackState(state, { + elsewhereDefined: true, + line: 14, + }); + }); + + it('steps next by line and skips a function', async () => { + const threads = await dc.threadsRequest(); + const threadId = threads.body.threads[0].id; + expectStackState(await getFrameState(threadId), { + elsewhereDefined: false, + line: 8, + }); + await Promise.all([ + dc.nextRequest({ threadId, granularity: 'statement' }), + dc.waitForEvent('stopped'), + ]); + expectStackState(await getFrameState(threadId), { + elsewhereDefined: false, + line: 9, + }); + await Promise.all([ + dc.nextRequest({ threadId, granularity: 'statement' }), + dc.waitForEvent('stopped'), + ]); + expectStackState(await getFrameState(threadId), { + elsewhereDefined: false, + line: 6, + }); + }); + + it('steps next by instruction and skips a function', async () => { + const threads = await dc.threadsRequest(); + const threadId = threads.body.threads[0].id; + let state = await getFrameState(threadId); + expectStackState(state, { + elsewhereDefined: false, + line: 8, + }); + // Step until we get off line 8. + while (state.main?.line === 8 && !state.elsewhere) { + await Promise.all([ + dc.nextRequest({ threadId, granularity: 'instruction' }), + dc.waitForEvent('stopped'), + ]); + state = await getFrameState(threadId); + } + // The first line we should see after 8 is nine, not something in `getFromElsewhere`. + expectStackState(state, { + elsewhereDefined: false, + line: 9, + }); + }); +}); diff --git a/src/integration-tests/test-programs/.gitignore b/src/integration-tests/test-programs/.gitignore index 7166b0f1..5ddf1ca6 100644 --- a/src/integration-tests/test-programs/.gitignore +++ b/src/integration-tests/test-programs/.gitignore @@ -21,3 +21,4 @@ MultiThreadRunControl /log stderr bug275-测试 +stepping diff --git a/src/integration-tests/test-programs/Makefile b/src/integration-tests/test-programs/Makefile index 0472bea7..7ffa6a64 100644 --- a/src/integration-tests/test-programs/Makefile +++ b/src/integration-tests/test-programs/Makefile @@ -1,4 +1,4 @@ -BINS = empty empty\ space evaluate vars vars_cpp vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试 cwd.exe +BINS = empty empty\ space evaluate vars vars_cpp vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试 cwd.exe stepping .PHONY: all all: $(BINS) @@ -86,6 +86,9 @@ MultiThreadRunControl: MultiThreadRunControl.o stderr: stderr.o $(LINK) +stepping: stepping.o + $(LINK) + %.o: %.c $(CC) -c $< -g3 -O0 diff --git a/src/integration-tests/test-programs/stepping.c b/src/integration-tests/test-programs/stepping.c new file mode 100644 index 00000000..79b3f686 --- /dev/null +++ b/src/integration-tests/test-programs/stepping.c @@ -0,0 +1,22 @@ +#include + +int main (int argc, char *argv[]) { + char knownLocally = 10; + int i; + for (i = 0; i < 3; i++) { + knownLocally += 1; + int gottenFromElsewhere = getFromElsewhere(knownLocally); + printf("Saw it here first: %d", knownLocally); + } + return 0; +} + +// make the line of code the same as opening brace to account for different gdb/gcc combinations +int getFromElsewhere(int start) +{ int result = start; int i; + for (i = 1; i <= 5; i++) { + result += i; + printf("Eventually, I'll return something like... %d", result); + } + return result; +} diff --git a/src/mi/exec.ts b/src/mi/exec.ts index 97e2e580..029f2ab5 100644 --- a/src/mi/exec.ts +++ b/src/mi/exec.ts @@ -39,6 +39,14 @@ export function sendExecNext(gdb: GDBBackend, threadId?: number) { return gdb.sendCommand(command); } +export function sendExecNextInstruction(gdb: GDBBackend, threadId?: number) { + let command = '-exec-next-instruction'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + export function sendExecStep(gdb: GDBBackend, threadId?: number) { let command = '-exec-step'; if (threadId !== undefined) { @@ -47,6 +55,14 @@ export function sendExecStep(gdb: GDBBackend, threadId?: number) { return gdb.sendCommand(command); } +export function sendExecStepInstruction(gdb: GDBBackend, threadId?: number) { + let command = '-exec-step-instruction'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + export function sendExecFinish(gdb: GDBBackend, threadId?: number) { let command = '-exec-finish'; if (threadId !== undefined) {