diff --git a/test/int/stackTrace.test.ts b/test/int/stackTrace.test.ts index f3559db8..354e7979 100644 --- a/test/int/stackTrace.test.ts +++ b/test/int/stackTrace.test.ts @@ -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; @@ -125,21 +32,7 @@ async function validateStackTrace(config: StackTraceValidationConfig): Promise { - 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) => { @@ -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'}, diff --git a/test/int/wizards/breakpoints/breakpointsWizard.ts b/test/int/wizards/breakpoints/breakpointsWizard.ts index bcd9c3e0..932c2d33 100644 --- a/test/int/wizards/breakpoints/breakpointsWizard.ts +++ b/test/int/wizards/breakpoints/breakpointsWizard.ts @@ -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}`); } diff --git a/test/int/wizards/breakpoints/implementation/breakpointsAssertions.ts b/test/int/wizards/breakpoints/implementation/breakpointsAssertions.ts index 2608b51f..edbdea5d 100644 --- a/test/int/wizards/breakpoints/implementation/breakpointsAssertions.ts +++ b/test/int/wizards/breakpoints/implementation/breakpointsAssertions.ts @@ -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'; @@ -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 { @@ -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 @@ -94,19 +64,9 @@ export class BreakpointsAssertions { public async assertIsHitThenResume(breakpoint: BreakpointWizard, verifications: IVerifications): Promise { 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 }); @@ -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) { @@ -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; diff --git a/test/int/wizards/breakpoints/implementation/stackTraceObjectAssertions.ts b/test/int/wizards/breakpoints/implementation/stackTraceObjectAssertions.ts new file mode 100644 index 00000000..9d2983a8 --- /dev/null +++ b/test/int/wizards/breakpoints/implementation/stackTraceObjectAssertions.ts @@ -0,0 +1,119 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as testSetup from '../../../testSetup'; +import { expect } from 'chai'; +import { URL } from 'url'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { BreakpointsWizard } from '../breakpointsWizard'; + +export interface ExpectedSource { + fileRelativePath?: string; + urlRelativePath?: string; + evalCode?: boolean; +} + +export interface ExpectedFrame { + name: string | RegExp; + line?: number; + column?: number; + source?: ExpectedSource; + presentationHint?: string; +} + +export class StackTraceObjectAssertions { + private readonly _projectRoot: string; + private readonly _projectURL: URL; + + public constructor(breakpointsWizard: BreakpointsWizard) { + this._projectRoot = breakpointsWizard.project.props.projectRoot; + this._projectURL = new URL(breakpointsWizard.project.props.url); + } + + private 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(this._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(this._projectURL.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`); + } + + private 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`); + } + + this.assertSourceMatches(actual.source, expected.source, index); + } + + private assertResponseMatchesFrames(actualResponse: DebugProtocol.StackTraceResponse, expectedFrames: ExpectedFrame[]) { + // Check totalFrames property + expect(actualResponse.body.totalFrames).to.equal(expectedFrames.length, 'body.totalFrames'); + + // Check array length + const actualFrames = actualResponse.body.stackFrames; + expect(actualFrames.length).to.equal(expectedFrames.length, 'Number of stack frames'); + + // Check each frame + actualFrames.forEach((actualFrame, i) => { + this.assertFrameMatches(actualFrame, expectedFrames[i], i); + }); + } + + public assertResponseMatches(actualResponse: DebugProtocol.StackTraceResponse, expectedFrames: ExpectedFrame[]) { + try { + this.assertResponseMatchesFrames(actualResponse, expectedFrames); + } catch (e) { + const error: assert.AssertionError = e; + error.message += '\nActual stack trace response: \n' + JSON.stringify(actualResponse, null, 2); + + throw error; + } + } + } \ No newline at end of file diff --git a/test/int/wizards/breakpoints/implementation/stackTraceStringAssertions.ts b/test/int/wizards/breakpoints/implementation/stackTraceStringAssertions.ts new file mode 100644 index 00000000..41e9cfb1 --- /dev/null +++ b/test/int/wizards/breakpoints/implementation/stackTraceStringAssertions.ts @@ -0,0 +1,49 @@ +import * as assert from 'assert'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { expect } from 'chai'; +import { findLineNumber } from '../../../utils/findPositionOfTextInFile'; +import { BreakpointWizard } from '../breakpointWizard'; + +export class StackTraceStringAssertions { + public constructor( + private readonly _breakpoint: BreakpointWizard) { } + + public assertResponseMatches(actualResponse: DebugProtocol.StackTraceResponse, expectedString: string) { + expect(actualResponse.success, `Expected the response to the stack trace request to be succesful yet it failed: ${JSON.stringify(actualResponse)}`).to.equal(true); + + const stackTraceFrames = actualResponse.body.stackFrames; + expect(actualResponse.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 = expectedString.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 ${this._breakpoint} to be:\n${formattedExpectedStackTrace}\nyet it is:\n${actualStackTrace}`); + } + + private 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})` : ''}`; + } +} \ No newline at end of file