spinach 0.5.2 → 0.6.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.
- 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)
|