Replay Debugger steps in/out/over (#279)

@W-4549465@
This commit is contained in:
Jonathan Widjaja 2018-01-31 14:14:27 -08:00 коммит произвёл GitHub
Родитель 5eb8a1e733
Коммит 91f116109e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 266 добавлений и 38 удалений

Просмотреть файл

@ -42,6 +42,13 @@ export type TraceCategory =
| 'launch'
| 'breakpoints';
export enum Step {
Over,
In,
Out,
Run
}
export interface LaunchRequestArguments
extends DebugProtocol.LaunchRequestArguments {
logFile: string;
@ -174,31 +181,81 @@ export class ApexReplayDebug extends LoggingDebugSession {
public continueRequest(
response: DebugProtocol.ContinueResponse,
args: DebugProtocol.ContinueArguments
): void {
this.executeStep(response, Step.Run);
}
public nextRequest(
response: DebugProtocol.NextResponse,
args: DebugProtocol.NextArguments
): void {
this.executeStep(response, Step.Over);
}
public stepInRequest(
response: DebugProtocol.StepInResponse,
args: DebugProtocol.StepInArguments
): void {
this.executeStep(response, Step.In);
}
public stepOutRequest(
response: DebugProtocol.StepOutResponse,
args: DebugProtocol.StepOutArguments
): void {
this.executeStep(response, Step.Out);
}
protected executeStep(
response: DebugProtocol.Response,
stepType: Step
): void {
response.success = true;
this.sendResponse(response);
const prevNumOfFrames = this.logContext.getNumOfFrames();
while (this.logContext.hasLogLines()) {
this.logContext.updateFrames(this.getHandlerForDebugConsole());
const topFrame = this.logContext.getTopFrame();
if (topFrame && topFrame.source) {
const topFrameUri = this.convertClientPathToDebugger(
topFrame.source.path
const curNumOfFrames = this.logContext.getNumOfFrames();
if (
(stepType === Step.Over &&
curNumOfFrames !== 0 &&
curNumOfFrames <= prevNumOfFrames) ||
(stepType === Step.In && curNumOfFrames >= prevNumOfFrames) ||
(stepType === Step.Out &&
curNumOfFrames !== 0 &&
curNumOfFrames < prevNumOfFrames)
) {
return this.sendEvent(
new StoppedEvent('step', ApexReplayDebug.THREAD_ID)
);
const topFrameLine = this.convertClientLineToDebugger(topFrame.line);
if (
this.breakpoints.has(topFrameUri) &&
this.breakpoints.get(topFrameUri)!.indexOf(topFrameLine) !== -1
) {
this.sendEvent(
new StoppedEvent('breakpoint', ApexReplayDebug.THREAD_ID)
);
return;
}
}
if (this.shouldStopForBreakpoint()) {
return;
}
}
this.sendEvent(new TerminatedEvent());
}
protected shouldStopForBreakpoint(): boolean {
const topFrame = this.logContext.getTopFrame();
if (topFrame && topFrame.source) {
const topFrameUri = this.convertClientPathToDebugger(
topFrame.source.path
);
const topFrameLine = this.convertClientLineToDebugger(topFrame.line);
if (
this.breakpoints.has(topFrameUri) &&
this.breakpoints.get(topFrameUri)!.indexOf(topFrameLine) !== -1
) {
this.sendEvent(
new StoppedEvent('breakpoint', ApexReplayDebug.THREAD_ID)
);
return true;
}
}
return false;
}
public setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments

Просмотреть файл

@ -18,4 +18,4 @@ export const EVENT_METHOD_EXIT = 'METHOD_EXIT';
export const EVENT_STATEMENT_EXECUTE = 'STATEMENT_EXECUTE';
export const EXEC_ANON_SIGNATURE = 'execute_anonymous_apex';
export const GET_LINE_BREAKPOINT_INFO_EVENT = 'getLineBreakpointInfo';
export const LINE_BREAKPOINT_INFO_REQUEST = 'lineBreakpointInfo';
export const LINE_BREAKPOINT_INFO_REQUEST = 'lineBreakpointInfo';

Просмотреть файл

@ -81,6 +81,10 @@ export class LogContext {
return this.stackFrameInfos;
}
public getNumOfFrames(): number {
return this.stackFrameInfos.length;
}
public getTopFrame(): StackFrame | undefined {
if (this.stackFrameInfos.length > 0) {
return this.stackFrameInfos[this.stackFrameInfos.length - 1];

Просмотреть файл

@ -54,6 +54,10 @@ export class MockApexReplayDebug extends ApexReplayDebug {
public getBreakpoints(): Map<string, number[]> {
return this.breakpoints;
}
public shouldStopForBreakpoint(): boolean {
return super.shouldStopForBreakpoint();
}
}
// tslint:disable:no-unused-expression
@ -165,9 +169,7 @@ describe('Replay debugger adapter - unit', () => {
0
).args[0];
expect(actualResponse.success).to.be.false;
expect(actualResponse.message).to.be.equal(
nls.localize('no_log_file_text')
);
expect(actualResponse.message).to.equal(nls.localize('no_log_file_text'));
});
it('Should send response', () => {
@ -180,7 +182,7 @@ describe('Replay debugger adapter - unit', () => {
expect(hasLogLinesStub.calledOnce).to.be.true;
expect(printToDebugConsoleStub.calledOnce).to.be.true;
const consoleMessage = printToDebugConsoleStub.getCall(0).args[0];
expect(consoleMessage).to.be.equal(
expect(consoleMessage).to.equal(
nls.localize('session_started_text', logFileName)
);
expect(sendResponseSpy.calledOnce).to.be.true;
@ -280,9 +282,7 @@ describe('Replay debugger adapter - unit', () => {
expect(printToDebugConsoleStub.calledOnce).to.be.true;
const consoleMessage = printToDebugConsoleStub.getCall(0).args[0];
expect(consoleMessage).to.be.equal(
nls.localize('session_terminated_text')
);
expect(consoleMessage).to.equal(nls.localize('session_terminated_text'));
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.DisconnectResponse = sendResponseSpy.getCall(
0
@ -401,7 +401,7 @@ describe('Replay debugger adapter - unit', () => {
let sendEventSpy: sinon.SinonSpy;
let hasLogLinesStub: sinon.SinonStub;
let updateFramesStub: sinon.SinonStub;
let getTopFrameStub: sinon.SinonStub;
let shouldStopForBreakpointStub: sinon.SinonStub;
let response: DebugProtocol.ContinueResponse;
let args: DebugProtocol.ContinueArguments;
const launchRequestArgs: LaunchRequestArguments = {
@ -429,8 +429,8 @@ describe('Replay debugger adapter - unit', () => {
if (updateFramesStub) {
updateFramesStub.restore();
}
if (getTopFrameStub) {
getTopFrameStub.restore();
if (shouldStopForBreakpointStub) {
shouldStopForBreakpointStub.restore();
}
});
@ -458,13 +458,89 @@ describe('Replay debugger adapter - unit', () => {
.onSecondCall()
.returns(false);
updateFramesStub = sinon.stub(LogContext.prototype, 'updateFrames');
getTopFrameStub = sinon
.stub(LogContext.prototype, 'getTopFrame')
.returns({ line: 2, source: { path: '/path/foo.cls' } } as StackFrame);
adapter.getBreakpoints().set('file:///path/foo.cls', [2]);
shouldStopForBreakpointStub = sinon
.stub(MockApexReplayDebug.prototype, 'shouldStopForBreakpoint')
.returns(true);
adapter.continueRequest(response, args);
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.StackTraceResponse = sendResponseSpy.getCall(
0
).args[0];
expect(actualResponse.success).to.be.true;
expect(sendEventSpy.called).to.be.false;
});
it('Should not hit breakpoint', () => {
hasLogLinesStub = sinon
.stub(LogContext.prototype, 'hasLogLines')
.onFirstCall()
.returns(true)
.onSecondCall()
.returns(false);
updateFramesStub = sinon.stub(LogContext.prototype, 'updateFrames');
shouldStopForBreakpointStub = sinon
.stub(MockApexReplayDebug.prototype, 'shouldStopForBreakpoint')
.returns(false);
adapter.continueRequest(response, args);
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.StackTraceResponse = sendResponseSpy.getCall(
0
).args[0];
expect(actualResponse.success).to.be.true;
expect(sendEventSpy.calledOnce).to.be.true;
const event = sendEventSpy.getCall(0).args[0];
expect(event).to.be.instanceof(TerminatedEvent);
});
});
describe('Stepping', () => {
let sendResponseSpy: sinon.SinonSpy;
let sendEventSpy: sinon.SinonSpy;
let hasLogLinesStub: sinon.SinonStub;
let updateFramesStub: sinon.SinonStub;
let getNumOfFramesStub: sinon.SinonStub;
beforeEach(() => {
sendResponseSpy = sinon.spy(ApexReplayDebug.prototype, 'sendResponse');
sendEventSpy = sinon.spy(ApexReplayDebug.prototype, 'sendEvent');
updateFramesStub = sinon.stub(LogContext.prototype, 'updateFrames');
hasLogLinesStub = sinon
.stub(LogContext.prototype, 'hasLogLines')
.onFirstCall()
.returns(true)
.onSecondCall()
.returns(false);
});
afterEach(() => {
sendResponseSpy.restore();
sendEventSpy.restore();
hasLogLinesStub.restore();
updateFramesStub.restore();
getNumOfFramesStub.restore();
});
it('Should send step over', () => {
getNumOfFramesStub = sinon
.stub(LogContext.prototype, 'getNumOfFrames')
.onFirstCall()
.returns(2)
.onSecondCall()
.returns(2);
adapter.nextRequest(
Object.assign(adapter.getDefaultResponse(), {
body: {}
}),
{
threadId: ApexReplayDebug.THREAD_ID
}
);
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.StackTraceResponse = sendResponseSpy.getCall(
0
@ -473,12 +549,71 @@ describe('Replay debugger adapter - unit', () => {
expect(sendEventSpy.calledOnce).to.be.true;
const event = sendEventSpy.getCall(0).args[0];
expect(event).to.be.instanceof(StoppedEvent);
expect((event as StoppedEvent).body.reason).to.equal('step');
});
it('Should send step in', () => {
getNumOfFramesStub = sinon
.stub(LogContext.prototype, 'getNumOfFrames')
.onFirstCall()
.returns(2)
.onSecondCall()
.returns(3);
adapter.stepInRequest(
Object.assign(adapter.getDefaultResponse(), {
body: {}
}),
{
threadId: ApexReplayDebug.THREAD_ID
}
);
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.StackTraceResponse = sendResponseSpy.getCall(
0
).args[0];
expect(actualResponse.success).to.be.true;
expect(sendEventSpy.calledOnce).to.be.true;
const event = sendEventSpy.getCall(0).args[0];
expect(event).to.be.instanceof(StoppedEvent);
expect((event as StoppedEvent).body.reason).to.equal('step');
});
it('Should send step out', () => {
getNumOfFramesStub = sinon
.stub(LogContext.prototype, 'getNumOfFrames')
.onFirstCall()
.returns(2)
.onSecondCall()
.returns(1);
adapter.stepOutRequest(
Object.assign(adapter.getDefaultResponse(), {
body: {}
}),
{
threadId: ApexReplayDebug.THREAD_ID
}
);
expect(sendResponseSpy.calledOnce).to.be.true;
const actualResponse: DebugProtocol.StackTraceResponse = sendResponseSpy.getCall(
0
).args[0];
expect(actualResponse.success).to.be.true;
expect(sendEventSpy.calledOnce).to.be.true;
const event = sendEventSpy.getCall(0).args[0];
expect(event).to.be.instanceof(StoppedEvent);
expect((event as StoppedEvent).body.reason).to.equal('step');
});
});
describe('Breakpoints', () => {
let sendResponseSpy: sinon.SinonSpy;
let sendEventSpy: sinon.SinonSpy;
let canSetLineBreakpointStub: sinon.SinonStub;
let getTopFrameStub: sinon.SinonStub;
let response: DebugProtocol.SetBreakpointsResponse;
let args: DebugProtocol.SetBreakpointsArguments;
const launchRequestArgs: LaunchRequestArguments = {
@ -496,13 +631,44 @@ describe('Replay debugger adapter - unit', () => {
source: {}
};
sendResponseSpy = sinon.spy(ApexReplayDebug.prototype, 'sendResponse');
sendEventSpy = sinon.spy(ApexReplayDebug.prototype, 'sendEvent');
});
afterEach(() => {
sendResponseSpy.restore();
sendEventSpy.restore();
if (canSetLineBreakpointStub) {
canSetLineBreakpointStub.restore();
}
if (getTopFrameStub) {
getTopFrameStub.restore();
}
});
it('Should stop for breakpoint', () => {
getTopFrameStub = sinon
.stub(LogContext.prototype, 'getTopFrame')
.returns({ line: 2, source: { path: '/path/foo.cls' } } as StackFrame);
adapter.getBreakpoints().set('file:///path/foo.cls', [2]);
const isStopped = adapter.shouldStopForBreakpoint();
expect(isStopped).to.be.true;
expect(sendEventSpy.called).to.be.true;
const event = sendEventSpy.getCall(0).args[0];
expect(event).to.be.instanceof(StoppedEvent);
});
it('Should not stop for breakpoint', () => {
getTopFrameStub = sinon
.stub(LogContext.prototype, 'getTopFrame')
.returns({ line: 2, source: { path: '/path/foo.cls' } } as StackFrame);
adapter.getBreakpoints().set('file:///path/bar.cls', [2]);
const isStopped = adapter.shouldStopForBreakpoint();
expect(isStopped).to.be.false;
expect(sendEventSpy.called).to.be.false;
});
it('Should not return breakpoints when path argument is invalid', () => {

Просмотреть файл

@ -96,6 +96,7 @@ describe('LogContext', () => {
it('Should start with empty array of stackframes', () => {
expect(context.getFrames()).to.be.empty;
expect(context.getNumOfFrames()).to.equal(0);
expect(context.getTopFrame()).to.be.undefined;
});
@ -177,8 +178,8 @@ describe('LogContext', () => {
context.setState(new LogEntryState());
context.parseLogEvent(`${EVENT_EXECUTE_ANONYMOUS}: foo`);
expect(context.getExecAnonScriptMapping().size).to.be.equal(1);
expect(context.getExecAnonScriptMapping().get(1)).to.be.equal(0);
expect(context.getExecAnonScriptMapping().size).to.equal(1);
expect(context.getExecAnonScriptMapping().get(1)).to.equal(0);
});
it('Should detect FrameEntry with CODE_UNIT_STARTED', () => {
@ -255,7 +256,7 @@ describe('LogContext', () => {
});
it('Should return debug log path for execute anonymous signature', () => {
expect(context.getUriFromSignature(EXEC_ANON_SIGNATURE)).to.be.equal(
expect(context.getUriFromSignature(EXEC_ANON_SIGNATURE)).to.equal(
encodeURI('file://' + context.getLogFilePath())
);
});
@ -263,7 +264,7 @@ describe('LogContext', () => {
it('Should return URI for inner class', () => {
expect(
context.getUriFromSignature('namespace.Foo.Bar(Integer)')
).to.be.equal('/path/foo.cls');
).to.equal('/path/foo.cls');
});
});
});

Просмотреть файл

@ -27,7 +27,7 @@ describe('Log context utilities', () => {
});
it('Should strip brackets', () => {
expect(util.stripBrackets('[20]')).to.be.equal('20');
expect(util.stripBrackets('[20]')).to.equal('20');
});
});
});

Просмотреть файл

@ -43,7 +43,7 @@ describe('Frame entry event', () => {
expect(state.handle(context)).to.be.false;
const frames = context.getFrames();
expect(frames.length).to.be.equal(2);
expect(context.getNumOfFrames()).to.equal(2);
expect(frames[1]).to.deep.equal({
id: 1,
line: 0,

Просмотреть файл

@ -41,7 +41,7 @@ describe('LogEntry event', () => {
expect(isStopped).to.be.true;
const stackFrames = context.getFrames();
expect(stackFrames.length).to.equal(1);
expect(context.getNumOfFrames()).to.equal(1);
const stackFrame = stackFrames[0];
expect(stackFrame.id).to.equal(0);
expect(stackFrame.name).to.equal('');

Просмотреть файл

@ -39,7 +39,7 @@ describe('Statement execute event', () => {
const state = new StatementExecuteState(['2']);
expect(state.handle(context)).to.be.true;
expect(context.getFrames()[0].line).to.be.equal(2);
expect(context.getFrames()[0].line).to.equal(2);
});
it('Should update execute anonymous specific frame', () => {
@ -48,6 +48,6 @@ describe('Statement execute event', () => {
const state = new StatementExecuteState(['2']);
expect(state.handle(context)).to.be.true;
expect(context.getFrames()[0].line).to.be.equal(5);
expect(context.getFrames()[0].line).to.equal(5);
});
});