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 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
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.

182 changes: 42 additions & 140 deletions test/int/stackTrace.test.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,38 @@
import * as assert from 'assert';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import * as testSetup from './testSetup';
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 } from './wizards/breakpoints/breakpointsWizard';
import { ExpectedFrame } from './wizards/breakpoints/implementation/stackTraceObjectAssertions';

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;
}

interface ExpectedFrame {
name: string;
line?: number;
column?: number;
source?: ExpectedSource;
presentationHint?: string;
}

function assertSourceMatches(actual: DebugProtocol.Source | undefined, expected: ExpectedSource | undefined, index: number) {
if (actual == null && expected == null) {
return;
}

if (expected == null) {
assert.fail(`Source was returned for frame ${index} but none was expected`);
return;
}

if (actual == null) {
assert.fail(`Source was expected for frame ${index} but none was returned`);
return;
}

let expectedName: string;
let expectedPath: string;

if (expected.fileRelativePath) {
// Generate the expected path from the relative path and the project root
expectedPath = path.join(TEST_SPEC.props.projectRoot, expected.fileRelativePath);
expectedName = path.parse(expectedPath).base;
} else if (expected.urlRelativePath) {
// Generate the expected path from the relative path and the project url
const url = new URL(TEST_URL.toString()); // Clone URL so we can update it
url.pathname = expected.urlRelativePath;
expectedName = url.host;
expectedPath = url.toString();
} else {
assert.fail('Not enough information for expected source: set either "fileRelativePath" or "urlRelativePath"');
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`);
}

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`);

// 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);
} else {
assert.equal(actual.presentationHint, expected.presentationHint, `Presentation hint for frame ${index} does not match`);
}

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');

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

// Check each frame
actualFrames.forEach((actualFrame, i) => {
assertFrameMatches(actualFrame, expectedFrames[i], i);
});
}

interface StackTraceValidationConfig {
suiteContext: FrameworkTestContext;
page: puppeteer.Page;
breakPointLabel: string;
buttonIdToClick: string;
args: DebugProtocol.StackTraceArguments;
expectedFranes: ExpectedFrame[];
format?: DebugProtocol.StackFrameFormat;
expectedFrames: 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;
}
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), {stackTrace: config.expectedFrames, stackFrameFormat: config.format});
}

puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
Expand All @@ -137,12 +42,12 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
args: {
threadId: THREAD_ID
},
expectedFranes: [
{ 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'},
format: {},
expectedFrames: [
{ 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 +61,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'},
expectedFrames: [
{ 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'},
{ name: /\(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 +82,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'},
expectedFrames: [
{ 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 +103,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'},
expectedFrames: [
{ 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'},
{ name: /\(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
4 changes: 4 additions & 0 deletions test/int/wizards/breakpoints/breakpointsWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export class BreakpointsWizard {
this._client.on('breakpoint', breakpointStatusChange => this.onBreakpointStatusChange(breakpointStatusChange.body));
}

public get project() {
return this._project;
}

private logState() {
logger.log(`BreakpointsWizard #events = ${this._eventsToBeConsumed.length}, state = ${this.state}`);
}
Expand Down
15 changes: 13 additions & 2 deletions test/int/wizards/breakpoints/fileBreakpointsWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BreakpointWizard } from './breakpointWizard';
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
import { PromiseOrNot } from 'vscode-chrome-debug-core';
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
import { PauseOnHitCount } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';

export interface IBreakpointOptions {
text: string;
Expand All @@ -15,15 +16,25 @@ 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());
}

public async unsetHitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
return wrapWithMethodLogger(await this._internal.hitCountBreakpoint({
return wrapWithMethodLogger(await this._internal.breakpoint({
text: options.text,
boundText: options.boundText,
hitCountCondition: options.hitCountCondition,
actionWhenHit: new PauseOnHitCount(options.hitCountCondition),
name: `BP @ ${options.text}`
}));
}
Expand Down
Loading