spinach 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/Gemfile +1 -1
- data/README.markdown +158 -112
- data/bin/spinach +0 -1
- data/features/before_and_after_hooks.feature +10 -0
- data/features/before_and_after_hooks_inheritance.feature +12 -0
- data/features/reporting/customized_reporter.feature +9 -0
- data/features/reporting/show_step_source_location.feature +1 -1
- data/features/steps/before_and_after_hooks.rb +21 -0
- data/features/steps/before_and_after_hooks_inheritance.rb +43 -0
- data/features/steps/reporting/use_customized_reporter.rb +98 -0
- data/lib/spinach.rb +1 -0
- data/lib/spinach/cli.rb +12 -20
- data/lib/spinach/config.rb +22 -1
- data/lib/spinach/dsl.rb +82 -0
- data/lib/spinach/feature_steps.rb +3 -0
- data/lib/spinach/helpers.rb +16 -0
- data/lib/spinach/runner.rb +9 -0
- data/lib/spinach/runner/feature_runner.rb +1 -1
- data/lib/spinach/runner/scenario_runner.rb +2 -0
- data/lib/spinach/tags_matcher.rb +15 -8
- data/lib/spinach/version.rb +1 -1
- data/spinach.gemspec +2 -2
- data/test/spinach/cli_test.rb +70 -18
- data/test/spinach/config_test.rb +74 -63
- data/test/spinach/dsl_test.rb +108 -0
- data/test/spinach/feature_steps_test.rb +8 -0
- data/test/spinach/helpers_test.rb +9 -0
- data/test/spinach/runner/feature_runner_test.rb +19 -1
- data/test/spinach/runner/scenario_runner_test.rb +28 -5
- data/test/spinach/runner_test.rb +41 -0
- metadata +101 -31
@@ -0,0 +1,43 @@
|
|
1
|
+
class BeforeAndAfterHooksInheritanceBase < Spinach::FeatureSteps
|
2
|
+
class << self
|
3
|
+
attr_accessor :var1
|
4
|
+
attr_accessor :var2
|
5
|
+
end
|
6
|
+
|
7
|
+
before do
|
8
|
+
if self.class.var2.nil?
|
9
|
+
self.class.var2 = :i_am_here
|
10
|
+
else
|
11
|
+
self.class.var2 = :dirty
|
12
|
+
end
|
13
|
+
if self.class.var1.nil?
|
14
|
+
self.class.var1 = :clean
|
15
|
+
else
|
16
|
+
self.class.var1 = :dirty
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
after do
|
21
|
+
self.class.var1 = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class BeforeAndAfterHooksInheritance < BeforeAndAfterHooksInheritanceBase
|
27
|
+
before do
|
28
|
+
self.class.var1 = :in_subclass
|
29
|
+
end
|
30
|
+
|
31
|
+
after do
|
32
|
+
self.class.var2 = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
Then 'I can see the variable being overridden in the subclass' do
|
36
|
+
self.class.var1.must_equal :in_subclass
|
37
|
+
end
|
38
|
+
|
39
|
+
Then "I can see the variable setup in the super class before hook" do
|
40
|
+
self.class.var2.must_equal :i_am_here
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
class UseCustomizedReporter < Spinach::FeatureSteps
|
2
|
+
|
3
|
+
feature "Use customized reporter"
|
4
|
+
|
5
|
+
include Integration::SpinachRunner
|
6
|
+
|
7
|
+
before do
|
8
|
+
class_str = <<-EOF
|
9
|
+
class Spinach::TestReporter < Spinach::Reporter
|
10
|
+
attr_reader :out, :error
|
11
|
+
attr_accessor :scenario_error
|
12
|
+
attr_accessor :scenario
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
super(*args)
|
16
|
+
@out = options[:output] || $stdout
|
17
|
+
@error = options[:error] || $stderr
|
18
|
+
@max_step_name_length = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def before_feature_run(feature)
|
22
|
+
out.puts "The customized class"
|
23
|
+
end
|
24
|
+
|
25
|
+
def before_scenario_run(scenario, step_definitions = nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_scenario_run(scenario, step_definitions = nil)
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_successful_step(step, step_location, step_definitions = nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_failed_step(step, failure, step_location, step_definitions = nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_error_step(step, failure, step_location, step_definitions = nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_undefined_step(step, failure, step_definitions = nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_pending_step(step, failure)
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_feature_not_found(feature)
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_skipped_step(step, step_definitions = nil)
|
50
|
+
end
|
51
|
+
|
52
|
+
def output_step(symbol, step, color, step_location = nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
def after_run(success)
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_summary
|
59
|
+
end
|
60
|
+
|
61
|
+
def full_step(step)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
EOF
|
66
|
+
|
67
|
+
write_file "test_reporter.rb", class_str
|
68
|
+
end
|
69
|
+
|
70
|
+
Given 'I have a feature that has no error or failure' do
|
71
|
+
write_file('features/success_feature.feature', """
|
72
|
+
Feature: A success feature
|
73
|
+
|
74
|
+
Scenario: This is scenario will succeed
|
75
|
+
Then I succeed
|
76
|
+
""")
|
77
|
+
|
78
|
+
write_file('features/steps/success_feature.rb',
|
79
|
+
<<-EOF
|
80
|
+
require_relative "../../test_reporter"
|
81
|
+
class ASuccessFeature < Spinach::FeatureSteps
|
82
|
+
feature "A success feature"
|
83
|
+
Then "I succeed" do
|
84
|
+
end
|
85
|
+
end
|
86
|
+
EOF
|
87
|
+
)
|
88
|
+
@feature = "features/success_feature.feature"
|
89
|
+
end
|
90
|
+
|
91
|
+
When 'I run it using the new reporter' do
|
92
|
+
run_feature @feature, append: "-r Spinach::TestReporter"
|
93
|
+
end
|
94
|
+
|
95
|
+
Then 'I see the desired output' do
|
96
|
+
@stdout.must_include("The customized class")
|
97
|
+
end
|
98
|
+
end
|
data/lib/spinach.rb
CHANGED
data/lib/spinach/cli.rb
CHANGED
@@ -20,17 +20,10 @@ module Spinach
|
|
20
20
|
#
|
21
21
|
# @api public
|
22
22
|
def run
|
23
|
+
options
|
23
24
|
Spinach::Runner.new(feature_files).run
|
24
25
|
end
|
25
26
|
|
26
|
-
# Inits the reporter with a default one.
|
27
|
-
#
|
28
|
-
# @api public
|
29
|
-
def init_reporter
|
30
|
-
reporter = Spinach::Reporter::Stdout.new(options[:reporter])
|
31
|
-
reporter.bind
|
32
|
-
end
|
33
|
-
|
34
27
|
# @return [Hash]
|
35
28
|
# A hash of options separated by its type.
|
36
29
|
#
|
@@ -52,8 +45,6 @@ module Spinach
|
|
52
45
|
#
|
53
46
|
# @api private
|
54
47
|
def parse_options
|
55
|
-
reporter_options = {}
|
56
|
-
reporter_options[:backtrace] = false
|
57
48
|
config = {}
|
58
49
|
|
59
50
|
begin
|
@@ -65,7 +56,7 @@ module Spinach
|
|
65
56
|
|
66
57
|
opts.on('-b', '--backtrace',
|
67
58
|
'Show backtrace of errors') do |show_backtrace|
|
68
|
-
|
59
|
+
config[:reporter_options] = {backtrace: show_backtrace}
|
69
60
|
end
|
70
61
|
|
71
62
|
opts.on('-t', '--tags TAG',
|
@@ -73,13 +64,7 @@ module Spinach
|
|
73
64
|
config[:tags] ||= []
|
74
65
|
tags = tag.delete('@').split(',')
|
75
66
|
|
76
|
-
|
77
|
-
tag_groups.any? { |tag_group|
|
78
|
-
tag_group.any? { |tag| tag =~ /wip$/ }
|
79
|
-
}
|
80
|
-
}
|
81
|
-
|
82
|
-
unless references_wip.(config[:tags]) || references_wip.([tags])
|
67
|
+
if (config[:tags] + tags).flatten.none? { |t| t =~ /wip$/ }
|
83
68
|
tags.unshift '~wip'
|
84
69
|
end
|
85
70
|
|
@@ -100,16 +85,23 @@ module Spinach
|
|
100
85
|
'Path where your features will be searched for') do |path|
|
101
86
|
config[:features_path] = path
|
102
87
|
end
|
88
|
+
|
89
|
+
opts.on('-r', '--reporter CLASS_NAME',
|
90
|
+
'Formatter class name') do |class_name|
|
91
|
+
config[:reporter_class] = class_name
|
92
|
+
end
|
103
93
|
end.parse!(@args)
|
104
94
|
|
105
95
|
Spinach.config.parse_from_file
|
106
96
|
config.each{|k,v| Spinach.config[k] = v}
|
97
|
+
if Spinach.config.tags.empty? ||
|
98
|
+
Spinach.config.tags.flatten.none?{ |t| t =~ /wip$/ }
|
99
|
+
Spinach.config.tags.unshift ['~wip']
|
100
|
+
end
|
107
101
|
rescue OptionParser::ParseError => exception
|
108
102
|
puts exception.message.capitalize
|
109
103
|
exit 1
|
110
104
|
end
|
111
|
-
|
112
|
-
{reporter: reporter_options}
|
113
105
|
end
|
114
106
|
|
115
107
|
# Uses given args to list the feature files to run. It will find a single
|
data/lib/spinach/config.rb
CHANGED
@@ -29,7 +29,10 @@ module Spinach
|
|
29
29
|
:failure_exceptions,
|
30
30
|
:config_path,
|
31
31
|
:tags,
|
32
|
-
:save_and_open_page_on_failure
|
32
|
+
:save_and_open_page_on_failure,
|
33
|
+
:reporter_class,
|
34
|
+
:reporter_options
|
35
|
+
|
33
36
|
|
34
37
|
# The "features path" holds the place where your features will be
|
35
38
|
# searched for. Defaults to 'features'
|
@@ -42,6 +45,24 @@ module Spinach
|
|
42
45
|
@features_path || 'features'
|
43
46
|
end
|
44
47
|
|
48
|
+
# The "reporter class" holds the reporter class name
|
49
|
+
# Default to Spinach::Reporter::Stdout
|
50
|
+
#
|
51
|
+
# @return [reporter object]
|
52
|
+
# The reporter that responds to specific messages.
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def reporter_class
|
56
|
+
@reporter_class || "Spinach::Reporter::Stdout"
|
57
|
+
end
|
58
|
+
|
59
|
+
# The "reporter_options" holds the options of reporter_class
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def reporter_options
|
63
|
+
@reporter_options || {}
|
64
|
+
end
|
65
|
+
|
45
66
|
# The "step definitions path" holds the place where your feature step
|
46
67
|
# classes will be searched for. Defaults to '#{features_path}/steps'
|
47
68
|
#
|
data/lib/spinach/dsl.rb
CHANGED
@@ -58,6 +58,69 @@ module Spinach
|
|
58
58
|
alias_method :Then, :step
|
59
59
|
alias_method :And, :step
|
60
60
|
alias_method :But, :step
|
61
|
+
|
62
|
+
# Defines a before hook for each scenario. The scope is limited only to the current
|
63
|
+
# step class (thus the current feature).
|
64
|
+
#
|
65
|
+
# When a scenario is executed, the before each block will be run first before any steps
|
66
|
+
#
|
67
|
+
# User can define multiple before blocks throughout the class hierarchy and they are chained
|
68
|
+
# through the inheritance chain when executing
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
#
|
72
|
+
# class MySpinach::Base< Spinach::FeatureSteps
|
73
|
+
# before do
|
74
|
+
# @var1 = 30
|
75
|
+
# @var2 = 40
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# class MyFeature < MySpinach::Base
|
80
|
+
# before do
|
81
|
+
# self.original_session_timeout = 1000
|
82
|
+
# change_session_timeout_to(1)
|
83
|
+
# @var2 = 50
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# When running a scenario in MyFeature, @var1 is 30 and @var2 is 50
|
88
|
+
#
|
89
|
+
# @api public
|
90
|
+
def before(&block)
|
91
|
+
define_before_or_after_method_with_block(:before, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Defines a after hook for each scenario. The scope is limited only to the current
|
95
|
+
# step class (thus the current feature).
|
96
|
+
#
|
97
|
+
# When a scenario is executed, the after each block will be run after any steps
|
98
|
+
#
|
99
|
+
# User can define multiple after blocks throughout the class hierarchy and they are chained
|
100
|
+
# through the inheritance chain when executing.
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
#
|
104
|
+
# class MySpinach::Base < Spinach::FeatureSteps
|
105
|
+
# after do
|
106
|
+
# @var1 = 30
|
107
|
+
# @var2 = 40
|
108
|
+
# end
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# class MyFeature < MySpinach::Base
|
112
|
+
# after do
|
113
|
+
# change_session_timeout_to(original_session_timeout)
|
114
|
+
# @var2 = 50
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# When running a scenario in MyFeature, @var1 is 30 and @var2 is 50
|
119
|
+
#
|
120
|
+
# @api public
|
121
|
+
def after(&block)
|
122
|
+
define_before_or_after_method_with_block(:after, &block)
|
123
|
+
end
|
61
124
|
|
62
125
|
# Sets the feature name.
|
63
126
|
#
|
@@ -74,6 +137,25 @@ module Spinach
|
|
74
137
|
@feature_name = name
|
75
138
|
end
|
76
139
|
|
140
|
+
private
|
141
|
+
|
142
|
+
def before_or_after_private_method_name(location)
|
143
|
+
hash_value = hash
|
144
|
+
class_name = self.name || ""
|
145
|
+
class_name = class_name.gsub("::", "__").downcase
|
146
|
+
private_method_name = "_#{location}_each_block_#{hash.abs}_#{class_name}" #uniqueness
|
147
|
+
end
|
148
|
+
|
149
|
+
def define_before_or_after_method_with_block(location, &block)
|
150
|
+
define_method(before_or_after_private_method_name(location), &block)
|
151
|
+
private before_or_after_private_method_name(location)
|
152
|
+
private_method_name = before_or_after_private_method_name location
|
153
|
+
define_method "#{location}_each" do
|
154
|
+
super()
|
155
|
+
send(private_method_name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
77
159
|
end
|
78
160
|
|
79
161
|
# Instance methods to include in the host class.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Spinach
|
2
|
+
module Helpers
|
3
|
+
class << self
|
4
|
+
def constantize(string)
|
5
|
+
names = string.split('::')
|
6
|
+
names.shift if names.empty? || names.first.empty?
|
7
|
+
|
8
|
+
constant = Object
|
9
|
+
names.each do |name|
|
10
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
11
|
+
end
|
12
|
+
constant
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/spinach/runner.rb
CHANGED
@@ -46,6 +46,14 @@ module Spinach
|
|
46
46
|
# The default path where the support files are located
|
47
47
|
attr_reader :support_path
|
48
48
|
|
49
|
+
# Inits the reporter with a default one.
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def init_reporter
|
53
|
+
reporter = Helpers.constantize(Spinach.config[:reporter_class]).new(Spinach.config.reporter_options)
|
54
|
+
reporter.bind
|
55
|
+
end
|
56
|
+
|
49
57
|
# Runs this runner and outputs the results in a colorful manner.
|
50
58
|
#
|
51
59
|
# @return [true, false]
|
@@ -55,6 +63,7 @@ module Spinach
|
|
55
63
|
def run
|
56
64
|
require_dependencies
|
57
65
|
require_frameworks
|
66
|
+
init_reporter
|
58
67
|
|
59
68
|
Spinach.hooks.run_before_run
|
60
69
|
|
@@ -74,7 +74,7 @@ module Spinach
|
|
74
74
|
|
75
75
|
def run_scenario?(scenario, current_scenario_index)
|
76
76
|
match_line(current_scenario_index) &&
|
77
|
-
TagsMatcher.match(
|
77
|
+
TagsMatcher.match(feature_tags + scenario.tags)
|
78
78
|
end
|
79
79
|
|
80
80
|
def match_line(current_scenario_index)
|
@@ -47,6 +47,7 @@ module Spinach
|
|
47
47
|
scenario_run = false
|
48
48
|
Spinach.hooks.run_around_scenario @scenario, step_definitions do
|
49
49
|
scenario_run = true
|
50
|
+
step_definitions.before_each
|
50
51
|
steps.each do |step|
|
51
52
|
Spinach.hooks.run_before_step step, step_definitions
|
52
53
|
|
@@ -58,6 +59,7 @@ module Spinach
|
|
58
59
|
|
59
60
|
Spinach.hooks.run_after_step step, step_definitions
|
60
61
|
end
|
62
|
+
step_definitions.after_each
|
61
63
|
end
|
62
64
|
raise "around_scenario hooks *must* yield" if !scenario_run && !@exception
|
63
65
|
Spinach.hooks.run_after_scenario @scenario, step_definitions
|
data/lib/spinach/tags_matcher.rb
CHANGED
@@ -17,7 +17,10 @@ module Spinach
|
|
17
17
|
def match(tags)
|
18
18
|
return true if tag_groups.empty?
|
19
19
|
|
20
|
-
tag_groups.all? { |tag_group|
|
20
|
+
tag_groups.all? { |tag_group|
|
21
|
+
res = match_tag_group(Array(tag_group), tags)
|
22
|
+
res
|
23
|
+
}
|
21
24
|
end
|
22
25
|
|
23
26
|
private
|
@@ -28,16 +31,20 @@ module Spinach
|
|
28
31
|
|
29
32
|
def match_tag_group(tag_group, tags)
|
30
33
|
matched_tags = tag_group.select { |tag| !tag_negated?(tag) }
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
matched = if matched_tags.empty?
|
35
|
+
true
|
36
|
+
else
|
37
|
+
!tags.empty? && matched_tags.any? { |tag| tags.include?(tag) }
|
38
|
+
end
|
35
39
|
|
36
40
|
negated_tags = tag_group.select { |tag| tag_negated? tag }
|
37
|
-
negated =
|
41
|
+
negated = if tags.empty?
|
42
|
+
false
|
43
|
+
else
|
44
|
+
negated_tags.any? {|tag| tags.include?(tag.delete(NEGATION_SIGN))}
|
45
|
+
end
|
38
46
|
|
39
|
-
|
40
|
-
false
|
47
|
+
!negated && matched
|
41
48
|
end
|
42
49
|
|
43
50
|
def tag_negated?(tag)
|