cuke_slicer 1.0.0 → 2.0.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/.travis.yml +7 -0
- data/Gemfile +2 -0
- data/History.rdoc +11 -0
- data/README.md +14 -1
- data/Rakefile +53 -49
- data/cuke_slicer.gemspec +1 -1
- data/features/step_definitions/action_steps.rb +12 -11
- data/features/step_definitions/setup_steps.rb +24 -0
- data/features/step_definitions/verification_steps.rb +11 -0
- data/features/test_case_extraction.feature +7 -1
- data/features/validation.feature +6 -0
- data/lib/cuke_slicer.rb +10 -10
- data/lib/cuke_slicer/collections/nested_tag_collection.rb +27 -0
- data/lib/cuke_slicer/collections/path_collection.rb +26 -0
- data/lib/cuke_slicer/collections/tag_collection.rb +28 -0
- data/lib/cuke_slicer/extractors/directory_extractor.rb +17 -0
- data/lib/cuke_slicer/extractors/file_extractor.rb +32 -0
- data/lib/cuke_slicer/filters/filter_set.rb +42 -0
- data/lib/cuke_slicer/helpers/extraction_helpers.rb +40 -0
- data/lib/cuke_slicer/helpers/filter_helpers.rb +60 -0
- data/lib/cuke_slicer/helpers/helpers.rb +21 -0
- data/lib/cuke_slicer/helpers/matching_helpers.rb +47 -0
- data/lib/cuke_slicer/slicer.rb +18 -194
- data/lib/cuke_slicer/version.rb +1 -1
- data/spec/slicer_integration_spec.rb +114 -43
- data/spec/slicer_unit_spec.rb +3 -3
- metadata +17 -10
@@ -0,0 +1,32 @@
|
|
1
|
+
require "cuke_slicer/helpers/matching_helpers"
|
2
|
+
require "cuke_slicer/helpers/filter_helpers"
|
3
|
+
require "cuke_slicer/helpers/extraction_helpers"
|
4
|
+
|
5
|
+
|
6
|
+
module CukeSlicer
|
7
|
+
class FileExtractor
|
8
|
+
|
9
|
+
include ExtractionHelpers
|
10
|
+
|
11
|
+
|
12
|
+
def extract(target, filters, format, &block)
|
13
|
+
Array.new.tap do |test_cases|
|
14
|
+
unless target.feature.nil?
|
15
|
+
tests = target.feature.tests
|
16
|
+
|
17
|
+
runnable_elements = extract_runnable_elements(extract_runnable_block_elements(tests, filters, &block))
|
18
|
+
|
19
|
+
runnable_elements.each do |element|
|
20
|
+
case
|
21
|
+
when format == :file_line
|
22
|
+
test_cases << "#{element.get_ancestor(:feature_file).path}:#{element.source_line}"
|
23
|
+
when format == :test_object
|
24
|
+
test_cases << element
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "cuke_slicer/helpers/helpers"
|
2
|
+
require "cuke_slicer/collections/tag_collection"
|
3
|
+
require "cuke_slicer/collections/path_collection"
|
4
|
+
|
5
|
+
|
6
|
+
module CukeSlicer
|
7
|
+
class FilterSet
|
8
|
+
|
9
|
+
include Helpers
|
10
|
+
|
11
|
+
|
12
|
+
def initialize filter_type, filter_value
|
13
|
+
self.filter_type = filter_type
|
14
|
+
self.filter_value = filter_value
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
block_unknown
|
19
|
+
block_invalid
|
20
|
+
|
21
|
+
if filter_value.is_a?(Array)
|
22
|
+
TagCollection.new(filter_value).validate if is_tag?(filter_type)
|
23
|
+
PathCollection.new(filter_value).validate if is_path?(filter_type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
|
31
|
+
def block_unknown
|
32
|
+
raise(ArgumentError, "Unknown filter '#{filter_type}'") unless CukeSlicer::Slicer.known_filters.include?(filter_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
def block_invalid
|
36
|
+
raise(ArgumentError, "Invalid filter '#{filter_value}'. Must be a String, Regexp, or Array thereof. Got #{filter_value.class}") unless str_regex_arr?(filter_value)
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :filter_type, :filter_value
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CukeSlicer
|
2
|
+
module ExtractionHelpers
|
3
|
+
|
4
|
+
include FilterHelpers
|
5
|
+
|
6
|
+
|
7
|
+
def extract_runnable_block_elements(things, filters, &block)
|
8
|
+
Array.new.tap do |elements|
|
9
|
+
things.each do |thing|
|
10
|
+
if thing.is_a?(CukeModeler::Outline)
|
11
|
+
elements.concat(thing.examples)
|
12
|
+
else
|
13
|
+
elements << thing
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
filter_excluded_paths(elements, filters[:excluded_paths])
|
18
|
+
filter_included_paths(elements, filters[:included_paths])
|
19
|
+
filter_excluded_tags(elements, filters[:excluded_tags])
|
20
|
+
filter_included_tags(elements, filters[:included_tags])
|
21
|
+
|
22
|
+
apply_custom_filter(elements, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_runnable_elements(things)
|
27
|
+
Array.new.tap do |elements|
|
28
|
+
things.each do |thing|
|
29
|
+
if thing.is_a?(CukeModeler::Example)
|
30
|
+
# Slicing in order to remove the parameter row element
|
31
|
+
elements.concat(thing.row_elements.slice(1, thing.row_elements.count - 1))
|
32
|
+
else
|
33
|
+
elements << thing
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CukeSlicer
|
2
|
+
module FilterHelpers
|
3
|
+
|
4
|
+
include MatchingHelpers
|
5
|
+
|
6
|
+
|
7
|
+
def apply_custom_filter(elements, &block)
|
8
|
+
if block
|
9
|
+
elements.reject! do |element|
|
10
|
+
block.call(element)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter_excluded_tags(elements, filters)
|
16
|
+
if filters
|
17
|
+
filters = [filters] unless filters.is_a?(Array)
|
18
|
+
|
19
|
+
unless filters.empty?
|
20
|
+
elements.reject! do |element|
|
21
|
+
matching_tag?(element, filters)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def filter_included_tags(elements, filters)
|
28
|
+
if filters
|
29
|
+
filters = [filters] unless filters.is_a?(Array)
|
30
|
+
|
31
|
+
elements.keep_if do |element|
|
32
|
+
matching_tag?(element, filters)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def filter_excluded_paths(elements, filters)
|
38
|
+
if filters
|
39
|
+
filters = [filters] unless filters.is_a?(Array)
|
40
|
+
|
41
|
+
elements.reject! do |element|
|
42
|
+
matching_path?(element, filters)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def filter_included_paths(elements, filters)
|
48
|
+
if filters
|
49
|
+
filters = [filters] unless filters.is_a?(Array)
|
50
|
+
|
51
|
+
unless filters.empty?
|
52
|
+
elements.keep_if do |element|
|
53
|
+
matching_path?(element, filters)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CukeSlicer
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def str_regex?(parameter)
|
5
|
+
parameter.is_a?(String) or parameter.is_a?(Regexp)
|
6
|
+
end
|
7
|
+
|
8
|
+
def str_regex_arr?(parameter)
|
9
|
+
parameter.is_a?(String) or parameter.is_a?(Regexp) or parameter.is_a?(Array)
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_tag?(parameter)
|
13
|
+
parameter.to_s =~ /tag/
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_path?(parameter)
|
17
|
+
parameter.to_s =~ /path/
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CukeSlicer
|
2
|
+
module MatchingHelpers
|
3
|
+
|
4
|
+
def matching_tag?(element, filters)
|
5
|
+
filters.each do |filter|
|
6
|
+
if filter.is_a?(Array)
|
7
|
+
filter_match = or_filter_match(element, filter)
|
8
|
+
else
|
9
|
+
filter_match = and_filter_match(element, filter)
|
10
|
+
end
|
11
|
+
|
12
|
+
return false unless filter_match
|
13
|
+
end
|
14
|
+
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def and_filter_match(element, filter)
|
19
|
+
filter_match(element, filter)
|
20
|
+
end
|
21
|
+
|
22
|
+
def or_filter_match(element, filters)
|
23
|
+
filters.any? do |filter|
|
24
|
+
filter_match(element, filter)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def filter_match(element, filter)
|
29
|
+
if filter.is_a?(Regexp)
|
30
|
+
element.all_tags.any? { |tag| tag =~ filter }
|
31
|
+
else
|
32
|
+
element.all_tags.include?(filter)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def matching_path?(element, filters)
|
37
|
+
filters.any? do |filtered_path|
|
38
|
+
if filtered_path.is_a?(Regexp)
|
39
|
+
element.get_ancestor(:feature_file).path =~ filtered_path
|
40
|
+
else
|
41
|
+
element.get_ancestor(:feature_file).path == filtered_path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/cuke_slicer/slicer.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
require "cuke_slicer/extractors/directory_extractor"
|
2
|
+
require "cuke_slicer/extractors/file_extractor"
|
3
|
+
require "cuke_slicer/filters/filter_set"
|
4
|
+
|
5
|
+
|
1
6
|
module CukeSlicer
|
2
7
|
|
3
8
|
# The object responsible for slicing up a Cucumber test suite into discrete test cases.
|
@@ -13,12 +18,16 @@ module CukeSlicer
|
|
13
18
|
# exposes the underlying modeling objects and knowledge of how they work is then required to make good use of the
|
14
19
|
# filter.
|
15
20
|
#
|
21
|
+
# Finally, the test cases can be provided as a collection of file:line strings or as a collection of the object
|
22
|
+
# types used to represent test cases by the underlying modeling library.
|
23
|
+
#
|
16
24
|
# @param target [String] the location that will be sliced up
|
17
25
|
# @param filters [Hash] the filters that will be applied to the sliced test cases
|
18
|
-
|
26
|
+
# @param format [Symbol] the type of output: :file_line or :test_object
|
27
|
+
def slice(target, filters = {}, format, &block)
|
19
28
|
validate_target(target)
|
20
29
|
validate_filters(filters)
|
21
|
-
|
30
|
+
validate_format(format)
|
22
31
|
|
23
32
|
begin
|
24
33
|
target = File.directory?(target) ? CukeModeler::Directory.new(target) : CukeModeler::FeatureFile.new(target)
|
@@ -27,9 +36,9 @@ module CukeSlicer
|
|
27
36
|
end
|
28
37
|
|
29
38
|
if target.is_a?(CukeModeler::Directory)
|
30
|
-
sliced_tests =
|
39
|
+
sliced_tests = DirectoryExtractor.new.extract(target, filters, format, &block)
|
31
40
|
else
|
32
|
-
sliced_tests =
|
41
|
+
sliced_tests = FileExtractor.new.extract(target, filters, format, &block)
|
33
42
|
end
|
34
43
|
|
35
44
|
sliced_tests
|
@@ -51,198 +60,13 @@ module CukeSlicer
|
|
51
60
|
raise(ArgumentError, "File or directory '#{target}' does not exist") unless File.exists?(target.to_s)
|
52
61
|
end
|
53
62
|
|
54
|
-
def validate_filters(
|
55
|
-
filter_sets.
|
56
|
-
|
57
|
-
raise(ArgumentError, "Invalid filter '#{filter_value}'. Must be a String, Regexp, or Array thereof. Got #{filter_value.class}") unless filter_value.is_a?(String) or filter_value.is_a?(Regexp) or filter_value.is_a?(Array)
|
58
|
-
|
59
|
-
if filter_value.is_a?(Array)
|
60
|
-
validate_tag_collection(filter_value) if filter_type.to_s =~ /tag/
|
61
|
-
validate_path_collection(filter_value) if filter_type.to_s =~ /path/
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def validate_tag_collection(filter_collection)
|
67
|
-
filter_collection.each do |filter|
|
68
|
-
raise(ArgumentError, "Filter '#{filter}' must be a String, Regexp, or Array. Got #{filter.class}") unless filter.is_a?(String) or filter.is_a?(Regexp) or filter.is_a?(Array)
|
69
|
-
|
70
|
-
validate_nested_tag_collection(filter) if filter.is_a?(Array)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def validate_nested_tag_collection(filter_collection)
|
75
|
-
filter_collection.each do |filter|
|
76
|
-
raise(ArgumentError, "Tag filters cannot be nested more than one level deep.") if filter.is_a?(Array)
|
77
|
-
raise(ArgumentError, "Filter '#{filter}' must be a String or Regexp. Got #{filter.class}") unless filter.is_a?(String) or filter.is_a?(Regexp)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def validate_path_collection(filter_collection)
|
82
|
-
filter_collection.each do |filter|
|
83
|
-
raise(ArgumentError, "Filter '#{filter}' must be a String or Regexp. Got #{filter.class}") unless filter.is_a?(String) or filter.is_a?(Regexp)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def extract_test_cases_from_directory(target, filters, &block)
|
88
|
-
entries = Dir.entries(target.path)
|
89
|
-
entries.delete '.'
|
90
|
-
entries.delete '..'
|
91
|
-
|
92
|
-
Array.new.tap do |test_cases|
|
93
|
-
entries.each do |entry|
|
94
|
-
entry = "#{target.path}/#{entry}"
|
95
|
-
|
96
|
-
case
|
97
|
-
when File.directory?(entry)
|
98
|
-
test_cases.concat(extract_test_cases_from_directory(CukeModeler::Directory.new(entry), filters, &block))
|
99
|
-
when entry =~ /\.feature$/
|
100
|
-
test_cases.concat(extract_test_cases_from_file(CukeModeler::FeatureFile.new(entry), filters, &block))
|
101
|
-
else
|
102
|
-
# Non-feature files are ignored
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def extract_test_cases_from_file(target, filters, &block)
|
109
|
-
Array.new.tap do |test_cases|
|
110
|
-
unless target.feature.nil?
|
111
|
-
tests = target.feature.tests
|
112
|
-
|
113
|
-
runnable_elements = extract_runnable_elements(extract_runnable_block_elements(tests, filters))
|
114
|
-
|
115
|
-
apply_custom_filter(runnable_elements, &block)
|
116
|
-
|
117
|
-
runnable_elements.each do |element|
|
118
|
-
test_cases << "#{element.get_ancestor(:feature_file).path}:#{element.source_line}"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def extract_runnable_block_elements(things, filters)
|
125
|
-
Array.new.tap do |elements|
|
126
|
-
things.each do |thing|
|
127
|
-
if thing.is_a?(CukeModeler::Outline)
|
128
|
-
elements.concat(thing.examples)
|
129
|
-
else
|
130
|
-
elements << thing
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
filter_excluded_paths(elements, filters[:excluded_paths])
|
135
|
-
filter_included_paths(elements, filters[:included_paths])
|
136
|
-
filter_excluded_tags(elements, filters[:excluded_tags])
|
137
|
-
filter_included_tags(elements, filters[:included_tags])
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def extract_runnable_elements(things)
|
142
|
-
Array.new.tap do |elements|
|
143
|
-
things.each do |thing|
|
144
|
-
if thing.is_a?(CukeModeler::Example)
|
145
|
-
# Slicing in order to remove the parameter row element
|
146
|
-
elements.concat(thing.row_elements.slice(1, thing.row_elements.count - 1))
|
147
|
-
else
|
148
|
-
elements << thing
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def apply_custom_filter(elements, &block)
|
155
|
-
if block
|
156
|
-
elements.reject! do |element|
|
157
|
-
block.call(element)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def filter_excluded_tags(elements, filters)
|
163
|
-
if filters
|
164
|
-
filters = [filters] unless filters.is_a?(Array)
|
165
|
-
|
166
|
-
unless filters.empty?
|
167
|
-
elements.reject! do |element|
|
168
|
-
matching_tag?(element, filters)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def filter_included_tags(elements, filters)
|
175
|
-
if filters
|
176
|
-
filters = [filters] unless filters.is_a?(Array)
|
177
|
-
|
178
|
-
elements.keep_if do |element|
|
179
|
-
matching_tag?(element, filters)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def filter_excluded_paths(elements, filters)
|
185
|
-
if filters
|
186
|
-
filters = [filters] unless filters.is_a?(Array)
|
187
|
-
|
188
|
-
elements.reject! do |element|
|
189
|
-
matching_path?(element, filters)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
def filter_included_paths(elements, filters)
|
195
|
-
if filters
|
196
|
-
filters = [filters] unless filters.is_a?(Array)
|
197
|
-
|
198
|
-
unless filters.empty?
|
199
|
-
elements.keep_if do |element|
|
200
|
-
matching_path?(element, filters)
|
201
|
-
end
|
202
|
-
end
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def matching_tag?(element, filters)
|
207
|
-
filters.each do |filter|
|
208
|
-
if filter.is_a?(Array)
|
209
|
-
filter_match = or_filter_match(element, filter)
|
210
|
-
else
|
211
|
-
filter_match = and_filter_match(element, filter)
|
212
|
-
end
|
213
|
-
|
214
|
-
return false unless filter_match
|
215
|
-
end
|
216
|
-
|
217
|
-
true
|
218
|
-
end
|
219
|
-
|
220
|
-
def and_filter_match(element, filter)
|
221
|
-
filter_match(element, filter)
|
63
|
+
def validate_filters(filters)
|
64
|
+
filter_sets = filters.map { |filter_type, value| FilterSet.new(filter_type, value) }
|
65
|
+
filter_sets.each(&:validate)
|
222
66
|
end
|
223
67
|
|
224
|
-
def
|
225
|
-
|
226
|
-
filter_match(element, filter)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def filter_match(element, filter)
|
231
|
-
if filter.is_a?(Regexp)
|
232
|
-
element.all_tags.any? { |tag| tag =~ filter }
|
233
|
-
else
|
234
|
-
element.all_tags.include?(filter)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
def matching_path?(element, filters)
|
239
|
-
filters.any? do |filtered_path|
|
240
|
-
if filtered_path.is_a?(Regexp)
|
241
|
-
element.get_ancestor(:feature_file).path =~ filtered_path
|
242
|
-
else
|
243
|
-
element.get_ancestor(:feature_file).path == filtered_path
|
244
|
-
end
|
245
|
-
end
|
68
|
+
def validate_format(format)
|
69
|
+
raise(ArgumentError, "Invalid Output Format: #{format}") unless [:test_object, :file_line].include?(format)
|
246
70
|
end
|
247
71
|
|
248
72
|
end
|