spinach 0.1.5.4 → 0.2.0.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.
Files changed (69) hide show
  1. data/Gemfile +2 -0
  2. data/README.markdown +18 -12
  3. data/features/background.feature +13 -0
  4. data/features/reporting/show_step_source_location.feature +11 -1
  5. data/features/steps/automatic_feature_generation.rb +7 -7
  6. data/features/steps/background.rb +30 -0
  7. data/features/steps/exit_status.rb +12 -13
  8. data/features/steps/feature_name_guessing.rb +7 -7
  9. data/features/steps/reporting/display_run_summary.rb +22 -22
  10. data/features/steps/reporting/error_reporting.rb +7 -7
  11. data/features/steps/reporting/show_step_source_location.rb +63 -14
  12. data/features/steps/reporting/undefined_feature_reporting.rb +7 -7
  13. data/features/steps/rspec_compatibility.rb +14 -14
  14. data/features/support/error_reporting.rb +5 -5
  15. data/features/support/filesystem.rb +71 -0
  16. data/features/support/spinach_runner.rb +5 -6
  17. data/lib/spinach.rb +11 -6
  18. data/lib/spinach/background.rb +11 -0
  19. data/lib/spinach/capybara.rb +7 -1
  20. data/lib/spinach/cli.rb +36 -13
  21. data/lib/spinach/config.rb +40 -6
  22. data/lib/spinach/dsl.rb +14 -11
  23. data/lib/spinach/exceptions.rb +1 -1
  24. data/lib/spinach/feature.rb +16 -0
  25. data/lib/spinach/frameworks.rb +2 -0
  26. data/lib/spinach/{suites → frameworks}/minitest.rb +0 -0
  27. data/lib/spinach/{suites → frameworks}/rspec.rb +0 -0
  28. data/lib/spinach/generators/feature_generator.rb +12 -23
  29. data/lib/spinach/generators/step_generator.rb +5 -5
  30. data/lib/spinach/hookable.rb +6 -4
  31. data/lib/spinach/hooks.rb +12 -4
  32. data/lib/spinach/parser.rb +6 -8
  33. data/lib/spinach/parser/visitor.rb +109 -0
  34. data/lib/spinach/reporter.rb +10 -6
  35. data/lib/spinach/reporter/stdout.rb +41 -16
  36. data/lib/spinach/reporter/stdout/error_reporting.rb +2 -2
  37. data/lib/spinach/runner.rb +9 -6
  38. data/lib/spinach/runner/feature_runner.rb +40 -34
  39. data/lib/spinach/runner/scenario_runner.rb +63 -36
  40. data/lib/spinach/scenario.rb +12 -0
  41. data/lib/spinach/step.rb +10 -0
  42. data/lib/spinach/version.rb +1 -1
  43. data/spinach.gemspec +3 -3
  44. data/test/spinach/background_test.rb +6 -0
  45. data/test/spinach/capybara_test.rb +30 -13
  46. data/test/spinach/cli_test.rb +46 -1
  47. data/test/spinach/config_test.rb +39 -0
  48. data/test/spinach/dsl_test.rb +12 -10
  49. data/test/spinach/feature_steps_test.rb +3 -3
  50. data/test/spinach/feature_test.rb +6 -0
  51. data/test/spinach/{suites → frameworks}/minitest_test.rb +2 -2
  52. data/test/spinach/generators/feature_generator_test.rb +18 -58
  53. data/test/spinach/generators/step_generator_test.rb +3 -3
  54. data/test/spinach/generators_test.rb +12 -10
  55. data/test/spinach/hookable_test.rb +8 -0
  56. data/test/spinach/hooks_test.rb +6 -7
  57. data/test/spinach/parser/visitor_test.rb +173 -0
  58. data/test/spinach/parser_test.rb +14 -27
  59. data/test/spinach/reporter/stdout/error_reporting_test.rb +9 -9
  60. data/test/spinach/reporter/stdout_test.rb +15 -19
  61. data/test/spinach/reporter_test.rb +15 -0
  62. data/test/spinach/runner/feature_runner_test.rb +79 -69
  63. data/test/spinach/runner/scenario_runner_test.rb +118 -92
  64. data/test/spinach/runner_test.rb +10 -6
  65. data/test/spinach/scenario_test.rb +6 -0
  66. data/test/spinach/step_test.rb +6 -0
  67. data/test/spinach_test.rb +7 -7
  68. metadata +60 -39
  69. data/lib/spinach/suites.rb +0 -2
@@ -18,7 +18,7 @@ module Spinach
18
18
  #
19
19
  # @api public
20
20
  def message
21
- "Step '#{@step}' not found"
21
+ "Step '#{@step.name}' not found"
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,16 @@
1
+ module Spinach
2
+ class Feature
3
+ attr_accessor :line
4
+ attr_accessor :name, :scenarios
5
+ attr_accessor :background
6
+
7
+ def initialize
8
+ @scenarios = []
9
+ end
10
+
11
+ def background_steps
12
+ @background.nil? ? [] : @background.steps
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'frameworks/minitest' if defined?(MiniTest)
2
+ require_relative 'frameworks/rspec' if defined?(RSpec::Expectations)
File without changes
File without changes
@@ -4,39 +4,28 @@ module Spinach
4
4
  # given the parsed feture data
5
5
  class FeatureGenerator
6
6
 
7
- # @param [Hash] data
8
- # the parsed feature data returned from the {Parser}
9
- def initialize(data)
10
- @data = data
7
+ attr_reader :feature
8
+
9
+ # @param [Feature] feature
10
+ # The feature returned from the {Parser}
11
+ def initialize(feature)
12
+ @feature = feature
11
13
  end
12
14
 
13
15
  # @return [Array<Hash>]
14
- # an array of unique steps found in this scenario, avoiding name
16
+ # an array of unique steps found in this feature, avoiding name
15
17
  # repetition
16
18
  def steps
17
- return @steps if @steps
18
- @steps = []
19
- if scenarios = @data['elements']
20
- scenarios.each do |scenario|
21
- if scenario_steps = scenario['steps']
22
- scenario_steps.each do |step|
23
- unless @steps.any?{|s| s['name'] == step['name']}
24
- @steps << {
25
- 'keyword' => step['keyword'].strip,
26
- 'name' => step['name'].strip
27
- }
28
- end
29
- end
30
- end
31
- end
32
- end
33
- @steps
19
+ scenario_steps = @feature.scenarios.map(&:steps).flatten
20
+ background_steps = @feature.background_steps
21
+
22
+ (scenario_steps + background_steps).uniq(&:name)
34
23
  end
35
24
 
36
25
  # @return [String]
37
26
  # this feature's name
38
27
  def name
39
- @data['name'].strip if @data['name']
28
+ @feature.name
40
29
  end
41
30
 
42
31
  # @return [String]
@@ -4,17 +4,17 @@ module Spinach
4
4
  #
5
5
  class Generators::StepGenerator
6
6
 
7
- # @param [Hash] data
8
- # the parsed step data returned from the {Parser}
9
- def initialize(data)
10
- @data = data
7
+ # @param [Step] step
8
+ # The step.
9
+ def initialize(step)
10
+ @step = step
11
11
  end
12
12
 
13
13
  # @return [String]
14
14
  # an example step definition
15
15
  def generate
16
16
  result = StringIO.new
17
- result.puts "#{@data['keyword']} '#{Spinach::Support.escape_single_commas @data['name']}' do"
17
+ result.puts "#{@step.keyword} '#{Spinach::Support.escape_single_commas @step.name}' do"
18
18
  result.puts " raise 'step not implemented'"
19
19
  result.puts "end"
20
20
  result.string
@@ -24,8 +24,8 @@ module Spinach
24
24
  define_method hook do |&block|
25
25
  add_hook(hook, &block)
26
26
  end
27
- define_method "run_#{hook}" do |*args|
28
- run_hook(hook, *args)
27
+ define_method "run_#{hook}" do |*args, &block|
28
+ run_hook(hook, *args, &block)
29
29
  end
30
30
  end
31
31
  end
@@ -50,9 +50,11 @@ module Spinach
50
50
  # @param [String] name
51
51
  # the hook's name
52
52
  #
53
- def run_hook(name, *args)
53
+ def run_hook(name, *args, &block)
54
54
  if callbacks = hooks[name.to_sym]
55
- callbacks.each{ |c| c.call(*args) }
55
+ callbacks.each{ |c| c.call(*args, &block) }
56
+ else
57
+ yield if block
56
58
  end
57
59
  end
58
60
 
data/lib/spinach/hooks.rb CHANGED
@@ -56,6 +56,14 @@ module Spinach
56
56
  # end
57
57
  hook :before_scenario
58
58
 
59
+ # Runs around every scenario
60
+ # @example
61
+ # Spinach.hooks.around_scenario do |scenario_data, &block|
62
+ # # feature_data is a hash of the parsed scenario data
63
+ # block.call
64
+ # end
65
+ hook :around_scenario
66
+
59
67
  # Runs after every scenario
60
68
  #
61
69
  # @example
@@ -139,10 +147,10 @@ module Spinach
139
147
  # # change capybara driver
140
148
  # end
141
149
  def on_tag(tag)
142
- before_scenario do |data|
143
- next unless data["tags"]
144
- tags = data["tags"].map{ |tag| tag["name"].gsub(/^@/, "") }
145
- yield(data) if tags.include? tag.to_s
150
+ before_scenario do |scenario|
151
+ tags = scenario.tags
152
+ next unless tags.any?
153
+ yield(scenario) if tags.include? tag.to_s
146
154
  end
147
155
  end
148
156
  end
@@ -1,5 +1,5 @@
1
1
  require 'gherkin'
2
- require 'gherkin/formatter/json_formatter'
2
+ require_relative 'parser/visitor'
3
3
 
4
4
  module Spinach
5
5
  # Parser leverages Gherkin to parse the feature definition.
@@ -11,8 +11,6 @@ module Spinach
11
11
  # @api public
12
12
  def initialize(content)
13
13
  @content = content
14
- @formatter = Gherkin::Formatter::JSONFormatter.new(nil)
15
- @parser = Gherkin::Parser::Parser.new(@formatter)
16
14
  end
17
15
 
18
16
  # @param [String] filename
@@ -31,15 +29,15 @@ module Spinach
31
29
  # @api public
32
30
  attr_reader :content
33
31
 
34
- # Parses the feature file and returns an AST as a Hash.
32
+ # Parses the feature file and returns a Feature.
35
33
  #
36
- # @return [Hash]
37
- # The parsed Gherkin output.
34
+ # @return [Feature]
35
+ # The Feature.
38
36
  #
39
37
  # @api public
40
38
  def parse
41
- @parser.parse(content, @filename, __LINE__-1)
42
- @formatter.gherkin_object
39
+ ast = Gherkin.parse(@content.strip)
40
+ Visitor.new.visit(ast)
43
41
  end
44
42
  end
45
43
  end
@@ -0,0 +1,109 @@
1
+ module Spinach
2
+ class Parser
3
+ # The Spinach Visitor traverses the output AST from the Gherkin parser and
4
+ # populates its Feature with all the scenarios, tags, steps, etc.
5
+ #
6
+ # @example
7
+ #
8
+ # ast = Gherkin.parse(File.read('some.feature')
9
+ # visitor = Spinach::Parser::Visitor.new
10
+ # feature = visitor.visit(ast)
11
+ #
12
+ class Visitor
13
+ attr_reader :feature
14
+
15
+ # @param [Feature] feature
16
+ # The feature to populate,
17
+ #
18
+ # @api public
19
+ def initialize
20
+ @feature = Feature.new
21
+ end
22
+
23
+ # @param [Gherkin::AST::Feature] ast
24
+ # The AST root node to visit.
25
+ #
26
+ # @api public
27
+ def visit(ast)
28
+ ast.accept self
29
+ @feature
30
+ end
31
+
32
+ # Sets the feature name and iterates over the feature scenarios.
33
+ #
34
+ # @param [Gherkin::AST::Feature] feature
35
+ # The feature to visit.
36
+ #
37
+ # @api public
38
+ def visit_Feature(node)
39
+ @feature.name = node.name
40
+ node.background.accept(self)
41
+ node.scenarios.each { |scenario| scenario.accept(self) }
42
+ end
43
+
44
+ # Iterates over the steps.
45
+ #
46
+ # @param [Gherkin::AST::Scenario] node
47
+ # The scenario to visit.
48
+ #
49
+ # @api public
50
+ def visit_Background(node)
51
+ background = Background.new(@feature)
52
+ background.line = node.line
53
+
54
+ @current_step_set = background
55
+ node.steps.each { |step| step.accept(self) }
56
+ @current_step_set = nil
57
+
58
+ @feature.background = background
59
+ end
60
+
61
+ # Sets the scenario name and iterates over the steps.
62
+ #
63
+ # @param [Gherkin::AST::Scenario] node
64
+ # The scenario to visit.
65
+ #
66
+ # @api public
67
+ def visit_Scenario(node)
68
+ scenario = Scenario.new(@feature)
69
+ scenario.name = node.name
70
+ scenario.line = node.line
71
+
72
+ @current_scenario = scenario
73
+ node.tags.each { |tag| tag.accept(self) }
74
+ @current_scenario = nil
75
+
76
+ @current_step_set = scenario
77
+ node.steps.each { |step| step.accept(self) }
78
+ @current_step_set = nil
79
+
80
+ @feature.scenarios << scenario
81
+ end
82
+
83
+ # Adds the tag to the current scenario.
84
+ #
85
+ # @param [Gherkin::AST::Tag] node
86
+ # The tag to add.
87
+ #
88
+ # @api public
89
+ def visit_Tag(node)
90
+ @current_scenario.tags << node.name
91
+ end
92
+
93
+ # Adds the step to the current scenario.
94
+ #
95
+ # @param [Gherkin::AST::Step] step
96
+ # The step to add.
97
+ #
98
+ # @api public
99
+ def visit_Step(node)
100
+ step = Step.new(@current_scenario)
101
+ step.name = node.name
102
+ step.line = node.line
103
+ step.keyword = node.keyword
104
+
105
+ @current_step_set.steps << step
106
+ end
107
+ end
108
+ end
109
+ end
@@ -31,6 +31,7 @@ module Spinach
31
31
  hooks.after_feature { |*args| after_feature_run(*args) }
32
32
  hooks.on_undefined_feature { |*args| on_feature_not_found(*args) }
33
33
  hooks.before_scenario { |*args| before_scenario_run(*args) }
34
+ hooks.around_scenario { |*args, &block| around_scenario_run(*args, &block) }
34
35
  hooks.after_scenario { |*args| after_scenario_run(*args) }
35
36
  hooks.on_successful_step { |*args| on_successful_step(*args) }
36
37
  hooks.on_undefined_step { |*args| on_undefined_step(*args) }
@@ -50,6 +51,9 @@ module Spinach
50
51
  def after_feature_run(*args); end
51
52
  def on_feature_not_found(*args); end
52
53
  def before_scenario_run(*args); end
54
+ def around_scenario_run(*args)
55
+ yield
56
+ end
53
57
  def after_scenario_run(*args); end
54
58
  def on_successful_step(*args); end;
55
59
  def on_failed_step(*args); end;
@@ -59,10 +63,10 @@ module Spinach
59
63
 
60
64
  # Stores the current feature
61
65
  #
62
- # @param [Hash]
63
- # the data for this feature
64
- def set_current_feature(data)
65
- @current_feature = data
66
+ # @param [Feature]
67
+ # The feature.
68
+ def set_current_feature(feature)
69
+ @current_feature = feature
66
70
  end
67
71
 
68
72
  # Clears this current feature
@@ -74,8 +78,8 @@ module Spinach
74
78
  #
75
79
  # @param [Hash]
76
80
  # the data for this scenario
77
- def set_current_scenario(data)
78
- @current_scenario = data
81
+ def set_current_scenario(scenario)
82
+ @current_scenario = scenario
79
83
  end
80
84
 
81
85
  # Clears this current scenario
@@ -36,8 +36,8 @@ module Spinach
36
36
  # @param [Hash] data
37
37
  # The feature in a JSON Gherkin format
38
38
  #
39
- def before_feature_run(data)
40
- name = data['name']
39
+ def before_feature_run(feature)
40
+ name = feature.name
41
41
  out.puts "\n#{'Feature:'.magenta} #{name.light_magenta}"
42
42
  end
43
43
 
@@ -46,9 +46,9 @@ module Spinach
46
46
  # @param [Hash] data
47
47
  # The feature in a JSON Gherkin format
48
48
  #
49
- def before_scenario_run(data)
50
- @max_step_name_length = data['steps'].map{|step| step['name'].length}.max if data['steps']
51
- name = data['name']
49
+ def before_scenario_run(scenario)
50
+ @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any?
51
+ name = scenario.name
52
52
  out.puts "\n #{'Scenario:'.green} #{name.light_green}"
53
53
  end
54
54
 
@@ -57,7 +57,7 @@ module Spinach
57
57
  # @param [Hash] data
58
58
  # The feature in a JSON Gherkin format
59
59
  #
60
- def after_scenario_run(data)
60
+ def after_scenario_run(scenario)
61
61
  if scenario_error
62
62
  report_error(scenario_error, :full)
63
63
  self.scenario_error = nil
@@ -66,8 +66,8 @@ module Spinach
66
66
 
67
67
  # Adds a passed step to the output buffer.
68
68
  #
69
- # @param [Hash] step
70
- # The step in a JSON Gherkin format
69
+ # @param [Step] step
70
+ # The step.
71
71
  #
72
72
  # @param [Array] step_location
73
73
  # The step source location
@@ -127,7 +127,7 @@ module Spinach
127
127
  #
128
128
  def on_feature_not_found(feature)
129
129
  generator = Generators::FeatureGenerator.new(feature)
130
- lines = "Could not find steps for `#{feature['name']}` feature\n\n"
130
+ lines = "Could not find steps for `#{feature.name}` feature\n\n"
131
131
  lines << "\nPlease create the file #{generator.filename} at #{generator.path}, with:\n\n"
132
132
 
133
133
  lines << generator.generate
@@ -167,8 +167,18 @@ module Spinach
167
167
  def output_step(symbol, step, color, step_location = nil)
168
168
  step_location = step_location.first.gsub("#{File.expand_path('.')}/", '# ')+":#{step_location.last.to_s}" if step_location
169
169
  max_length = @max_step_name_length + 60 # Colorize and output format correction
170
+
170
171
  # REMEMBER TO CORRECT PREVIOUS MAX LENGTH IF OUTPUT FORMAT IS MODIFIED
171
- out.puts " #{symbol.colorize(:"light_#{color}")} #{step['keyword'].strip.colorize(:"light_#{color}")} #{step['name'].strip.colorize(color)} ".ljust(max_length) + step_location.to_s.colorize(:grey)
172
+ buffer = []
173
+ buffer << indent(4)
174
+ buffer << symbol.colorize(:"light_#{color}")
175
+ buffer << indent(2)
176
+ buffer << step.keyword.colorize(:"light_#{color}")
177
+ buffer << indent(1)
178
+ buffer << step.name.colorize(color)
179
+ joined = buffer.join.ljust(max_length)
180
+
181
+ out.puts(joined + step_location.to_s.colorize(:grey))
172
182
  end
173
183
 
174
184
  # It prints the error summary if the run has failed
@@ -186,22 +196,37 @@ module Spinach
186
196
  # Prints the feature success summary for this run.
187
197
  #
188
198
  def run_summary
189
- successful_summary = "(".colorize(:green)+successful_steps.length.to_s.colorize(:light_green)+") Successful".colorize(:green)
190
- undefined_summary = "(".colorize(:yellow)+undefined_steps.length.to_s.colorize(:light_yellow)+") Undefined".colorize(:yellow)
191
- failed_summary = "(".colorize(:red)+failed_steps.length.to_s.colorize(:light_red)+") Failed".colorize(:red)
192
- error_summary = "(".colorize(:red)+error_steps.length.to_s.colorize(:light_red)+") Error".colorize(:red)
199
+ successful_summary = format_summary(:green, successful_steps, 'Successful')
200
+ undefined_summary = format_summary(:yellow, undefined_steps, 'Undefined')
201
+ failed_summary = format_summary(:red, failed_steps, 'Failed')
202
+ error_summary = format_summary(:red, error_steps, 'Error')
203
+
193
204
  out.puts "Steps Summary: #{successful_summary}, #{undefined_summary}, #{failed_summary}, #{error_summary}\n\n"
194
205
  end
195
206
 
196
207
  # Constructs the full step definition
197
208
  #
198
209
  # @param [Hash] step
199
- # The step in a JSON Gherkin format
210
+ # The step.
200
211
  #
201
212
  def full_step(step)
202
- "#{step['keyword'].strip} #{step['name'].strip}"
213
+ "#{step.keyword} #{step.name}"
203
214
  end
204
215
 
216
+ private
217
+
218
+ def indent(n = 1)
219
+ " " * n
220
+ end
221
+
222
+ def format_summary(color, steps, message)
223
+ buffer = []
224
+ buffer << "(".colorize(color)
225
+ buffer << steps.length.to_s.colorize(:"light_#{color}")
226
+ buffer << ") ".colorize(color)
227
+ buffer << message.colorize(color)
228
+ buffer.join
229
+ end
205
230
  end
206
231
  end
207
232
  end