spinach 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,3 +96,4 @@ end
96
96
 
97
97
  require_relative 'reporter/stdout'
98
98
  require_relative 'reporter/progress'
99
+ require_relative 'reporter/failure_file'
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+ require_relative 'reporting'
3
+ require 'pp'
4
+
5
+ module Spinach
6
+ class Reporter
7
+ # The FailureFile reporter outputs failing scenarios to a temporary file, one per line.
8
+ #
9
+ class FailureFile < Reporter
10
+
11
+ # Initializes the output filename and the temporary directory.
12
+ #
13
+ def initialize(*args)
14
+ super(*args)
15
+
16
+ # Generate a unique filename for this test run, or use the supplied option
17
+ @filename = options[:failure_filename] || ENV['SPINACH_FAILURE_FILE'] || "tmp/spinach-failures_#{Time.now.strftime('%F_%H-%M-%S-%L')}.txt"
18
+
19
+ # Create the temporary directory where we will output our file, if necessary
20
+ Dir.mkdir('tmp', 0755) unless Dir.exist?('tmp')
21
+
22
+ # Collect an array of failing scenarios
23
+ @failing_scenarios = []
24
+ end
25
+
26
+ attr_reader :failing_scenarios, :filename
27
+
28
+ # Writes all failing scenarios to a file, unless our run was successful.
29
+ #
30
+ # @param [Boolean] success
31
+ # Indicates whether the entire test run was successful
32
+ #
33
+ def after_run(success)
34
+ # Save our failed scenarios to a file
35
+ File.open(@filename, 'w') { |f| f.write @failing_scenarios.join("\n") } unless success
36
+ end
37
+
38
+ # Adds a failing step to the output buffer.
39
+ #
40
+ def on_failed_step(*args)
41
+ add_scenario(current_feature.filename, current_scenario.lines[0])
42
+ end
43
+
44
+ # Adds a step that has raised an error to the output buffer.
45
+ #
46
+ def on_error_step(*args)
47
+ add_scenario(current_feature.filename, current_scenario.lines[0])
48
+ end
49
+
50
+ private
51
+
52
+ # Adds a filename and line number to the output buffer, suitable for rerunning.
53
+ #
54
+ def add_scenario(filename, line)
55
+ @failing_scenarios << "#{filename}:#{line}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,18 @@
1
+ # Inspired by cucumber/rspec/mocks
2
+ require 'rspec/mocks'
3
+
4
+ class Spinach::FeatureSteps
5
+ include RSpec::Mocks::ExampleMethods
6
+ end
7
+
8
+ Spinach.hooks.before_scenario do
9
+ RSpec::Mocks.setup
10
+ end
11
+
12
+ Spinach.hooks.after_scenario do
13
+ begin
14
+ RSpec::Mocks.verify
15
+ ensure
16
+ RSpec::Mocks.teardown
17
+ end
18
+ end
@@ -37,21 +37,14 @@ module Spinach
37
37
  Spinach.config.support_path
38
38
  end
39
39
 
40
- # The feature files to run
41
- attr_reader :filenames
42
-
43
- # The default path where the steps are located
44
- attr_reader :step_definitions_path
45
-
46
- # The default path where the support files are located
47
- attr_reader :support_path
48
-
49
40
  # Inits the reporter with a default one.
50
41
  #
51
42
  # @api public
52
- def init_reporter
53
- reporter = Support.constantize(Spinach.config[:reporter_class]).new(Spinach.config.reporter_options)
54
- reporter.bind
43
+ def init_reporters
44
+ Spinach.config[:reporter_classes].each do |reporter_class|
45
+ reporter = Support.constantize(reporter_class).new(Spinach.config.reporter_options)
46
+ reporter.bind
47
+ end
55
48
  end
56
49
 
57
50
  # Runs this runner and outputs the results in a colorful manner.
@@ -63,36 +56,34 @@ module Spinach
63
56
  def run
64
57
  require_dependencies
65
58
  require_frameworks
66
- init_reporter
59
+ init_reporters
67
60
 
68
- Spinach.hooks.run_before_run
61
+ features = filenames.map do |filename|
62
+ file, *lines = filename.split(":") # little more complex than just a "filename"
69
63
 
70
- successful = true
64
+ # FIXME Feature should be instantiated directly, not through an unrelated class method
65
+ feature = Parser.open_file(file).parse
66
+ feature.filename = file
71
67
 
72
- filenames.map! do |filename|
73
- file, *lines = filename.split(":")
74
- [file, lines]
68
+ feature.lines_to_run = lines if lines.any?
69
+
70
+ feature
75
71
  end
76
72
 
77
- catch :fail do
78
- filenames.each do |filename, lines|
79
- lines = [nil] if lines.empty?
73
+ suite_passed = true
74
+
75
+ Spinach.hooks.run_before_run
80
76
 
81
- feature = Parser.open_file(filename).parse
82
- feature.filename = filename
83
- feature.lines = lines
77
+ features.each do |feature|
78
+ feature_passed = FeatureRunner.new(feature).run
79
+ suite_passed &&= feature_passed
84
80
 
85
- lines.each do |line|
86
- success = FeatureRunner.new(feature, line).run
87
- successful = false unless success
88
- throw :fail if fail_fast? && !successful
89
- end
90
- end
81
+ break if fail_fast? && !feature_passed
91
82
  end
92
83
 
93
- Spinach.hooks.run_after_run(successful)
84
+ Spinach.hooks.run_after_run(suite_passed)
94
85
 
95
- successful
86
+ suite_passed
96
87
  end
97
88
 
98
89
  # Loads support files and step definitions, ensuring that env.rb is loaded
@@ -10,14 +10,9 @@ module Spinach
10
10
  # @param [GherkinRuby::AST::Feature] feature
11
11
  # The feature to run.
12
12
  #
13
- # @param [#to_i] line
14
- # If a scenario line is passed, then only the scenario defined on that
15
- # line will be run.
16
- #
17
13
  # @api public
18
- def initialize(feature, line=nil)
14
+ def initialize(feature)
19
15
  @feature = feature
20
- @line = line.to_i if line
21
16
  end
22
17
 
23
18
  # @return [String]
@@ -25,7 +20,7 @@ module Spinach
25
20
  #
26
21
  # @api public
27
22
  def feature_name
28
- @feature.name
23
+ feature.name
29
24
  end
30
25
 
31
26
  # @return [Array<GherkinRuby::AST::Scenario>]
@@ -33,7 +28,7 @@ module Spinach
33
28
  #
34
29
  # @api public
35
30
  def scenarios
36
- @feature.scenarios
31
+ feature.scenarios
37
32
  end
38
33
 
39
34
  # Runs this feature.
@@ -43,52 +38,58 @@ module Spinach
43
38
  #
44
39
  # @api public
45
40
  def run
46
- Spinach.hooks.run_before_feature @feature
41
+ Spinach.hooks.run_before_feature(feature)
42
+
47
43
  if Spinach.find_step_definitions(feature_name)
48
44
  run_scenarios!
49
45
  else
50
46
  undefined_steps!
51
47
  end
52
- Spinach.hooks.run_after_feature @feature
48
+
49
+ Spinach.hooks.run_after_feature(feature)
50
+
51
+ # FIXME The feature & scenario runners should have the same structure.
52
+ # They should either both return inverted failure or both return
53
+ # raw success.
53
54
  !@failed
54
55
  end
55
56
 
56
57
  private
57
58
 
58
59
  def feature_tags
59
- if @feature.respond_to?(:tags)
60
- @feature.tags
60
+ if feature.respond_to?(:tags)
61
+ feature.tags
61
62
  else
62
63
  []
63
64
  end
64
65
  end
65
66
 
66
67
  def run_scenarios!
67
- scenarios.each_with_index do |scenario, current_scenario_index|
68
- if run_scenario?(scenario, current_scenario_index)
69
- success = ScenarioRunner.new(scenario).run
70
- @failed = true unless success
71
- break if Spinach.config.fail_fast && @failed
72
- end
68
+ scenarios_to_run.each do |scenario|
69
+ success = ScenarioRunner.new(scenario).run
70
+ @failed = true unless success
71
+
72
+ break if Spinach.config.fail_fast && @failed
73
73
  end
74
74
  end
75
75
 
76
- def run_scenario?(scenario, current_scenario_index)
77
- match_line(current_scenario_index) &&
78
- TagsMatcher.match(feature_tags + scenario.tags)
79
- end
76
+ def undefined_steps!
77
+ Spinach.hooks.run_on_undefined_feature(feature)
80
78
 
81
- def match_line(current_scenario_index)
82
- return true unless @line
83
- return false if @line < scenarios[current_scenario_index].line
84
- next_scenario = scenarios[current_scenario_index+1]
85
- !next_scenario || @line < next_scenario.line
79
+ @failed = true
86
80
  end
87
81
 
82
+ def scenarios_to_run
83
+ feature.scenarios.select do |scenario|
84
+ has_a_tag_that_will_be_run = TagsMatcher.match(feature_tags + scenario.tags)
85
+ on_a_line_that_will_be_run = if feature.run_every_scenario?
86
+ true
87
+ else
88
+ (scenario.lines & feature.lines_to_run).any?
89
+ end
88
90
 
89
- def undefined_steps!
90
- Spinach.hooks.run_on_undefined_feature @feature
91
- @failed = true
91
+ has_a_tag_that_will_be_run && on_a_line_that_will_be_run
92
+ end
92
93
  end
93
94
  end
94
95
  end
@@ -61,7 +61,7 @@ module Spinach
61
61
  end
62
62
  step_definitions.after_each
63
63
  end
64
- raise "around_scenario hooks *must* yield" if !scenario_run && !@exception
64
+ raise Spinach::HookNotYieldException.new('around_scenario') if !scenario_run && !@exception
65
65
  Spinach.hooks.run_after_scenario @scenario, step_definitions
66
66
  !@exception
67
67
  end
@@ -74,8 +74,15 @@ module Spinach
74
74
  # @api semipublic
75
75
  def run_step(step)
76
76
  step_location = step_definitions.step_location_for(step.name)
77
- step_definitions.execute(step)
77
+ step_run = false
78
+ Spinach.hooks.run_around_step step, step_definitions do
79
+ step_run = true
80
+ step_definitions.execute(step)
81
+ end
82
+ raise Spinach::HookNotYieldException.new('around_step') if !step_run
78
83
  Spinach.hooks.run_on_successful_step step, step_location, step_definitions
84
+ rescue Spinach::HookNotYieldException => e
85
+ raise e
79
86
  rescue *Spinach.config[:failure_exceptions] => e
80
87
  @exception = e
81
88
  Spinach.hooks.run_on_failed_step step, @exception, step_location, step_definitions
@@ -1,12 +1,13 @@
1
1
  module Spinach
2
2
  class Scenario
3
- attr_accessor :line
3
+ attr_accessor :lines
4
4
  attr_accessor :name, :steps, :tags, :feature
5
5
 
6
6
  def initialize(feature)
7
7
  @feature = feature
8
8
  @steps = []
9
9
  @tags = []
10
+ @lines = []
10
11
  end
11
12
  end
12
13
  end
@@ -1,4 +1,4 @@
1
1
  module Spinach
2
2
  # Spinach version.
3
- VERSION = "0.10.0"
3
+ VERSION = "0.10.1"
4
4
  end
@@ -76,7 +76,15 @@ tags:
76
76
  Spinach.stubs(:config).returns(config)
77
77
  cli = Spinach::Cli.new([opt, 'progress'])
78
78
  cli.options
79
- config.reporter_class.must_equal 'Spinach::Reporter::Progress'
79
+ config.reporter_classes.must_equal ['Spinach::Reporter::Progress']
80
+ end
81
+
82
+ it 'sets multiple reporter classes' do
83
+ config = Spinach::Config.new
84
+ Spinach.stubs(:config).returns(config)
85
+ cli = Spinach::Cli.new([opt, 'progress,stdout'])
86
+ cli.options
87
+ config.reporter_classes.must_equal ['Spinach::Reporter::Progress', 'Spinach::Reporter::Stdout']
80
88
  end
81
89
  end
82
90
  end
@@ -16,14 +16,14 @@ describe Spinach::Config do
16
16
  end
17
17
  end
18
18
 
19
- describe '#reporter_class' do
19
+ describe '#reporter_classes' do
20
20
  it 'returns a default' do
21
- subject[:reporter_class].must_equal "Spinach::Reporter::Stdout"
21
+ subject[:reporter_classes].must_equal ["Spinach::Reporter::Stdout"]
22
22
  end
23
23
 
24
24
  it 'can be overwritten' do
25
- subject[:reporter_class] = "MyOwnReporter"
26
- subject[:reporter_class].must_equal "MyOwnReporter"
25
+ subject[:reporter_classes] = ["MyOwnReporter"]
26
+ subject[:reporter_classes].must_equal ["MyOwnReporter"]
27
27
  end
28
28
  end
29
29
 
@@ -2,5 +2,32 @@ require 'test_helper'
2
2
 
3
3
  module Spinach
4
4
  describe Feature do
5
+ describe "#lines_to_run=" do
6
+ subject { Feature.new }
7
+
8
+ before { subject.lines_to_run = [4, 12] }
9
+
10
+ it 'writes lines_to_run' do
11
+ subject.lines_to_run.must_equal [4, 12]
12
+ end
13
+ end
14
+
15
+ describe '#run_every_scenario?' do
16
+ subject { Feature.new }
17
+
18
+ describe 'when no line constraints have been specified' do
19
+ it 'is true' do
20
+ subject.run_every_scenario?.must_equal true
21
+ end
22
+ end
23
+
24
+ describe 'when line constraints have been specified' do
25
+ before { subject.lines_to_run = [4, 12] }
26
+
27
+ it 'is false' do
28
+ subject.run_every_scenario?.must_equal false
29
+ end
30
+ end
31
+ end
5
32
  end
6
33
  end
@@ -59,6 +59,24 @@ describe Spinach::Hooks do
59
59
  end
60
60
  end
61
61
 
62
+ describe "around_step" do
63
+ it "responds to around_step" do
64
+ subject.must_respond_to :around_step
65
+ end
66
+
67
+ it "executes the hook with params" do
68
+ array = []
69
+ block = Proc.new do |arg1, arg2|
70
+ array << arg1
71
+ array << arg2
72
+ end
73
+ subject.send(:around_step, &block)
74
+ subject.send("run_around_step", 1, 2) do
75
+ end
76
+ array.must_equal [1, 2]
77
+ end
78
+ end
79
+
62
80
  describe "#on_tag" do
63
81
  let(:scenario) do
64
82
  stub(tags: ['javascript', 'capture'])
@@ -63,7 +63,11 @@ module Spinach
63
63
 
64
64
  describe '#visit_Scenario' do
65
65
  before do
66
- @steps = [stub_everything, stub_everything, stub_everything]
66
+ @steps = [
67
+ stub_everything(line: 4),
68
+ stub_everything(line: 5),
69
+ stub_everything(line: 6)
70
+ ]
67
71
  @tags = [stub_everything, stub_everything, stub_everything]
68
72
  @node = stub(
69
73
  tags: @tags,
@@ -83,9 +87,9 @@ module Spinach
83
87
  visitor.feature.scenarios.first.name.must_equal 'Go shopping on Saturday morning'
84
88
  end
85
89
 
86
- it 'sets the line' do
90
+ it 'sets the lines' do
87
91
  visitor.visit_Scenario(@node)
88
- visitor.feature.scenarios.first.line.must_equal 3
92
+ visitor.feature.scenarios.first.lines.must_equal (3..6).to_a
89
93
  end
90
94
 
91
95
  it 'sets the tags' do