Skip to content
This repository has been archived by the owner on Oct 2, 2021. It is now read-only.

Support source mapping of stack traces in the Debug Console #6 #190

Merged
merged 5 commits into from
Mar 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions src/chrome/chromeDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,10 +740,38 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter {

protected onExceptionThrown(params: Crdp.Runtime.ExceptionThrownEvent): void {
const formattedException = formatExceptionDetails(params.exceptionDetails);
this._session.sendEvent(new OutputEvent(
formattedException,
'stderr'
));
this.sourceMapFormattedException(formattedException).then(exceptionStr => {
this._session.sendEvent(new OutputEvent(
exceptionStr,
'stderr'
));
});
}

// We parse stack trace from `formattedException`, source map it and return a new string
protected async sourceMapFormattedException(formattedException: string): Promise<string> {
const exceptionLines = formattedException.split(/\r?\n/);

for (let i = 0, len = exceptionLines.length; i < len; ++i) {
const line = exceptionLines[i];
const matches = line.match(/^\s+at (.*?)\s*\(?([^ ]+\.js):(\d+):(\d+)\)?$/);

if (!matches) continue;
const path = matches[2];
const lineNum = parseInt(matches[3], 10);
const columnNum = parseInt(matches[4], 10);
const clientPath = this._pathTransformer.getClientPathFromTargetPath(path);
const mapped = await this._sourceMapTransformer.mapToAuthored(clientPath || path, lineNum, columnNum);

if (mapped && mapped.source && mapped.line && mapped.column) {
exceptionLines[i] = exceptionLines[i].replace(
`${path}:${lineNum}:${columnNum}`,
`${mapped.source}:${mapped.line}:${mapped.column}`
);
}
}

return exceptionLines.join('\n');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/consoleHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function stackTraceToString(stackTrace: Crdp.Runtime.StackTrace): string {
.map(frame => {
const fnName = frame.functionName || (frame.url ? '(anonymous)' : '(eval)');
const fileName = frame.url ? url.parse(frame.url).pathname : 'eval';
return ` at ${fnName} (${fileName}:${frame.lineNumber})`;
return ` at ${fnName} (${fileName}:${frame.lineNumber}:${frame.columnNumber})`;
})
.join('\n');
}
Expand Down
102 changes: 89 additions & 13 deletions test/chrome/chromeDebugAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,30 @@ suite('ChromeDebugAdapter', () => {
mockSourceMapTransformer = getMockSourceMapTransformer();
mockPathTransformer = getMockPathTransformer();

initChromeDebugAdapter();
});

function initChromeDebugAdapter(): void {
// Instantiate the ChromeDebugAdapter, injecting the mock ChromeConnection
/* tslint:disable */
chromeDebugAdapter = new (require(MODULE_UNDER_TEST).ChromeDebugAdapter)({
chromeConnection: function() { return mockChromeConnection.object; },
lineColTransformer: function() { return mockLineNumberTransformer.object; },
sourceMapTransformer: function() { return mockSourceMapTransformer.object; },
pathTransformer: function() { return mockPathTransformer.object; }
chromeConnection: function () { return mockChromeConnection.object; },
lineColTransformer: function () { return mockLineNumberTransformer.object; },
sourceMapTransformer: function () { return mockSourceMapTransformer.object; },
pathTransformer: function () { return mockPathTransformer.object; }
},
{
sendEvent: (e: DebugProtocol.Event) => {
if (sendEventHandler) {
// Filter telemetry events
if (!(e.event === 'output' && (<DebugProtocol.OutputEvent>e).body.category === 'telemetry')) {
sendEventHandler(e);
{
sendEvent: (e: DebugProtocol.Event) => {
if (sendEventHandler) {
// Filter telemetry events
if (!(e.event === 'output' && (<DebugProtocol.OutputEvent>e).body.category === 'telemetry')) {
sendEventHandler(e);
}
}
}
}
});
});
/* tslint:enable */
});
}

teardown(() => {
sendEventHandler = undefined;
Expand All @@ -113,6 +117,16 @@ suite('ChromeDebugAdapter', () => {
mockEventEmitter.emit('Debugger.scriptParsed', <Crdp.Debugger.ScriptParsedEvent>{ scriptId, url });
}

// Helper to run async asserts inside promises so they can be correctly awaited
function asyncAssert(assertFn: Function, resolve: (value?: any) => void, reject: (reason?: any) => void): void {
try {
assertFn();
resolve();
} catch (e) {
reject(e);
}
}

suite('attach()', () => {
test('if successful, an initialized event is fired', () => {
let initializedFired = false;
Expand Down Expand Up @@ -468,6 +482,68 @@ suite('ChromeDebugAdapter', () => {
});
});

suite('onExceptionThrown', () => {
const authoredPath = '/Users/me/error.ts';
const generatedPath = 'http://localhost:9999/error.js';

const getExceptionStr = (path, line) => 'Error: kaboom!\n' +
` at error (${path}:${line}:1)\n` +
` at ${path}:${line}:1`;

const generatedExceptionStr = getExceptionStr(generatedPath, 6);
const authoredExceptionStr = getExceptionStr(authoredPath, 12);

const exceptionEvent: Crdp.Runtime.ExceptionThrownEvent = {
"timestamp": 1490164925297,
"exceptionDetails": {
"exceptionId": 21,
"text": "Uncaught",
"lineNumber": 5,
"columnNumber": 10,
"url": "http://localhost:9999/error.js",
"stackTrace": null,
"exception": {
"type": "object",
"subtype": "error",
"className": "Error",
"description": generatedExceptionStr,
"objectId": "{\"injectedScriptId\":148,\"id\":1}"
},
"executionContextId": 148
}
};

test('passes through exception when no source mapping present', async () => {
await chromeDebugAdapter.attach(ATTACH_ARGS);
const sendEventP = new Promise((resolve, reject) => {
sendEventHandler = (event) =>
asyncAssert(() => assert.equal(event.body.output, generatedExceptionStr), resolve, reject);
});

mockEventEmitter.emit('Runtime.exceptionThrown', exceptionEvent);
await sendEventP;
});

test('translates callstack to authored files via source mapping', async () => {
// We need to reset mocks and re-initialize chromeDebugAdapter
// because reset() creates a new instance of object
mockSourceMapTransformer.reset();
initChromeDebugAdapter();

await chromeDebugAdapter.attach(ATTACH_ARGS);
const sendEventP = new Promise((resolve, reject) => {
sendEventHandler = (event) =>
asyncAssert(() => assert.equal(event.body.output, authoredExceptionStr), resolve, reject);
});

mockSourceMapTransformer.setup(m => m.mapToAuthored(It.isValue(generatedPath), It.isAnyNumber(), It.isAnyNumber()))
.returns(() => Promise.resolve({ source: authoredPath, line: 12, column: 1 }));

mockEventEmitter.emit('Runtime.exceptionThrown', exceptionEvent);
await sendEventP;
});
});

suite('setExceptionBreakpoints()', () => { });
suite('stepping', () => { });
suite('stackTrace()', () => { });
Expand Down
4 changes: 2 additions & 2 deletions test/chrome/consoleHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ suite('ConsoleHelper', () => {

suite('console.assert()', () => {
test(`Prints params and doesn't resolve format specifiers`, () => {
doAssertForString(Runtime.makeAssert('Fail %s 123', 456), 'Assertion failed: Fail %s 123 456\n at myFn (/script/a.js:4)', true);
doAssertForString(Runtime.makeAssert('Fail %s 123', 456), 'Assertion failed: Fail %s 123 456\n at myFn (/script/a.js:4:1)', true);
});
});
});
Expand Down Expand Up @@ -162,7 +162,7 @@ namespace Runtime {

export function makeAssert(...args: any[]): Crdp.Runtime.ConsoleAPICalledEvent {
const fakeStackTrace: Crdp.Runtime.StackTrace = {
callFrames: [{ url: '/script/a.js', lineNumber: 4, columnNumber: 0, scriptId: '1', functionName: 'myFn' }]
callFrames: [{ url: '/script/a.js', lineNumber: 4, columnNumber: 1, scriptId: '1', functionName: 'myFn' }]
};
return makeMockMessage('assert', args, { level: 'error', stackTrace: fakeStackTrace });
}
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/debugProtocolMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function getRuntimeStubs(mockEventEmitter) {
evaluate() { },

onConsoleAPICalled(handler) { mockEventEmitter.on('Runtime.consoleAPICalled', handler); },
onExceptionThrown(handler) { mockEventEmitter.on('Runtime.onExceptionThrown', handler); },
onExceptionThrown(handler) { mockEventEmitter.on('Runtime.exceptionThrown', handler); },
onExecutionContextsCleared(handler) { mockEventEmitter.on('Runtime.executionContextsCleared', handler); }
};
}
Expand Down