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.
- 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
|