From d1df428862f8ba67620ded4d56ce93b90cd0bf2d Mon Sep 17 00:00:00 2001 From: Julien Biezemans Date: Fri, 15 May 2015 16:16:18 +0200 Subject: [PATCH] Filter stack traces (close #157) --- features/synchronous_callbacks.feature | 6 +++ lib/cucumber/cli/argument_parser.js | 15 ++++-- lib/cucumber/cli/configuration.js | 11 +++-- lib/cucumber/listener/summary_formatter.js | 3 +- lib/cucumber/runtime.js | 10 +++- lib/cucumber/runtime/ast_tree_walker.js | 4 +- lib/cucumber/runtime/stack_trace_filter.js | 23 ++++++++++ lib/cucumber/volatile_configuration.js | 5 ++ package.json | 2 + spec/cucumber/cli/argument_parser_spec.js | 34 ++++++++++++++ spec/cucumber/cli/configuration_spec.js | 17 +++++++ spec/cucumber/runtime_spec.js | 48 +++++++++++++++++++- spec/cucumber/volatile_configuration_spec.js | 22 ++++++++- 13 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 features/synchronous_callbacks.feature create mode 100644 lib/cucumber/runtime/stack_trace_filter.js diff --git a/features/synchronous_callbacks.feature b/features/synchronous_callbacks.feature new file mode 100644 index 000000000..63730861d --- /dev/null +++ b/features/synchronous_callbacks.feature @@ -0,0 +1,6 @@ +Feature: Synchronous callbacks + + Scenario: calling back synchronously from a step definition + Given a scenario calling a synchronous step definition + When Cucumber executes the scenario + Then the scenario passes diff --git a/lib/cucumber/cli/argument_parser.js b/lib/cucumber/cli/argument_parser.js index de006f0fa..010a0b1e7 100644 --- a/lib/cucumber/cli/argument_parser.js +++ b/lib/cucumber/cli/argument_parser.js @@ -83,6 +83,7 @@ function ArgumentParser(argv) { definitions[ArgumentParser.COFFEE_SCRIPT_SNIPPETS_FLAG_NAME] = Boolean; definitions[ArgumentParser.SNIPPETS_FLAG_NAME] = Boolean; definitions[ArgumentParser.STRICT_FLAG_NAME] = Boolean; + definitions[ArgumentParser.BACKTRACE_FLAG_NAME] = Boolean; return definitions; }, @@ -93,6 +94,7 @@ function ArgumentParser(argv) { definitions[ArgumentParser.HELP_FLAG_SHORT_NAME] = [ArgumentParser.LONG_OPTION_PREFIX + ArgumentParser.HELP_FLAG_NAME]; definitions[ArgumentParser.SNIPPETS_FLAG_SHORT_NAME] = [ArgumentParser.LONG_OPTION_PREFIX + 'no-' + ArgumentParser.SNIPPETS_FLAG_NAME]; definitions[ArgumentParser.STRICT_FLAG_SHORT_NAME] = [ArgumentParser.LONG_OPTION_PREFIX + ArgumentParser.STRICT_FLAG_NAME]; + definitions[ArgumentParser.BACKTRACE_FLAG_SHORT_NAME] = [ArgumentParser.LONG_OPTION_PREFIX + ArgumentParser.BACKTRACE_FLAG_NAME]; return definitions; }, @@ -109,13 +111,15 @@ function ArgumentParser(argv) { }, shouldSnippetsBeInCoffeeScript: function shouldSnippetsBeInCoffeeScript() { - var areSnippetsInCoffeeScript = self.getOptionOrDefault(ArgumentParser.COFFEE_SCRIPT_SNIPPETS_FLAG_NAME, ArgumentParser.DEFAULT_COFFEE_SCRIPT_SNIPPETS_FLAG_VALUE); - return areSnippetsInCoffeeScript; + return self.getOptionOrDefault(ArgumentParser.COFFEE_SCRIPT_SNIPPETS_FLAG_NAME, ArgumentParser.DEFAULT_COFFEE_SCRIPT_SNIPPETS_FLAG_VALUE); }, shouldSnippetsBeShown: function shouldSnippetsBeInCoffeeScript() { - var areSnippetsShown = self.getOptionOrDefault(ArgumentParser.SNIPPETS_FLAG_NAME, ArgumentParser.DEFAULT_SNIPPETS_FLAG_VALUE); - return areSnippetsShown; + return self.getOptionOrDefault(ArgumentParser.SNIPPETS_FLAG_NAME, ArgumentParser.DEFAULT_SNIPPETS_FLAG_VALUE); + }, + + shouldFilterStackTraces: function shouldFilterStackTraces() { + return !self.getOptionOrDefault(ArgumentParser.BACKTRACE_FLAG_NAME, ArgumentParser.DEFAULT_BACKTRACE_FLAG_VALUE); }, storeOptions: function storeOptions(newOptions) { @@ -160,6 +164,9 @@ ArgumentParser.DEFAULT_COFFEE_SCRIPT_SNIPPETS_FLAG_VALUE = false; ArgumentParser.SNIPPETS_FLAG_NAME = 'snippets'; ArgumentParser.SNIPPETS_FLAG_SHORT_NAME = 'i'; ArgumentParser.DEFAULT_SNIPPETS_FLAG_VALUE = true; +ArgumentParser.BACKTRACE_FLAG_NAME = 'backtrace'; +ArgumentParser.BACKTRACE_FLAG_SHORT_NAME = 'b'; +ArgumentParser.DEFAULT_BACKTRACE_FLAG_VALUE = false; ArgumentParser.FeaturePathExpander = require('./argument_parser/feature_path_expander'); ArgumentParser.PathExpander = require('./argument_parser/path_expander'); ArgumentParser.SupportCodePathExpander = require('./argument_parser/support_code_path_expander'); diff --git a/lib/cucumber/cli/configuration.js b/lib/cucumber/cli/configuration.js index e93834395..9b1ac11b5 100644 --- a/lib/cucumber/cli/configuration.js +++ b/lib/cucumber/cli/configuration.js @@ -90,15 +90,16 @@ function Configuration(argv) { }, shouldSnippetsBeInCoffeeScript: function shouldSnippetsBeInCoffeeScript() { - var coffeeDisplay = argumentParser.shouldSnippetsBeInCoffeeScript(); - return coffeeDisplay; + return argumentParser.shouldSnippetsBeInCoffeeScript(); }, shouldSnippetsBeShown: function shouldSnippetsBeShown() { - var snippetsDisplay = argumentParser.shouldSnippetsBeShown(); - return snippetsDisplay; - } + return argumentParser.shouldSnippetsBeShown(); + }, + shouldFilterStackTraces: function shouldFilterStackTraces() { + return argumentParser.shouldFilterStackTraces(); + } }; return self; } diff --git a/lib/cucumber/listener/summary_formatter.js b/lib/cucumber/listener/summary_formatter.js index 5a166a465..5d500403a 100644 --- a/lib/cucumber/listener/summary_formatter.js +++ b/lib/cucumber/listener/summary_formatter.js @@ -110,7 +110,8 @@ function SummaryFormatter(options) { self.logFailedStepResult = function logFailedStepResult(stepResult) { var failureMessage = stepResult.getFailureException(); - self.log(failureMessage.stack || failureMessage); + if (failureMessage) + self.log(failureMessage.stack || failureMessage); self.log('\n\n'); }; diff --git a/lib/cucumber/runtime.js b/lib/cucumber/runtime.js index d5e10f549..bf19c2f0b 100644 --- a/lib/cucumber/runtime.js +++ b/lib/cucumber/runtime.js @@ -16,7 +16,14 @@ function Runtime(configuration) { var features = self.getFeatures(); var supportCodeLibrary = self.getSupportCodeLibrary(); var astTreeWalker = Runtime.AstTreeWalker(features, supportCodeLibrary, listeners, isStrictRequested); - astTreeWalker.walk(callback); + + if (configuration.shouldFilterStackTraces()) + Runtime.StackTraceFilter.filter(); + + astTreeWalker.walk(function (result) { + Runtime.StackTraceFilter.unfilter(); + callback(result); + }); }, attachListener: function attachListener(listener) { @@ -48,5 +55,6 @@ Runtime.FailedStepResult = require('./runtime/failed_step_result'); Runtime.SkippedStepResult = require('./runtime/skipped_step_result'); Runtime.UndefinedStepResult = require('./runtime/undefined_step_result'); Runtime.Attachment = require('./runtime/attachment'); +Runtime.StackTraceFilter = require('./runtime/stack_trace_filter'); module.exports = Runtime; diff --git a/lib/cucumber/runtime/ast_tree_walker.js b/lib/cucumber/runtime/ast_tree_walker.js index b9047f387..0d27c6ace 100644 --- a/lib/cucumber/runtime/ast_tree_walker.js +++ b/lib/cucumber/runtime/ast_tree_walker.js @@ -28,7 +28,7 @@ function AstTreeWalker(features, supportCodeLibrary, listeners, strictMode) { visitFeatures: function visitFeatures(features, callback) { var payload = { features: features }; var event = AstTreeWalker.Event(AstTreeWalker.FEATURES_EVENT_NAME, payload); - self.broadcastEventAroundUserFunction ( + self.broadcastEventAroundUserFunction( event, function (callback) { features.acceptVisitor(self, callback); }, callback @@ -38,7 +38,7 @@ function AstTreeWalker(features, supportCodeLibrary, listeners, strictMode) { visitFeature: function visitFeature(feature, callback) { var payload = { feature: feature }; var event = AstTreeWalker.Event(AstTreeWalker.FEATURE_EVENT_NAME, payload); - self.broadcastEventAroundUserFunction ( + self.broadcastEventAroundUserFunction( event, function (callback) { feature.acceptVisitor(self, callback); }, callback diff --git a/lib/cucumber/runtime/stack_trace_filter.js b/lib/cucumber/runtime/stack_trace_filter.js new file mode 100644 index 000000000..c13719a24 --- /dev/null +++ b/lib/cucumber/runtime/stack_trace_filter.js @@ -0,0 +1,23 @@ +var path = require('path'); +var chain = require('stack-chain'); + +var currentFilter = null; + +function filter() { + currentFilter = chain.filter.attach(function (error, frames) { + return frames.filter(function (frame) { + var f = frame.getFileName() || ''; + var ignoredPath = path.join(__dirname, '..'); + return f.indexOf(ignoredPath) === -1; + }); + }); +} + +function unfilter() { + chain.filter.deattach(currentFilter); +} + +module.exports = { + filter: filter, + unfilter: unfilter +}; diff --git a/lib/cucumber/volatile_configuration.js b/lib/cucumber/volatile_configuration.js index f804959f5..e79ed9c83 100644 --- a/lib/cucumber/volatile_configuration.js +++ b/lib/cucumber/volatile_configuration.js @@ -5,6 +5,7 @@ function VolatileConfiguration(features, supportCodeInitializer, options) { options = options || {}; var strictMode = !!options.strict; var tagGroupStrings = options.tags || []; + var backtrace = !!options.backtrace; var self = { isStrictMode: function isStrictMode() { @@ -44,6 +45,10 @@ function VolatileConfiguration(features, supportCodeInitializer, options) { var tagGroup = tagGroupParser.parse(); var rule = Cucumber.Ast.Filter.AnyOfTagsRule(tagGroup); return rule; + }, + + shouldFilterStackTraces: function shouldFilterStackTraces() { + return !backtrace; } }; return self; diff --git a/package.json b/package.json index 88e151fed..7ccb1fb7c 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,10 @@ "coffee-script": "1.8.0", "cucumber-html": "0.2.3", "gherkin": "2.12.2", + "hide-stack-frames-from": "^1.0.0", "nopt": "3.0.1", "pogo": "0.9.4", + "stack-chain": "^1.3.1", "underscore": "1.7.0", "underscore.string": "2.3.3", "walkdir": "0.0.7" diff --git a/spec/cucumber/cli/argument_parser_spec.js b/spec/cucumber/cli/argument_parser_spec.js index 5600ac60b..c713b8555 100644 --- a/spec/cucumber/cli/argument_parser_spec.js +++ b/spec/cucumber/cli/argument_parser_spec.js @@ -88,6 +88,11 @@ describe("Cucumber.Cli.ArgumentParser", function () { var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.SNIPPETS_FLAG_NAME]).toEqual(Boolean); }); + + it("defines a --backtrace flag", function () { + var knownOptionDefinitions = argumentParser.getKnownOptionDefinitions(); + expect(knownOptionDefinitions[Cucumber.Cli.ArgumentParser.BACKTRACE_FLAG_NAME]).toEqual(Boolean); + }); }); describe("getShortenedOptionDefinitions()", function () { @@ -135,6 +140,14 @@ describe("Cucumber.Cli.ArgumentParser", function () { var shortenedOptionDefinitions = argumentParser.getShortenedOptionDefinitions(); expect(shortenedOptionDefinitions[aliasName]).toEqual(aliasValue); }); + + it("defines an alias to --backtrace as -b", function () { + var optionName = Cucumber.Cli.ArgumentParser.LONG_OPTION_PREFIX + Cucumber.Cli.ArgumentParser.BACKTRACE_FLAG_NAME; + var aliasName = Cucumber.Cli.ArgumentParser.BACKTRACE_FLAG_SHORT_NAME; + var aliasValue = [optionName]; + var shortenedOptionDefinitions = argumentParser.getShortenedOptionDefinitions(); + expect(shortenedOptionDefinitions[aliasName]).toEqual(aliasValue); + }); }); describe("getFeatureFilePaths()", function () { @@ -416,6 +429,27 @@ describe("Cucumber.Cli.ArgumentParser", function () { }); }); + describe("shouldFilterStackTraces()", function () { + beforeEach(function () { + spyOn(argumentParser, 'getOptionOrDefault'); + }); + + it("gets the 'backtrace' flag with a falsy default value", function () { + argumentParser.shouldFilterStackTraces(); + expect(argumentParser.getOptionOrDefault).toHaveBeenCalledWith("backtrace", false); + }); + + it("returns true when the backtrace flag isn't set", function () { + argumentParser.getOptionOrDefault.andReturn(false); + expect(argumentParser.shouldFilterStackTraces()).toBeTruthy(); + }); + + it("returns false when the backtrace flag is set", function () { + argumentParser.getOptionOrDefault.andReturn(true); + expect(argumentParser.shouldFilterStackTraces()).toBeFalsy(); + }); + }); + describe("getOptions() [storeOptions()]", function () { var options; diff --git a/spec/cucumber/cli/configuration_spec.js b/spec/cucumber/cli/configuration_spec.js index 98dcee0fb..44fa3664e 100644 --- a/spec/cucumber/cli/configuration_spec.js +++ b/spec/cucumber/cli/configuration_spec.js @@ -337,4 +337,21 @@ describe("Cucumber.Cli.Configuration", function () { }); }); + describe("shouldFilterStackTraces()", function () { + beforeEach(function () { + spyOnStub(argumentParser, 'shouldFilterStackTraces'); + }); + + it("asks the argument parser whether the stack traces are filtered", function () { + configuration.shouldFilterStackTraces(); + expect(argumentParser.shouldFilterStackTraces).toHaveBeenCalled(); + }); + + it("tells whether the stack traces are filtered or not", function () { + var shouldStackTracesBeFiltered = createSpy("filter stack traces?"); + argumentParser.shouldFilterStackTraces.andReturn(shouldStackTracesBeFiltered); + expect(configuration.shouldFilterStackTraces()).toBe(shouldStackTracesBeFiltered); + }); + }); + }); diff --git a/spec/cucumber/runtime_spec.js b/spec/cucumber/runtime_spec.js index e434db327..9e68b5617 100644 --- a/spec/cucumber/runtime_spec.js +++ b/spec/cucumber/runtime_spec.js @@ -10,8 +10,10 @@ describe("Cucumber.Runtime", function () { beforeEach(function () { isStrictRequested = false; listeners = createSpyWithStubs("listener collection", {add: null}); - configuration = createSpyWithStubs("configuration", { isStrictRequested: isStrictRequested }); + configuration = createSpyWithStubs("configuration", { isStrictRequested: isStrictRequested, shouldFilterStackTraces: true }); spyOn(Cucumber.Type, 'Collection').andReturn(listeners); + spyOn(Cucumber.Runtime.StackTraceFilter, 'filter'); + spyOn(Cucumber.Runtime.StackTraceFilter, 'unfilter'); runtime = Cucumber.Runtime(configuration); }); @@ -69,9 +71,51 @@ describe("Cucumber.Runtime", function () { expect(Cucumber.Runtime.AstTreeWalker).toHaveBeenCalledWith(features, supportCodeLibrary, listeners, isStrictRequested); }); + describe("when stack traces should be filtered", function () { + beforeEach(function () { + configuration.shouldFilterStackTraces.andReturn(true); + }); + + it("activates the stack trace filter", function () { + runtime.start(callback); + expect(Cucumber.Runtime.StackTraceFilter.filter).toHaveBeenCalled(); + }); + }); + + describe("when stack traces should be unfiltered", function () { + beforeEach(function () { + configuration.shouldFilterStackTraces.andReturn(false); + }); + + it("does not activate the stack trace filter", function () { + runtime.start(callback); + expect(Cucumber.Runtime.StackTraceFilter.filter).not.toHaveBeenCalled(); + }); + }); + it("tells the AST tree walker to walk", function () { runtime.start(callback); - expect(astTreeWalker.walk).toHaveBeenCalledWith(callback); + expect(astTreeWalker.walk).toHaveBeenCalledWithAFunctionAsNthParameter(1); + }); + + describe("when the AST tree walker is done walking", function () { + var walkCallback, walkResults; + + beforeEach(function () { + runtime.start(callback); + walkCallback = astTreeWalker.walk.mostRecentCall.args[0]; + walkResults = createSpy("AST tree walker results"); + }); + + it("deactivates the stack trace filter", function () { + walkCallback(walkResults); + expect(Cucumber.Runtime.StackTraceFilter.unfilter).toHaveBeenCalled(); + }); + + it("calls back", function () { + walkCallback(walkResults); + expect(callback).toHaveBeenCalledWith(walkResults); + }); }); }); diff --git a/spec/cucumber/volatile_configuration_spec.js b/spec/cucumber/volatile_configuration_spec.js index 3226ca93b..a1d364290 100644 --- a/spec/cucumber/volatile_configuration_spec.js +++ b/spec/cucumber/volatile_configuration_spec.js @@ -78,11 +78,12 @@ describe("Cucumber.VolatileConfiguration", function () { describe("isStrictMode()",function () { it("is false if strict option is not specified",function () { - configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, {}); + configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, {}); expect(configuration.isStrictMode()).toEqual(false); }); + it("is true if strict option is set",function () { - configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, {strict:true}); + configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, {strict:true}); expect(configuration.isStrictMode()).toEqual(true); }); }); @@ -151,4 +152,21 @@ describe("Cucumber.VolatileConfiguration", function () { expect(returned).toBe(rule); }); }); + + describe("shouldFilterStackTraces", function () { + it("returns true by default", function () { + configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, {}); + expect(configuration.shouldFilterStackTraces()).toBeTruthy(); + }); + + it("returns false when the backtrace option is truthy", function () { + configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, { backtrace: true }); + expect(configuration.shouldFilterStackTraces()).toBeFalsy(); + }); + + it("returns true when the backtrace option is falsy", function () { + configuration = Cucumber.VolatileConfiguration(featureSources, supportCodeInitializer, { backtrace: false}); + expect(configuration.shouldFilterStackTraces()).toBeTruthy(); + }); + }); });