Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Stacktrace test improvements #840

Merged
merged 5 commits into from
May 23, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
22 changes: 16 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

144 changes: 78 additions & 66 deletions test/int/stackTrace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import * as assert from 'assert';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import * as testSetup from './testSetup';
import { expect } from 'chai';
import { URL } from 'url';
import { DebugProtocol } from 'vscode-debugprotocol';
import { FrameworkTestContext, TestProjectSpec } from './framework/frameworkTestSupport';
import { puppeteerSuite, puppeteerTest } from './puppeteer/puppeteerSuite';
import { setBreakpoint } from './intTestSupport';
import { THREAD_ID } from 'vscode-chrome-debug-core-testsupport';
import { BreakpointsWizard as BreakpointsWizard } from './wizards/breakpoints/breakpointsWizard';
mrcrane marked this conversation as resolved.
Show resolved Hide resolved
import { IStackTraceVerifier } from './wizards/breakpoints/implementation/breakpointsAssertions';

const DATA_ROOT = testSetup.DATA_ROOT;
const SIMPLE_PROJECT_ROOT = path.join(DATA_ROOT, 'stackTrace');
const TEST_SPEC = new TestProjectSpec( { projectRoot: SIMPLE_PROJECT_ROOT } );
const TEST_SPEC = new TestProjectSpec( { projectRoot: SIMPLE_PROJECT_ROOT, projectSrc: SIMPLE_PROJECT_ROOT } );
const TEST_URL = new URL(TEST_SPEC.props.url);

interface ExpectedSource {
fileRelativePath?: string;
urlRelativePath?: string;
evalCode?: boolean;
}

interface ExpectedFrame {
name: string;
name?: string;
nameRegExp?: RegExp;
line?: number;
column?: number;
source?: ExpectedSource;
Expand Down Expand Up @@ -55,37 +58,50 @@ function assertSourceMatches(actual: DebugProtocol.Source | undefined, expected:
url.pathname = expected.urlRelativePath;
expectedName = url.host;
expectedPath = url.toString();
} else if (expected.evalCode === true) {
// Eval code has source that looks like 'VM123'. Check it by regex instead.
expect(actual.name).to.match(/.*VM.*/, `Frame ${index} source name`);
expect(actual.path).to.match(/.*VM.*/, `Frame ${index} source path`);
return;
} else {
assert.fail('Not enough information for expected source: set either "fileRelativePath" or "urlRelativePath"');
assert.fail('Not enough information for expected source: set either "fileRelativePath" or "urlRelativePath" or "eval"');
return;
}

assert.equal(actual.name, expectedName, `Source name for frame ${index} does not match`);
assert.equal(actual.path, expectedPath, `Source path for frame ${index} does not match`);
expect(actual.name).to.equal(expectedName, `Frame ${index} source name`);
expect(actual.path).to.equal(expectedPath, `Frame ${index} source path`);
}

function assertFrameMatches(actual: DebugProtocol.StackFrame, expected: ExpectedFrame, index: number) {
assert.equal(actual.name, expected.name, `Name for frame ${index} does not match`);
assert.equal(actual.line, expected.line, `Line number for frame ${index} does not match`);
assert.equal(actual.column, expected.column, `Column number for frame ${index} does not match`);
if (expected.name) {
mrcrane marked this conversation as resolved.
Show resolved Hide resolved
expect(actual.name).to.equal(expected.name, `Frame ${index} name`);
} else if (expected.nameRegExp) {
expect(actual.name).to.match(expected.nameRegExp, `Frame ${index} name`);
} else {
assert.fail('Not enough information for frame name: set either "name" or "nameRegExp"');
}

expect(actual.line).to.equal(expected.line, `Frame ${index} line`);
expect(actual.column).to.equal(expected.column, `Frame ${index} column`);

// Normal V1 stack frames will have no presentationHint, normal V2 stack frames will have presentationHint 'normal'
if (testSetup.isThisV1 && expected.presentationHint === 'normal') {
assert.equal(actual.presentationHint, undefined);
// tslint:disable-next-line:no-unused-expression
expect(actual.presentationHint, `Frame ${index} presentationHint`).to.be.undefined;
} else {
assert.equal(actual.presentationHint, expected.presentationHint, `Presentation hint for frame ${index} does not match`);
expect(actual.presentationHint).to.equal(expected.presentationHint, `Frame ${index} presentationHint`);
}

assertSourceMatches(actual.source, expected.source, index);
}

function assertResponseMatches(actual: DebugProtocol.StackTraceResponse, expectedFrames: ExpectedFrame[]) {
// Check totalFrames property
assert.equal(actual.body.totalFrames, expectedFrames.length, 'Property "totalFrames" does not match number of expected frames');
expect(actual.body.totalFrames).to.equal(expectedFrames.length, 'body.totalFrames');

// Check array length
const actualFrames = actual.body.stackFrames;
assert.equal(actualFrames.length, expectedFrames.length, 'Number of stack frames in array does not match');
expect(actualFrames.length).to.equal(expectedFrames.length, 'Number of stack frames');

// Check each frame
actualFrames.forEach((actualFrame, i) => {
Expand All @@ -98,36 +114,35 @@ interface StackTraceValidationConfig {
page: puppeteer.Page;
breakPointLabel: string;
buttonIdToClick: string;
args: DebugProtocol.StackTraceArguments;
format?: DebugProtocol.StackFrameFormat;
expectedFranes: ExpectedFrame[];
}

async function validateStackTrace(config: StackTraceValidationConfig): Promise<void> {
// Find the breakpoint location for this test
const location = config.suiteContext.breakpointLabels.get(config.breakPointLabel);

// Set the breakpoint, click the button, and wait for the breakpoint to hit
const incBtn = await config.page.waitForSelector(config.buttonIdToClick);
await setBreakpoint(config.suiteContext.debugClient, location);
const clicked = incBtn.click();
await config.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);

// Get the stack trace
const stackTraceResponse = await config.suiteContext.debugClient.send('stackTrace', config.args);
const breakpoints = BreakpointsWizard.create(config.suiteContext.debugClient, TEST_SPEC);
const breakpointWizard = breakpoints.at('app.js');

// Clean up the test before assertions, in case the assertions fail
await config.suiteContext.debugClient.continueRequest();
await clicked;

// Assert the response is as expected
try {
assertResponseMatches(stackTraceResponse, config.expectedFranes);
} catch (e) {
const error: assert.AssertionError = e;
error.message += '\nActual stack trace response: \n' + JSON.stringify(stackTraceResponse, null, 2);
const setStateBreakpoint = await breakpointWizard.breakpoint({
text: "console.log('Test stack trace here')"
});

throw error;
}
const stackTraceVerifier: IStackTraceVerifier = {
format: config.format,
verify: (stackTraceResponse: DebugProtocol.StackTraceResponse) => {
try {
assertResponseMatches(stackTraceResponse, config.expectedFranes);
} catch (e) {
const error: assert.AssertionError = e;
error.message += '\nActual stack trace response: \n' + JSON.stringify(stackTraceResponse, null, 2);

throw error;
}
}
};

await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), {stackTraceVerifier: stackTraceVerifier});
}

puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
Expand All @@ -137,12 +152,12 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
args: {
threadId: THREAD_ID
},
format: undefined,
expectedFranes: [
mrcrane marked this conversation as resolved.
Show resolved Hide resolved
{ name: '(anonymous function)', line: 7, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'inner', line: 8, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(anonymous function)', line: 11, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'evalCallback', line: 12, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(eval code)', line: 1, column: 1, source: { evalCode: true }, presentationHint: 'normal'},
{ name: 'timeoutCallback', line: 6, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '[ setTimeout ]', presentationHint: 'label'},
{ name: 'buttonClick', line: 2, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'onclick', line: 7, column: 49, source: { urlRelativePath: '/' }, presentationHint: 'normal'},
Expand All @@ -156,15 +171,14 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
args: {
threadId: THREAD_ID,
format: {
module: true
}
format: {
module: true
},
expectedFranes: [
{ name: '(anonymous function) [app.js]', line: 7, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'inner [app.js]', line: 8, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(anonymous function) [app.js]', line: 11, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'evalCallback [app.js]', line: 12, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ nameRegExp: /\(eval code\) \[.*VM.*]/, line: 1, column: 1, source: { evalCode: true }, presentationHint: 'normal'},
{ name: 'timeoutCallback [app.js]', line: 6, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '[ setTimeout ]', presentationHint: 'label'},
{ name: 'buttonClick [app.js]', line: 2, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: `onclick [${TEST_URL.host}]`, line: 7, column: 49, source: { urlRelativePath: '/' }, presentationHint: 'normal'},
Expand All @@ -178,15 +192,14 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
args: {
threadId: THREAD_ID,
format: {
line: true,
}
format: {
line: true,
},
expectedFranes: [
{ name: '(anonymous function) Line 7', line: 7, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'inner Line 8', line: 8, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(anonymous function) Line 11', line: 11, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'evalCallback Line 12', line: 12, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(eval code) Line 1', line: 1, column: 1, source: { evalCode: true }, presentationHint: 'normal'},
{ name: 'timeoutCallback Line 6', line: 6, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '[ setTimeout ]', presentationHint: 'label'},
{ name: 'buttonClick Line 2', line: 2, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'onclick Line 7', line: 7, column: 49, source: { urlRelativePath: '/' }, presentationHint: 'normal'},
Expand All @@ -200,19 +213,18 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
args: {
threadId: THREAD_ID,
format: {
parameters: true,
parameterTypes: true,
parameterNames: true,
line: true,
module: true
}
format: {
parameters: true,
parameterTypes: true,
parameterNames: true,
line: true,
module: true
},
expectedFranes: [
{ name: '(anonymous function) [app.js] Line 7', line: 7, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'inner [app.js] Line 8', line: 8, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '(anonymous function) [app.js] Line 11', line: 11, column: 9, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: 'evalCallback [app.js] Line 12', line: 12, column: 7, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ nameRegExp: /\(eval code\) \[.*VM.*] Line 1/, line: 1, column: 1, source: { evalCode: true }, presentationHint: 'normal'},
{ name: 'timeoutCallback [app.js] Line 6', line: 6, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: '[ setTimeout ]', presentationHint: 'label'},
{ name: 'buttonClick [app.js] Line 2', line: 2, column: 5, source: { fileRelativePath: 'app.js' }, presentationHint: 'normal'},
{ name: `onclick [${TEST_URL.host}] Line 7`, line: 7, column: 49, source: { urlRelativePath: '/' }, presentationHint: 'normal'},
Expand Down
10 changes: 10 additions & 0 deletions test/int/wizards/breakpoints/fileBreakpointsWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export interface IHitCountBreakpointOptions extends IBreakpointOptions {
export class FileBreakpointsWizard {
public constructor(private readonly _internal: InternalFileBreakpointsWizard) { }

public async breakpoint(options: IBreakpointOptions): Promise<BreakpointWizard> {
const wrappedBreakpoint = wrapWithMethodLogger(await this._internal.breakpoint({
text: options.text,
boundText: options.boundText,
name: `BP @ ${options.text}`
}));

return wrappedBreakpoint.setThenWaitForVerifiedThenValidate();
}

public async hitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
return (await (await this.unsetHitCountBreakpoint(options)).setThenWaitForVerifiedThenValidate());
}
Expand Down
Loading