spinach 0.1.5.4 → 0.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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