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

Commit

Permalink
Refactor stack trace assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcrane committed May 22, 2019
1 parent 0e3916e commit dd23fe9
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 177 deletions.
113 changes: 3 additions & 110 deletions test/int/stackTrace.test.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,18 @@
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 { BreakpointsWizard } from './wizards/breakpoints/breakpointsWizard';
import { IStackTraceVerifier } from './wizards/breakpoints/implementation/breakpointsAssertions';
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, 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 | RegExp;
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 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" or "eval"');
return;
}

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) {
if (typeof expected.name === 'string') {
expect(actual.name).to.equal(expected.name, `Frame ${index} name`);
} else if (expected.name instanceof RegExp) {
expect(actual.name).to.match(expected.name, `Frame ${index} name`);
}

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') {
// tslint:disable-next-line:no-unused-expression
expect(actual.presentationHint, `Frame ${index} presentationHint`).to.be.undefined;
} else {
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
expect(actual.body.totalFrames).to.equal(expectedFrames.length, 'body.totalFrames');

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

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

interface StackTraceValidationConfig {
suiteContext: FrameworkTestContext;
page: puppeteer.Page;
Expand All @@ -125,21 +32,7 @@ async function validateStackTrace(config: StackTraceValidationConfig): Promise<v
text: "console.log('Test stack trace here')"
});

const stackTraceVerifier: IStackTraceVerifier = {
format: config.format,
verify: (stackTraceResponse: DebugProtocol.StackTraceResponse) => {
try {
assertResponseMatches(stackTraceResponse, config.expectedFrames);
} 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(), {stackTrace: stackTraceVerifier});
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), {stackTrace: config.expectedFrames, stackFrameFormat: config.format});
}

puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
Expand All @@ -149,7 +42,7 @@ puppeteerSuite('Stack Traces', TEST_SPEC, (suiteContext) => {
page: page,
breakPointLabel: 'stackTraceBreakpoint',
buttonIdToClick: '#button',
format: undefined,
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'},
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as assert from 'assert';
import * as path from 'path';
import { expect, use } from 'chai';
import * as chaiString from 'chai-string';
Expand All @@ -8,20 +7,16 @@ import { BreakpointWizard } from '../breakpointWizard';
import { InternalFileBreakpointsWizard, CurrentBreakpointsMapping } from './internalFileBreakpointsWizard';
import { BreakpointsWizard } from '../breakpointsWizard';
import { waitUntilReadyWithTimeout } from '../../../utils/waitUntilReadyWithTimeout';
import { findLineNumber } from '../../../utils/findPositionOfTextInFile';
import { IExpectedVariables, VariablesAssertions } from './variablesAssertions';
import { ExpectedFrame, StackTraceObjectAssertions } from './stackTraceObjectAssertions';
import { StackTraceStringAssertions } from './stackTraceStringAssertions';

use(chaiString);

export type IExpectedStackTrace = string;
export interface IStackTraceVerifier {
format?: DebugProtocol.StackFrameFormat;
verify: (response: DebugProtocol.StackTraceResponse) => void;
}

export interface IVerifications {
variables?: IExpectedVariables;
stackTrace?: IExpectedStackTrace | IStackTraceVerifier;
stackTrace?: string | ExpectedFrame[];
stackFrameFormat?: DebugProtocol.StackFrameFormat;
}

interface IObjectWithLocation {
Expand All @@ -46,31 +41,6 @@ export class BreakpointsAssertions {
private readonly _internal: InternalFileBreakpointsWizard,
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping) { }

private getDefaultStackTraceVerifier(breakpoint: BreakpointWizard, expectedStackTrace: string): IStackTraceVerifier {
return {
format: this._defaultStackFrameFormat,
verify: (stackTraceResponse) => {
expect(stackTraceResponse.success, `Expected the response to the stack trace request to be succesful yet it failed: ${JSON.stringify(stackTraceResponse)}`).to.equal(true);

const stackTraceFrames = stackTraceResponse.body.stackFrames;
expect(stackTraceResponse.body.totalFrames, `The number of stackFrames was different than the value supplied on the totalFrames field`)
.to.equal(stackTraceFrames.length);
stackTraceFrames.forEach(frame => {
// Warning: We don't currently validate frame.source.path
expect(frame.source).not.to.equal(undefined);
const expectedSourceNameAndLine = ` [${frame.source!.name}] Line ${frame.line}`;
expect(frame.name, 'Expected the formatted name to match the source name and line supplied as individual attributes').to.endsWith(expectedSourceNameAndLine);
});


const formattedExpectedStackTrace = expectedStackTrace.replace(/^\s+/gm, ''); // Remove the white space we put at the start of the lines to make the stack trace align with the code
this.applyIgnores(formattedExpectedStackTrace, stackTraceFrames);
const actualStackTrace = this.extractStackTrace(stackTraceFrames);
assert.equal(actualStackTrace, formattedExpectedStackTrace, `Expected the stack trace when hitting ${breakpoint} to be:\n${formattedExpectedStackTrace}\nyet it is:\n${actualStackTrace}`);
}
};
}

public assertIsVerified(breakpoint: BreakpointWizard): void {
// Convert to one based to match the VS Code potocol and what VS Code does if you try to open that file at that line number

Expand All @@ -94,19 +64,9 @@ export class BreakpointsAssertions {
public async assertIsHitThenResume(breakpoint: BreakpointWizard, verifications: IVerifications): Promise<void> {
await this._breakpointsWizard.waitUntilPaused(breakpoint);

let stackTraceVerifier: IStackTraceVerifier | undefined = undefined;
if (typeof verifications.stackTrace === 'string') {
stackTraceVerifier = this.getDefaultStackTraceVerifier(breakpoint, verifications.stackTrace);
} else if (typeof verifications.stackTrace === 'object') {
stackTraceVerifier = verifications.stackTrace;
}

let stackFrameFormat: DebugProtocol.StackFrameFormat | undefined = undefined;
if (stackTraceVerifier) {
stackFrameFormat = stackTraceVerifier.format;
}
const stackFrameFormat = verifications.stackFrameFormat || this._defaultStackFrameFormat;

const stackTraceResponse = await await this._internal.client.send('stackTrace', {
const stackTraceResponse = await this._internal.client.send('stackTrace', {
threadId: THREAD_ID,
format: stackFrameFormat
});
Expand All @@ -115,8 +75,12 @@ export class BreakpointsAssertions {
// Validate that the topFrame is locate in the same place as the breakpoint
this.assertLocationMatchesExpected(topFrame, breakpoint);

if (stackTraceVerifier !== undefined) {
stackTraceVerifier.verify(stackTraceResponse);
if (typeof verifications.stackTrace === 'string') {
const assertions = new StackTraceStringAssertions(breakpoint);
assertions.assertResponseMatches(stackTraceResponse, verifications.stackTrace);
} else if (typeof verifications.stackTrace === 'object') {
const assertions = new StackTraceObjectAssertions(this._breakpointsWizard);
assertions.assertResponseMatches(stackTraceResponse, verifications.stackTrace);
}

if (verifications.variables !== undefined) {
Expand All @@ -126,25 +90,6 @@ export class BreakpointsAssertions {
await this._breakpointsWizard.resume();
}

public applyIgnores(formattedExpectedStackTrace: string, stackTrace: DebugProtocol.StackFrame[]): void {
const ignoreFunctionNameText = '<__IGNORE_FUNCTION_NAME__>';
const ignoreFunctionName = findLineNumber(formattedExpectedStackTrace, formattedExpectedStackTrace.indexOf(ignoreFunctionNameText));
if (ignoreFunctionName >= 0) {
expect(stackTrace.length).to.be.greaterThan(ignoreFunctionName);
const ignoredFrame = stackTrace[ignoreFunctionName];
ignoredFrame.name = `${ignoreFunctionNameText} [${ignoredFrame.source!.name}] Line ${ignoredFrame.line}`;
}
}

private extractStackTrace(stackTrace: DebugProtocol.StackFrame[]): string {
return stackTrace.map(f => this.printStackTraceFrame(f)).join('\n');
}

private printStackTraceFrame(frame: DebugProtocol.StackFrame): string {
let frameName = frame.name;
return `${frameName}:${frame.column}${frame.presentationHint && frame.presentationHint !== 'normal' ? ` (${frame.presentationHint})` : ''}`;
}

private assertLocationMatchesExpected(objectWithLocation: IObjectWithLocation, breakpoint: BreakpointWizard): void {
const expectedFilePath = this._internal.filePath;

Expand Down
Loading

0 comments on commit dd23fe9

Please sign in to comment.