spinach 0.0.6 → 0.1.0

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