From 4ea3227cf1ca2670242c26f736a2bfb7a5653f5d Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 12 Nov 2022 16:17:49 +0000 Subject: [PATCH] alternative stack trace approach (#2119) * reimpl getDefinitionLineAndUri * disable current stack trace filtering * filter stack traces without modifying error * rename file * rename file * add advice about source maps * WIP feature for transpiled stack traces * improve feature for transpiled stack traces * skip source-mapping scenarios when instrumenting with nyc * update CHANGELOG.md --- CHANGELOG.md | 2 + docs/transpiling.md | 11 ++++ features/stack_traces.feature | 51 ++++++++++++++++++ features/support/world.ts | 12 +++-- package-lock.json | 49 ++++++++++++----- package.json | 5 +- src/filter_stack_trace.ts | 38 +++++++++++++ src/runtime/format_error.ts | 22 ++++++++ src/runtime/index.ts | 10 +--- src/runtime/parallel/worker.ts | 10 +--- src/runtime/step_runner.ts | 6 ++- src/runtime/test_case_runner.ts | 5 ++ src/runtime/test_case_runner_spec.ts | 1 + src/stack_trace_filter.ts | 53 ------------------- .../get_definition_line_and_uri.ts | 13 ++--- 15 files changed, 187 insertions(+), 101 deletions(-) create mode 100644 features/stack_traces.feature create mode 100644 src/filter_stack_trace.ts create mode 100644 src/runtime/format_error.ts delete mode 100644 src/stack_trace_filter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c635c057..0dd8badee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +## Changed +- Handle stack traces without V8-specific modification ([#2119](https://github.com/cucumber/cucumber-js/pull/2119)) ## [8.7.0] - 2022-10-17 ### Deprecated diff --git a/docs/transpiling.md b/docs/transpiling.md index 2fdab1526..79dba1e1c 100644 --- a/docs/transpiling.md +++ b/docs/transpiling.md @@ -57,3 +57,14 @@ If you are using babel with [@babel/preset-typescript](https://babeljs.io/docs/e ### ESM See [ESM](./esm.md) for general advice on using loaders for transpilation in ESM projects. + +### Source maps + +Source maps are used to ensure accurate source references and stack traces in Cucumber's reporting, by giving traceability from a transpiled piece of code back to the original source code. + +Just-in-time transpilers like `ts-node` and `@babel/register` have sensible default configuration that emits source maps and enables them in the runtime environment, so you shouldn't have to do anything in order for source maps to work. + +If you're using step definition code that's _already_ transpiled (maybe because it's a shared library) then you'll need to: + +1. Ensure source maps are emitted by your transpiler. You can verify by checking for a comment starting with `//# sourceMappingURL=` at the end of your transpiled file(s). +2. Ensure source maps are enabled at runtime. Node.js supports this natively via [the `--enable-source-maps` flag](https://nodejs.org/docs/latest/api/cli.html#--enable-source-maps). diff --git a/features/stack_traces.feature b/features/stack_traces.feature new file mode 100644 index 000000000..1f8be6ce4 --- /dev/null +++ b/features/stack_traces.feature @@ -0,0 +1,51 @@ +@spawn +@source-mapping +Feature: Stack traces + Background: + Given a file named "features/a.feature" with: + """ + Feature: some feature + Scenario: some scenario + Given a passing step + And a failing step + """ + + Rule: Source maps are respected when dealing with transpiled support code + + Just-in-time transpilers like `@babel/register` and `ts-node` emit source maps with + the transpiled code. Cucumber users expect stack traces to point to the line and column + in the original source file when there is an error. + + Background: + Given a file named "features/steps.ts" with: + """ + import { Given } from '@cucumber/cucumber' + + interface Something { + field1: string + field2: string + } + + Given('a passing step', function() {}) + + Given('a failing step', function() { + throw new Error('boom') + }) + """ + + Scenario: commonjs + When I run cucumber-js with `--require-module ts-node/register --require features/steps.ts` + Then the output contains the text: + """ + /features/steps.ts:11:9 + """ + And it fails + + Scenario: esm + Given my env includes "{\"NODE_OPTIONS\":\"--loader ts-node/esm\"}" + When I run cucumber-js with `--import features/steps.ts` + Then the output contains the text: + """ + /features/steps.ts:11:9 + """ + And it fails diff --git a/features/support/world.ts b/features/support/world.ts index e60dbe46f..38c445560 100644 --- a/features/support/world.ts +++ b/features/support/world.ts @@ -41,10 +41,14 @@ export class World { parseEnvString(str: string): NodeJS.ProcessEnv { const result: NodeJS.ProcessEnv = {} if (doesHaveValue(str)) { - str - .split(/\s+/) - .map((keyValue) => keyValue.split('=')) - .forEach((pair) => (result[pair[0]] = pair[1])) + try { + Object.assign(result, JSON.parse(str)) + } catch { + str + .split(/\s+/) + .map((keyValue) => keyValue.split('=')) + .forEach((pair) => (result[pair[0]] = pair[1])) + } } return result } diff --git a/package-lock.json b/package-lock.json index d814a0ceb..389234e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "8.7.0", "license": "MIT", "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", "@cucumber/ci-environment": "9.1.0", "@cucumber/cucumber-expressions": "16.0.0", "@cucumber/gherkin": "24.1.0", @@ -27,6 +26,7 @@ "debug": "^4.3.4", "duration": "^0.2.2", "durations": "^3.4.2", + "error-stack-parser": "^2.1.4", "figures": "^3.2.0", "glob": "^7.1.6", "has-ansi": "^4.0.1", @@ -40,7 +40,6 @@ "progress": "^2.0.3", "resolve-pkg": "^2.0.0", "semver": "7.3.8", - "stack-chain": "^2.0.0", "string-argv": "^0.3.1", "strip-ansi": "6.0.1", "supports-color": "^8.1.1", @@ -528,6 +527,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.0.tgz", "integrity": "sha512-pOQRG+w/T1KogjiuO4uqqa+dw/IIb8kDY0ctYfiJstWv7TOTmtuAkx8ZB4YgauDNn2huHR33oruOgi45VcatOg==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -826,6 +826,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz", "integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -833,12 +834,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2901,6 +2904,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -7007,10 +7018,10 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "node_modules/stack-chain": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", - "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==" + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "node_modules/statuses": { "version": "2.0.1", @@ -8197,6 +8208,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.0.tgz", "integrity": "sha512-pOQRG+w/T1KogjiuO4uqqa+dw/IIb8kDY0ctYfiJstWv7TOTmtuAkx8ZB4YgauDNn2huHR33oruOgi45VcatOg==", + "dev": true, "requires": { "@jridgewell/trace-mapping": "0.3.9" } @@ -8430,17 +8442,20 @@ "@jridgewell/resolve-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz", - "integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==" + "integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==", + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -10105,6 +10120,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -13184,10 +13207,10 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "stack-chain": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", - "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==" + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "statuses": { "version": "2.0.1", diff --git a/package.json b/package.json index e350c4bd2..189dcb997 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,6 @@ "node": "12 || 14 || 16 || 17 || 18" }, "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", "@cucumber/ci-environment": "9.1.0", "@cucumber/cucumber-expressions": "16.0.0", "@cucumber/gherkin": "24.1.0", @@ -213,6 +212,7 @@ "debug": "^4.3.4", "duration": "^0.2.2", "durations": "^3.4.2", + "error-stack-parser": "^2.1.4", "figures": "^3.2.0", "glob": "^7.1.6", "has-ansi": "^4.0.1", @@ -226,7 +226,6 @@ "progress": "^2.0.3", "resolve-pkg": "^2.0.0", "semver": "7.3.8", - "stack-chain": "^2.0.0", "string-argv": "^0.3.1", "strip-ansi": "6.0.1", "supports-color": "^8.1.1", @@ -311,7 +310,7 @@ "prepublishOnly": "rm -rf lib && npm run build-local", "pretest-coverage": "npm run build-local", "pretypes-test": "npm run build-local", - "test-coverage": "nyc --silent mocha 'src/**/*_spec.ts' 'compatibility/**/*_spec.ts' && nyc --silent --no-clean node bin/cucumber.js && nyc report --reporter=lcov", + "test-coverage": "nyc --silent mocha 'src/**/*_spec.ts' 'compatibility/**/*_spec.ts' && nyc --silent --no-clean node bin/cucumber.js --tags \"not @source-mapping\" && nyc report --reporter=lcov", "test": "npm run lint && npm run types-test && npm run unit-test && npm run cck-test && npm run feature-test", "types-test": "tsd", "unit-test": "mocha 'src/**/*_spec.ts'", diff --git a/src/filter_stack_trace.ts b/src/filter_stack_trace.ts new file mode 100644 index 000000000..dae7e1006 --- /dev/null +++ b/src/filter_stack_trace.ts @@ -0,0 +1,38 @@ +import path from 'path' +import { valueOrDefault } from './value_checker' +import { StackFrame } from 'error-stack-parser' + +const projectRootPath = path.join(__dirname, '..') +const projectChildDirs = ['src', 'lib', 'node_modules'] + +export function isFileNameInCucumber(fileName: string): boolean { + return projectChildDirs.some((dir) => + fileName.startsWith(path.join(projectRootPath, dir)) + ) +} + +export function filterStackTrace(frames: StackFrame[]): StackFrame[] { + if (isErrorInCucumber(frames)) { + return frames + } + const index = frames.findIndex((x) => isFrameInCucumber(x)) + if (index === -1) { + return frames + } + return frames.slice(0, index) +} + +function isErrorInCucumber(frames: StackFrame[]): boolean { + const filteredFrames = frames.filter((x) => !isFrameInNode(x)) + return filteredFrames.length > 0 && isFrameInCucumber(filteredFrames[0]) +} + +function isFrameInCucumber(frame: StackFrame): boolean { + const fileName = valueOrDefault(frame.getFileName(), '') + return isFileNameInCucumber(fileName) +} + +function isFrameInNode(frame: StackFrame): boolean { + const fileName = valueOrDefault(frame.getFileName(), '') + return !fileName.includes(path.sep) +} diff --git a/src/runtime/format_error.ts b/src/runtime/format_error.ts new file mode 100644 index 000000000..2b803837a --- /dev/null +++ b/src/runtime/format_error.ts @@ -0,0 +1,22 @@ +import { format } from 'assertion-error-formatter' +import errorStackParser from 'error-stack-parser' +import { filterStackTrace } from '../filter_stack_trace' + +export function formatError(error: Error, filterStackTraces: boolean): string { + let filteredStack: string + if (filterStackTraces) { + try { + filteredStack = filterStackTrace(errorStackParser.parse(error)) + .map((f) => f.source) + .join('\n') + } catch { + // if we weren't able to parse and filter, we'll settle for the original + } + } + return format(error, { + colorFns: { + errorStack: (stack: string) => + filteredStack ? `\n${filteredStack}` : stack, + }, + }) +} diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 64d33e2f5..86abd0c1c 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -2,7 +2,6 @@ import * as messages from '@cucumber/messages' import { IdGenerator } from '@cucumber/messages' import { EventEmitter } from 'events' import { EventDataCollector } from '../formatter/helpers' -import StackTraceFilter from '../stack_trace_filter' import { ISupportCodeLibrary } from '../support_code_library_builder/types' import { assembleTestCases } from './assemble_test_cases' import { retriesForPickle, shouldCauseFailure } from './helpers' @@ -40,7 +39,6 @@ export default class Runtime implements IRuntime { private readonly newId: IdGenerator.NewId private readonly options: IRuntimeOptions private readonly pickleIds: string[] - private readonly stackTraceFilter: StackTraceFilter private readonly supportCodeLibrary: ISupportCodeLibrary private success: boolean private runTestRunHooks: RunsTestRunHooks @@ -59,7 +57,6 @@ export default class Runtime implements IRuntime { this.newId = newId this.options = options this.pickleIds = pickleIds - this.stackTraceFilter = new StackTraceFilter() this.supportCodeLibrary = supportCodeLibrary this.success = true this.runTestRunHooks = makeRunTestRunHooks( @@ -85,6 +82,7 @@ export default class Runtime implements IRuntime { testCase, retries, skip, + filterStackTraces: this.options.filterStacktraces, supportCodeLibrary: this.supportCodeLibrary, worldParameters: this.options.worldParameters, }) @@ -95,9 +93,6 @@ export default class Runtime implements IRuntime { } async start(): Promise { - if (this.options.filterStacktraces) { - this.stackTraceFilter.filter() - } const testRunStarted: messages.Envelope = { testRunStarted: { timestamp: this.stopwatch.timestamp(), @@ -132,9 +127,6 @@ export default class Runtime implements IRuntime { }, } this.eventBroadcaster.emit('envelope', testRunFinished) - if (this.options.filterStacktraces) { - this.stackTraceFilter.unfilter() - } return this.success } } diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index a6e37f5a1..11405ccb7 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -3,7 +3,6 @@ import { IdGenerator } from '@cucumber/messages' import { duration } from 'durations' import { EventEmitter } from 'events' import { pathToFileURL } from 'url' -import StackTraceFilter from '../../stack_trace_filter' import supportCodeLibraryBuilder from '../../support_code_library_builder' import { ISupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' @@ -33,7 +32,6 @@ export default class Worker { private filterStacktraces: boolean private readonly newId: IdGenerator.NewId private readonly sendMessage: IMessageSender - private readonly stackTraceFilter: StackTraceFilter private supportCodeLibrary: ISupportCodeLibrary private worldParameters: any private runTestRunHooks: RunsTestRunHooks @@ -55,7 +53,6 @@ export default class Worker { this.exit = exit this.sendMessage = sendMessage this.eventBroadcaster = new EventEmitter() - this.stackTraceFilter = new StackTraceFilter() this.eventBroadcaster.on('envelope', (envelope: messages.Envelope) => { this.sendMessage({ jsonEnvelope: JSON.stringify(envelope), @@ -81,9 +78,6 @@ export default class Worker { this.worldParameters = options.worldParameters this.filterStacktraces = filterStacktraces - if (this.filterStacktraces) { - this.stackTraceFilter.filter() - } this.runTestRunHooks = makeRunTestRunHooks( options.dryRun, this.supportCodeLibrary.defaultTimeout, @@ -102,9 +96,6 @@ export default class Worker { this.supportCodeLibrary.afterTestRunHookDefinitions, 'an AfterAll' ) - if (this.filterStacktraces) { - this.stackTraceFilter.unfilter() - } this.exit(0) } @@ -137,6 +128,7 @@ export default class Worker { testCase, retries, skip, + filterStackTraces: this.filterStacktraces, supportCodeLibrary: this.supportCodeLibrary, worldParameters: this.worldParameters, }) diff --git a/src/runtime/step_runner.ts b/src/runtime/step_runner.ts index 14e765bfd..8e59b6460 100644 --- a/src/runtime/step_runner.ts +++ b/src/runtime/step_runner.ts @@ -1,7 +1,6 @@ import Time from '../time' import UserCodeRunner from '../user_code_runner' import * as messages from '@cucumber/messages' -import { format } from 'assertion-error-formatter' import { ITestCaseHookParameter } from '../support_code_library_builder/types' import { IDefinition, IGetInvocationDataResponse } from '../models/definition' import { @@ -9,11 +8,13 @@ import { doesNotHaveValue, valueOrDefault, } from '../value_checker' +import { formatError } from './format_error' const { beginTiming, endTiming } = Time export interface IRunOptions { defaultTimeout: number + filterStackTraces: boolean hookParameter: ITestCaseHookParameter step: messages.PickleStep stepDefinition: IDefinition @@ -22,6 +23,7 @@ export interface IRunOptions { export async function run({ defaultTimeout, + filterStackTraces, hookParameter, step, stepDefinition, @@ -68,7 +70,7 @@ export async function run({ } else if (result === 'pending') { status = messages.TestStepResultStatus.PENDING } else if (doesHaveValue(error)) { - message = format(error) + message = formatError(error, filterStackTraces) status = messages.TestStepResultStatus.FAILED } else { status = messages.TestStepResultStatus.PASSED diff --git a/src/runtime/test_case_runner.ts b/src/runtime/test_case_runner.ts index 4a00f06d8..bace451bf 100644 --- a/src/runtime/test_case_runner.ts +++ b/src/runtime/test_case_runner.ts @@ -25,6 +25,7 @@ export interface INewTestCaseRunnerOptions { testCase: messages.TestCase retries: number skip: boolean + filterStackTraces: boolean supportCodeLibrary: ISupportCodeLibrary worldParameters: any } @@ -41,6 +42,7 @@ export default class TestCaseRunner { private readonly testCase: messages.TestCase private readonly maxAttempts: number private readonly skip: boolean + private readonly filterStackTraces: boolean private readonly supportCodeLibrary: ISupportCodeLibrary private testStepResults: messages.TestStepResult[] private world: any @@ -55,6 +57,7 @@ export default class TestCaseRunner { testCase, retries = 0, skip, + filterStackTraces, supportCodeLibrary, worldParameters, }: INewTestCaseRunnerOptions) { @@ -83,6 +86,7 @@ export default class TestCaseRunner { this.pickle = pickle this.testCase = testCase this.skip = skip + this.filterStackTraces = filterStackTraces this.supportCodeLibrary = supportCodeLibrary this.worldParameters = worldParameters this.resetTestProgressData() @@ -129,6 +133,7 @@ export default class TestCaseRunner { ): Promise { return await StepRunner.run({ defaultTimeout: this.supportCodeLibrary.defaultTimeout, + filterStackTraces: this.filterStackTraces, hookParameter, step, stepDefinition, diff --git a/src/runtime/test_case_runner_spec.ts b/src/runtime/test_case_runner_spec.ts index 0447ea605..e571853c3 100644 --- a/src/runtime/test_case_runner_spec.ts +++ b/src/runtime/test_case_runner_spec.ts @@ -54,6 +54,7 @@ async function testRunner( pickle: options.pickle, testCase, retries: valueOrDefault(options.retries, 0), + filterStackTraces: false, skip: valueOrDefault(options.skip, false), supportCodeLibrary: options.supportCodeLibrary, worldParameters: {}, diff --git a/src/stack_trace_filter.ts b/src/stack_trace_filter.ts deleted file mode 100644 index 2275d4685..000000000 --- a/src/stack_trace_filter.ts +++ /dev/null @@ -1,53 +0,0 @@ -import stackChain from 'stack-chain' -import path from 'path' -import { valueOrDefault } from './value_checker' -import CallSite = NodeJS.CallSite - -const projectRootPath = path.join(__dirname, '..') -const projectChildDirs = ['src', 'lib', 'node_modules'] - -export function isFileNameInCucumber(fileName: string): boolean { - return projectChildDirs.some((dir) => - fileName.startsWith(path.join(projectRootPath, dir)) - ) -} - -export default class StackTraceFilter { - private currentFilter: CallSite[] - - filter(): void { - this.currentFilter = stackChain.filter.attach( - (_err: any, frames: CallSite[]) => { - if (this.isErrorInCucumber(frames)) { - return frames - } - const index = frames.findIndex((x) => this.isFrameInCucumber(x)) - if (index === -1) { - return frames - } - return frames.slice(0, index) - } - ) - } - - isErrorInCucumber(frames: CallSite[]): boolean { - const filteredFrames = frames.filter((x) => !this.isFrameInNode(x)) - return ( - filteredFrames.length > 0 && this.isFrameInCucumber(filteredFrames[0]) - ) - } - - isFrameInCucumber(frame: CallSite): boolean { - const fileName = valueOrDefault(frame.getFileName(), '') - return isFileNameInCucumber(fileName) - } - - isFrameInNode(frame: CallSite): boolean { - const fileName = valueOrDefault(frame.getFileName(), '') - return !fileName.includes(path.sep) - } - - unfilter(): void { - stackChain.filter.deattach(this.currentFilter) - } -} diff --git a/src/support_code_library_builder/get_definition_line_and_uri.ts b/src/support_code_library_builder/get_definition_line_and_uri.ts index 583319e6a..21a5a442e 100644 --- a/src/support_code_library_builder/get_definition_line_and_uri.ts +++ b/src/support_code_library_builder/get_definition_line_and_uri.ts @@ -1,10 +1,8 @@ import path from 'path' -import { wrapCallSite } from '@cspotcode/source-map-support' -import stackChain from 'stack-chain' -import { isFileNameInCucumber } from '../stack_trace_filter' +import errorStackParser, { StackFrame } from 'error-stack-parser' +import { isFileNameInCucumber } from '../filter_stack_trace' import { doesHaveValue, valueOrDefault } from '../value_checker' import { ILineAndUri } from '../types' -import CallSite = NodeJS.CallSite export function getDefinitionLineAndUri( cwd: string, @@ -12,11 +10,10 @@ export function getDefinitionLineAndUri( ): ILineAndUri { let line: number let uri: string - - const stackframes: CallSite[] = stackChain.callSite().map(wrapCallSite) + const stackframes: StackFrame[] = errorStackParser.parse(new Error()) const stackframe = stackframes.find( - (frame: CallSite) => - frame.getFileName() !== __filename && !isExcluded(frame.getFileName()) + (frame: StackFrame) => + frame.fileName !== __filename && !isExcluded(frame.fileName) ) if (stackframe != null) { line = stackframe.getLineNumber()