spinach 0.0.6 → 0.1.0

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 (41) hide show
  1. data/Gemfile +1 -0
  2. data/Readme.md +3 -0
  3. data/bin/spinach +3 -1
  4. data/features/steps/error_reporting.rb +4 -8
  5. data/features/steps/exit_status.rb +0 -1
  6. data/features/steps/feature_name_guessing.rb +1 -2
  7. data/features/support/spinach_runner.rb +0 -2
  8. data/lib/spinach/capybara.rb +8 -8
  9. data/lib/spinach/cli.rb +30 -18
  10. data/lib/spinach/config.rb +29 -10
  11. data/lib/spinach/dsl.rb +45 -21
  12. data/lib/spinach/exceptions.rb +21 -8
  13. data/lib/spinach/feature.rb +8 -4
  14. data/lib/spinach/parser.rb +13 -7
  15. data/lib/spinach/reporter/stdout.rb +251 -53
  16. data/lib/spinach/reporter.rb +43 -26
  17. data/lib/spinach/runner/feature.rb +39 -26
  18. data/lib/spinach/runner/scenario.rb +43 -28
  19. data/lib/spinach/runner.rb +40 -20
  20. data/lib/spinach/support.rb +8 -7
  21. data/lib/spinach/version.rb +2 -1
  22. data/lib/spinach.rb +17 -9
  23. data/spinach.gemspec +2 -1
  24. data/test/spinach/capybara_test.rb +32 -3
  25. data/test/spinach/cli_test.rb +32 -11
  26. data/test/spinach/config_test.rb +9 -6
  27. data/test/spinach/dsl_test.rb +8 -1
  28. data/test/spinach/feature_test.rb +14 -11
  29. data/test/spinach/parser_test.rb +33 -20
  30. data/test/spinach/reporter/stdout_test.rb +364 -103
  31. data/test/spinach/reporter_test.rb +145 -20
  32. data/test/spinach/runner/feature_test.rb +26 -51
  33. data/test/spinach/runner/scenario_test.rb +64 -35
  34. data/test/spinach/runner_test.rb +41 -32
  35. data/test/spinach/support_test.rb +2 -10
  36. data/test/spinach_test.rb +14 -14
  37. data/test/test_helper.rb +13 -5
  38. metadata +36 -28
  39. data/examples/steps/user_logs_in.rb +0 -23
  40. data/examples/user_logs_in.feature +0 -6
  41. data/examples/user_logs_in.rb +0 -23
@@ -6,81 +6,279 @@ module Spinach
6
6
  #
7
7
  class Stdout < Reporter
8
8
 
9
+ # The output buffers to store the reports.
10
+ attr_reader :out, :error
11
+
12
+ # The last scenario error
13
+ attr_accessor :scenario_error
14
+
15
+ # Initialitzes the runner
16
+ #
17
+ # @param [Hash] options
18
+ # Sets a custom output buffer by setting options[:output]
19
+ # Sets a custom error buffer by setting options[:error]
20
+ #
21
+ def initialize(*args)
22
+ super(*args)
23
+ @out = options[:output] || $stdout
24
+ @error = options[:error] || $stderr
25
+ end
26
+
9
27
  # Prints the feature name to the standard output
10
28
  #
11
- def feature(name)
12
- puts "\n#{'Feature:'.magenta} #{name.light_magenta}"
29
+ # @param [Hash] data
30
+ # The feature in a JSON Gherkin format
31
+ #
32
+ def before_feature_run(data)
33
+ name = data['name']
34
+ out.puts "\n#{'Feature:'.magenta} #{name.light_magenta}"
13
35
  end
14
36
 
15
37
  # Prints the scenario name to the standard ouput
16
38
  #
17
- def scenario(name)
18
- puts "\n #{'Scenario:'.green} #{name.light_green}"
19
- puts
20
- end
21
-
22
- # Prints the step name to the standard output. If failed, it puts an
23
- # F! before
24
- #
25
- def step(keyword, name, result)
26
- color, symbol = case result
27
- when :success
28
- [:green, '✔']
29
- when :undefined_step
30
- [:yellow, '?']
31
- when :failure
32
- [:red, '✘']
33
- when :error
34
- [:red, '!']
35
- when :skip
36
- [:cyan, '~']
39
+ # @param [Hash] data
40
+ # The feature in a JSON Gherkin format
41
+ #
42
+ def before_scenario_run(data)
43
+ name = data['name']
44
+ out.puts "\n #{'Scenario:'.green} #{name.light_green}"
45
+ out.puts
46
+ end
47
+
48
+ # Adds an error report and re
49
+ #
50
+ # @param [Hash] data
51
+ # The feature in a JSON Gherkin format
52
+ #
53
+ def after_scenario_run(data)
54
+ if scenario_error
55
+ report_error(scenario_error, :full)
56
+ self.scenario_error = nil
37
57
  end
38
- puts " #{symbol.colorize(:"light_#{color}")} #{keyword.colorize(:"light_#{color}")} #{name.colorize(color)}"
58
+ end
39
59
 
60
+ # Adds a passed step to the output buffer.
61
+ #
62
+ # @param [Hash] step
63
+ # The step in a JSON Gherkin format
64
+ #
65
+ def on_successful_step(step)
66
+ output_step('✔', step, :green)
40
67
  end
41
68
 
42
- # Prints a blank line at the end
69
+ # Adds a failing step to the output buffer.
70
+ #
71
+ # @param [Hash] step
72
+ # The step in a JSON Gherkin format
73
+ #
74
+ # @param [Exception] failure
75
+ # The exception that caused the failure
43
76
  #
44
- def end
45
- puts ""
77
+ def on_failed_step(step, failure)
78
+ output_step('✘', step, :red)
79
+ self.scenario_error = [current_feature, current_scenario, step, failure]
80
+ failed_steps << scenario_error
46
81
  end
47
82
 
48
- def error_summary(errors)
49
- puts
50
- puts " ! Error summary for this feature (#{errors.length})".light_white
51
- errors.each do |error, step, line, scenario|
52
- step_file = error.backtrace.detect do |f|
53
- f =~ /<class:#{scenario.feature.class}>/
54
- end
55
- step_file = step_file.split(':')[0..1].join(':') if step_file
56
-
57
- color = if error.kind_of?(Spinach::StepNotDefinedException)
58
- :light_yellow
59
- else
60
- :light_red
61
- end
62
-
63
- puts
64
- puts " #{scenario.feature_name} :: #{scenario.name} :: #{step.colorize(color)} (line #{line})"
65
- puts " #{step_file}" if step_file
66
- error.message.split("\n").each do |line|
67
- puts " #{line}".colorize(color)
68
- end
69
- if options[:backtrace]
70
- puts error.backtrace.map {|e| " #{e}"}
83
+ # Adds a step that has raised an error to the output buffer.
84
+ #
85
+ # @param [Hash] step
86
+ # The step in a JSON Gherkin format
87
+ #
88
+ # @param [Exception] failure
89
+ # The exception that caused the failure
90
+ #
91
+ def on_error_step(step, failure)
92
+ output_step('!', step, :red)
93
+ self.scenario_error = [current_feature, current_scenario, step, failure]
94
+ error_steps << scenario_error
95
+ end
96
+
97
+ # Adds an undefined step to the output buffer.
98
+ #
99
+ # @param [Hash] step
100
+ # The step in a JSON Gherkin format
101
+ #
102
+ def on_undefined_step(step)
103
+ output_step('?', step, :yellow)
104
+ self.scenario_error = [current_feature, current_scenario, step]
105
+ undefined_steps << scenario_error
106
+ end
107
+
108
+ # Adds a step that has been skipped to the output buffer.
109
+ #
110
+ # @param [Hash] step
111
+ # The step that Gherkin extracts
112
+ #
113
+ def on_skipped_step(step)
114
+ output_step('~', step, :cyan)
115
+ end
116
+
117
+ # Adds to the output buffer a step result
118
+ #
119
+ # @param [String] symbol
120
+ # A symbol to prepend before the step keyword (might be useful to
121
+ # indicate if everything went ok or not).
122
+ #
123
+ # @param [Hash] step
124
+ # The step in a JSON Gherkin format
125
+ #
126
+ # @param [Symbol] color
127
+ # The color code to use with Colorize to colorize the output.
128
+ #
129
+ def output_step(symbol, step, color)
130
+ out.puts " #{symbol.colorize(:"light_#{color}")} #{step['keyword'].strip.colorize(:"light_#{color}")} #{step['name'].strip.colorize(color)}"
131
+ end
132
+
133
+ # It prints the error summary if the run has failed
134
+ #
135
+ # @param [True,False] success
136
+ # whether the run has succeed or not
137
+ #
138
+ def after_run(success)
139
+ error_summary unless success
140
+ end
141
+
142
+ # Prints the errors for ths run.
143
+ #
144
+ def error_summary
145
+ error.puts "\nError summary:\n"
146
+ report_error_steps
147
+ report_failed_steps
148
+ report_undefined_steps
149
+ end
150
+
151
+ # Prints the steps that raised an error.
152
+ #
153
+ def report_error_steps
154
+ report_errors('Errors', error_steps, :light_red) if error_steps.any?
155
+ end
156
+
157
+ # Prints failing steps.
158
+ #
159
+ def report_failed_steps
160
+ report_errors('Failures', failed_steps, :light_red) if failed_steps.any?
161
+ end
162
+
163
+ # Prints undefined steps.
164
+ #
165
+ def report_undefined_steps
166
+ report_errors('Undefined steps', undefined_steps, :yellow) if undefined_steps.any?
167
+ end
168
+
169
+ # Prints the error for a given set of steps
170
+ #
171
+ # @param [String] banner
172
+ # the text to prepend as the title
173
+ #
174
+ # @param [Array] steps
175
+ # the steps to output
176
+ #
177
+ # @param [Symbol] color
178
+ # The color code to use with Colorize to colorize the output.
179
+ #
180
+ def report_errors(banner, steps, color)
181
+ error.puts "#{banner} (#{steps.length})".colorize(color)
182
+ steps.each do |error|
183
+ report_error error
184
+ end
185
+ error.puts ""
186
+ end
187
+
188
+ # Prints an error in a nice format
189
+ #
190
+ # @param [Array] error
191
+ # An array containing the feature, scenario, step and exception
192
+ #
193
+ # @param [Symbol] format
194
+ # The format to output the error. Currently supproted formats are
195
+ # :summarized (default) and :full
196
+ #
197
+ # @returns [String]
198
+ # The error report
199
+ #
200
+ def report_error(error, format=:summarized)
201
+ case format
202
+ when :summarized
203
+ self.error.puts summarized_error(error)
204
+ when :full
205
+ self.error.puts full_error(error)
206
+ else
207
+ raise "Format not defined"
208
+ end
209
+ end
210
+
211
+ # Returns summarized error report
212
+ #
213
+ # @param [Array] error
214
+ # An array containing the feature, scenario, step and exception
215
+ #
216
+ # @returns [String]
217
+ # The summarized error report
218
+ #
219
+ def summarized_error(error)
220
+ feature, scenario, step, exception = error
221
+ summary = " #{feature['name']} :: #{scenario['name']} :: #{full_step step}"
222
+ if exception.kind_of?(Spinach::StepNotDefinedException)
223
+ summary.yellow
224
+ else
225
+ summary.red
226
+ end
227
+ end
228
+
229
+ # Returns a complete error report
230
+ #
231
+ # @param [Array] error
232
+ # An array containing the feature, scenario, step and exception
233
+ #
234
+ # @returns [String]
235
+ # The coplete error report
236
+ #
237
+ def full_error(error)
238
+ feature, scenario, step, exception = error
239
+ output = String.new
240
+ output += report_exception(exception)
241
+ output +="\n"
242
+
243
+ if options[:backtrace]
244
+ output += "\n"
245
+ exception.backtrace.map do |line|
246
+ output << " #{line}\n"
71
247
  end
72
- puts
248
+ else
249
+ output << " #{exception.backtrace[0]}"
73
250
  end
251
+ output
252
+ end
253
+
254
+ # Constructs the full step definition
255
+ #
256
+ # @param [Hash] step
257
+ # The step in a JSON Gherkin format
258
+ #
259
+ def full_step(step)
260
+ "#{step['keyword'].strip} #{step['name'].strip}"
74
261
  end
75
262
 
263
+ # Prints a information when an exception is raised.
264
+ #
265
+ # @param [Exception] exception
266
+ # The exception to report
267
+ #
268
+ # @returns [String]
269
+ # The exception report
270
+ #
76
271
  def report_exception(exception)
77
- @errors << exception
78
272
  output = exception.message.split("\n").map{ |line|
79
273
  " #{line}"
80
274
  }.join("\n")
81
- puts "#{output}\n\n"
82
- end
83
275
 
276
+ if exception.kind_of?(Spinach::StepNotDefinedException)
277
+ output.yellow
278
+ else
279
+ output.red
280
+ end
281
+ end
84
282
  end
85
283
  end
86
284
  end
@@ -10,40 +10,57 @@ module Spinach
10
10
  def initialize(options = {})
11
11
  @errors = []
12
12
  @options = options
13
+ @undefined_steps = []
14
+ @failed_steps = []
15
+ @error_steps = []
13
16
  end
14
17
 
15
- attr_accessor :options
16
-
17
- # Receives this hook when a feature is invoked
18
- # @param [String] name
19
- # the feature name
18
+ # A Hash with options for the reporter
20
19
  #
21
- def feature(name)
22
- raise "Abstract method!"
23
- end
20
+ attr_reader :options
24
21
 
25
- # Receives this hook when a scenario is invoked
26
- # @param [String] name
27
- # the scenario name
28
- #
29
- def scenario(name)
30
- raise "Abstract method!"
22
+ attr_accessor :current_feature, :current_scenario
23
+
24
+ attr_reader :undefined_steps, :failed_steps, :error_steps
25
+
26
+ def bind
27
+ runner.after_run method(:after_run)
28
+ feature_runner.before_run method(:before_feature_run)
29
+ feature_runner.after_run method(:after_feature_run)
30
+ scenario_runner.before_run method(:before_scenario_run)
31
+ scenario_runner.after_run method(:after_scenario_run)
32
+ scenario_runner.on_successful_step method(:on_successful_step)
33
+ scenario_runner.on_failed_step method(:on_failed_step)
34
+ scenario_runner.on_error_step method(:on_error_step)
35
+ scenario_runner.on_skipped_step method(:on_skipped_step)
36
+
37
+ feature_runner.before_run method(:current_feature=)
38
+ feature_runner.after_run method(:clear_current_feature)
39
+ scenario_runner.before_run method(:current_scenario=)
40
+ scenario_runner.after_run method(:clear_current_scenario)
31
41
  end
32
42
 
33
- # Receives this hook when a step is invoked
34
- # @param [String] name
35
- # the scenario name
36
- # @param [Symbol] result
37
- # the step name and its finishing state. May be :success or :failure
38
- #
39
- def step(keyword, name, result)
40
- raise "Abstract method!"
43
+ def feature_runner; Runner::Feature; end
44
+ def scenario_runner; Runner::Scenario; end
45
+ def runner; Runner; end
46
+
47
+ def after_run(*args); end;
48
+ def before_feature_run(*args); end
49
+ def after_feature_run(*args); end
50
+ def before_scenario_run(*args); end
51
+ def after_scenario_run(*args); end
52
+ def on_successful_step(*args); end;
53
+ def on_failed_step(*args); end;
54
+ def on_error_step(*args); end;
55
+ def on_undefined_step(*args); end;
56
+ def on_skipped_step(*args); end;
57
+
58
+ def clear_current_feature(*args)
59
+ self.current_feature = nil
41
60
  end
42
61
 
43
- # Receives this hook when a feature reaches its end
44
- #
45
- def end
46
- raise "Abstract method!"
62
+ def clear_current_scenario(*args)
63
+ self.current_scenario = nil
47
64
  end
48
65
 
49
66
  end
@@ -1,75 +1,88 @@
1
+ require 'hooks'
2
+
1
3
  module Spinach
2
4
  class Runner
3
- # A feature runner handles a particular Spinach::Feature run.
5
+ # A feature runner handles a particular feature run.
4
6
  #
5
7
  class Feature
8
+ include Hooks
9
+
10
+ # The {Reporter} used in this feature.
11
+ attr_reader :reporter
12
+
13
+ # The file that describes the feature.
14
+ attr_reader :filename
15
+
16
+ define_hook :before_run
17
+ define_hook :after_run
6
18
 
7
19
  # @param [String] filename
8
20
  # path to the feature file. Scenario line could be passed to run just
9
21
  # that scenario.
10
22
  # @example feature/a_cool_feature.feature:12
11
23
  #
12
- # @param [Spinach::Reporter] reporter
13
- # the reporter that will log this run
14
- #
15
- def initialize(filename, reporter)
24
+ # @api public
25
+ def initialize(filename)
16
26
  @filename, @scenario_line = filename.split(':')
17
- @reporter = reporter
18
27
  end
19
28
 
20
- attr_reader :reporter
29
+ # The file taht describes the feature.
30
+ #
21
31
  attr_reader :filename
22
32
 
23
- # @return [Spinach::Feature]
24
- # the feature towards which this scenario will be run
33
+ # @return [Feature]
34
+ # The feature object used to run this scenario.
25
35
  #
36
+ # @api public
26
37
  def feature
27
38
  @feature ||= Spinach.find_feature(feature_name).new
28
39
  end
29
40
 
30
41
  # @return [Hash]
31
- # the parsed data for this feature
42
+ # The parsed data for this feature.
32
43
  #
44
+ # @api public
33
45
  def data
34
46
  @data ||= Spinach::Parser.new(filename).parse
35
47
  end
36
48
 
37
49
  # @return [String]
38
- # this feature's name
50
+ # This feature name.
39
51
  #
52
+ # @api public
40
53
  def feature_name
41
54
  @feature_name ||= data['name']
42
55
  end
43
56
 
44
57
  # @return [Hash]
45
- # the parsed scenarios for this runner's feature
58
+ # The parsed scenarios for this runner's feature.
46
59
  #
60
+ # @api public
47
61
  def scenarios
48
62
  @scenarios ||= (data['elements'] || [])
49
63
  end
50
64
 
51
- # Runs this feature
65
+ # Runs this feature.
52
66
  #
67
+ # @return [true, false]
68
+ # Whether the run was successful or not.
69
+ #
70
+ # @api public
53
71
  def run
54
- reporter.feature(feature_name)
55
- failures = []
72
+ run_hook :before_run, data
73
+ feature.run_hook :before, data
74
+
56
75
  scenarios.each do |scenario|
57
76
  if !@scenario_line || scenario['line'].to_s == @scenario_line
58
- feature.send(:before)
59
- failure = Scenario.new(feature_name, feature, scenario, reporter).run
60
- failures << failure if failure
61
- feature.send(:after)
77
+ @success = Scenario.new(feature_name, feature, scenario).run
62
78
  end
63
79
  end
64
80
 
65
- unless failures.length.zero?
66
- reporter.error_summary(failures)
67
- return false
68
- else
69
- return true
70
- end
71
- end
81
+ feature.run_hook :after, data
82
+ run_hook :after_run, data
72
83
 
84
+ return !!@success
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -1,54 +1,69 @@
1
+ require 'hooks'
2
+
1
3
  module Spinach
2
4
  class Runner
3
5
  # A Scenario Runner handles a particular scenario run.
4
6
  #
5
7
  class Scenario
6
- attr_reader :name, :feature, :feature_name, :steps, :reporter
8
+ attr_reader :feature, :feature_name, :data
9
+
10
+ include Hooks
7
11
 
8
- # @param [Spinach::Feature] feature
9
- # the feature that contains the steps
12
+ define_hook :before_run
13
+ define_hook :on_successful_step
14
+ define_hook :on_failed_step
15
+ define_hook :on_error_step
16
+ define_hook :on_undefined_step
17
+ define_hook :on_skipped_step
18
+ define_hook :after_run
19
+
20
+ # @param [Feature] feature
21
+ # The feature that contains the steps.
10
22
  #
11
23
  # @param [Hash] data
12
- # the parsed feature data
13
- #
14
- # @param [Spinach::Reporter]
15
- # the reporter
24
+ # The parsed feature data.
16
25
  #
17
- def initialize(feature_name, feature, data, reporter)
26
+ # @api public
27
+ def initialize(feature_name, feature, data)
18
28
  @feature_name = feature_name
19
- @name = data['name']
20
- @steps = data['steps']
21
- @reporter = reporter
29
+ @data = data
22
30
  @feature = feature
23
31
  end
24
32
 
25
- # Runs this scenario and signals the reporter
26
- #
33
+ def steps
34
+ @steps ||= data['steps']
35
+ end
36
+
37
+ # Runs this scenario
38
+ # @return [True, False]
39
+ # true if this scenario succeeded, false if not
27
40
  def run
28
- reporter.scenario(name)
41
+ run_hook :before_run, data
42
+ feature.run_hook :before_scenario, data
29
43
  steps.each do |step|
30
- keyword = step['keyword'].strip
31
- name = step['name'].strip
32
- line = step['line']
33
- unless @failure
44
+ feature.run_hook :before_step, step
45
+ unless @exception
34
46
  begin
35
- feature.execute_step(name)
36
- reporter.step(keyword, name, :success)
47
+ feature.execute_step(step['name'])
48
+ run_hook :on_successful_step, step
37
49
  rescue MiniTest::Assertion => e
38
- @failure = [e, name, line, self]
39
- reporter.step(keyword, name, :failure)
50
+ @exception = e
51
+ run_hook :on_failed_step, step, @exception
40
52
  rescue Spinach::StepNotDefinedException => e
41
- @failure = [e, name, line, self]
42
- reporter.step(keyword, name, :undefined_step)
53
+ @exception = e
54
+ run_hook :on_undefined_step, step, @exception
43
55
  rescue StandardError => e
44
- @failure = [e, name, line, self]
45
- reporter.step(keyword, name, :error)
56
+ @exception = e
57
+ run_hook :on_error_step, step, @exception
46
58
  end
47
59
  else
48
- reporter.step(keyword, name, :skip)
60
+ run_hook :on_skipped_step, step
49
61
  end
62
+ feature.run_hook :after_step, step
50
63
  end
51
- @failure
64
+ feature.run_hook :after_scenario, data
65
+ run_hook :after_run, data
66
+ !@exception
52
67
  end
53
68
  end
54
69
  end