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.
- data/Gemfile +2 -0
- data/README.markdown +18 -12
- data/features/background.feature +13 -0
- data/features/reporting/show_step_source_location.feature +11 -1
- data/features/steps/automatic_feature_generation.rb +7 -7
- data/features/steps/background.rb +30 -0
- data/features/steps/exit_status.rb +12 -13
- data/features/steps/feature_name_guessing.rb +7 -7
- data/features/steps/reporting/display_run_summary.rb +22 -22
- data/features/steps/reporting/error_reporting.rb +7 -7
- data/features/steps/reporting/show_step_source_location.rb +63 -14
- data/features/steps/reporting/undefined_feature_reporting.rb +7 -7
- data/features/steps/rspec_compatibility.rb +14 -14
- data/features/support/error_reporting.rb +5 -5
- data/features/support/filesystem.rb +71 -0
- data/features/support/spinach_runner.rb +5 -6
- data/lib/spinach.rb +11 -6
- data/lib/spinach/background.rb +11 -0
- data/lib/spinach/capybara.rb +7 -1
- data/lib/spinach/cli.rb +36 -13
- data/lib/spinach/config.rb +40 -6
- data/lib/spinach/dsl.rb +14 -11
- data/lib/spinach/exceptions.rb +1 -1
- data/lib/spinach/feature.rb +16 -0
- data/lib/spinach/frameworks.rb +2 -0
- data/lib/spinach/{suites → frameworks}/minitest.rb +0 -0
- data/lib/spinach/{suites → frameworks}/rspec.rb +0 -0
- data/lib/spinach/generators/feature_generator.rb +12 -23
- data/lib/spinach/generators/step_generator.rb +5 -5
- data/lib/spinach/hookable.rb +6 -4
- data/lib/spinach/hooks.rb +12 -4
- data/lib/spinach/parser.rb +6 -8
- data/lib/spinach/parser/visitor.rb +109 -0
- data/lib/spinach/reporter.rb +10 -6
- data/lib/spinach/reporter/stdout.rb +41 -16
- data/lib/spinach/reporter/stdout/error_reporting.rb +2 -2
- data/lib/spinach/runner.rb +9 -6
- data/lib/spinach/runner/feature_runner.rb +40 -34
- data/lib/spinach/runner/scenario_runner.rb +63 -36
- data/lib/spinach/scenario.rb +12 -0
- data/lib/spinach/step.rb +10 -0
- data/lib/spinach/version.rb +1 -1
- data/spinach.gemspec +3 -3
- data/test/spinach/background_test.rb +6 -0
- data/test/spinach/capybara_test.rb +30 -13
- data/test/spinach/cli_test.rb +46 -1
- data/test/spinach/config_test.rb +39 -0
- data/test/spinach/dsl_test.rb +12 -10
- data/test/spinach/feature_steps_test.rb +3 -3
- data/test/spinach/feature_test.rb +6 -0
- data/test/spinach/{suites → frameworks}/minitest_test.rb +2 -2
- data/test/spinach/generators/feature_generator_test.rb +18 -58
- data/test/spinach/generators/step_generator_test.rb +3 -3
- data/test/spinach/generators_test.rb +12 -10
- data/test/spinach/hookable_test.rb +8 -0
- data/test/spinach/hooks_test.rb +6 -7
- data/test/spinach/parser/visitor_test.rb +173 -0
- data/test/spinach/parser_test.rb +14 -27
- data/test/spinach/reporter/stdout/error_reporting_test.rb +9 -9
- data/test/spinach/reporter/stdout_test.rb +15 -19
- data/test/spinach/reporter_test.rb +15 -0
- data/test/spinach/runner/feature_runner_test.rb +79 -69
- data/test/spinach/runner/scenario_runner_test.rb +118 -92
- data/test/spinach/runner_test.rb +10 -6
- data/test/spinach/scenario_test.rb +6 -0
- data/test/spinach/step_test.rb +6 -0
- data/test/spinach_test.rb +7 -7
- metadata +60 -39
- data/lib/spinach/suites.rb +0 -2
data/lib/spinach/exceptions.rb
CHANGED
@@ -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
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
16
|
+
# an array of unique steps found in this feature, avoiding name
|
15
17
|
# repetition
|
16
18
|
def steps
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
@
|
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 [
|
8
|
-
#
|
9
|
-
def initialize(
|
10
|
-
@
|
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 "#{@
|
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
|
data/lib/spinach/hookable.rb
CHANGED
@@ -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 |
|
143
|
-
|
144
|
-
|
145
|
-
yield(
|
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
|
data/lib/spinach/parser.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'gherkin'
|
2
|
-
|
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
|
32
|
+
# Parses the feature file and returns a Feature.
|
35
33
|
#
|
36
|
-
# @return [
|
37
|
-
# The
|
34
|
+
# @return [Feature]
|
35
|
+
# The Feature.
|
38
36
|
#
|
39
37
|
# @api public
|
40
38
|
def parse
|
41
|
-
|
42
|
-
|
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
|
data/lib/spinach/reporter.rb
CHANGED
@@ -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 [
|
63
|
-
#
|
64
|
-
def set_current_feature(
|
65
|
-
@current_feature =
|
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(
|
78
|
-
@current_scenario =
|
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(
|
40
|
-
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(
|
50
|
-
@max_step_name_length =
|
51
|
-
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(
|
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 [
|
70
|
-
# The step
|
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
|
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
|
-
|
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 =
|
190
|
-
undefined_summary
|
191
|
-
failed_summary
|
192
|
-
error_summary
|
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
|
210
|
+
# The step.
|
200
211
|
#
|
201
212
|
def full_step(step)
|
202
|
-
"#{step
|
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
|