cuke_slicer 1.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.
@@ -0,0 +1,45 @@
1
+ Feature: Tag logic
2
+
3
+ Normally, tag filters are applied using a logical AND. That is, a tag filter will trigger if a
4
+ test case's tags match ALL of the provided tag filters. If desired, the tag filters can be
5
+ evaluated using a logical OR and, further, the two kinds of evaluation can be combined.
6
+
7
+ Note: All tag filters are 'inclusive' and strings for the purposes of these examples but the same
8
+ logic notation and limitations apply for exclusive or regular expression tag filters.
9
+
10
+
11
+ Background:
12
+ And the following feature file "a_test.feature":
13
+ """
14
+ Feature:
15
+
16
+ @a
17
+ Scenario: test a
18
+ @b
19
+ Scenario: test b
20
+ @a @b
21
+ Scenario: test ab
22
+ @c
23
+ Scenario: test c
24
+ @d
25
+ Scenario: test d
26
+ @c @d
27
+ Scenario: test cd
28
+ """
29
+
30
+
31
+ Scenario Outline: ANDing and ORing tags
32
+ Tags that are grouped together will be OR'd together before the remaining AND evaluation occurs
33
+
34
+ When test cases are extracted from "a_test.feature" using "<tag filters>"
35
+ Then "<test cases>" are found
36
+ Examples:
37
+ | tag filters | test cases |
38
+ | '@a' | path/to/a_test.feature:4, path/to/a_test.feature:8 |
39
+ | ['@a'] | path/to/a_test.feature:4, path/to/a_test.feature:8 |
40
+ | '@a','@b' | path/to/a_test.feature:8 |
41
+ | ['@a','@b'] | path/to/a_test.feature:4, path/to/a_test.feature:6, path/to/a_test.feature:8 |
42
+
43
+ Scenario: Complex logic
44
+ When test cases are extracted from "a_test.feature" using "['@a','@c'],['@b','@d']"
45
+ Then "path/to/a_test.feature:8, path/to/a_test.feature:14" are found
@@ -0,0 +1,78 @@
1
+ Feature: Test case extraction
2
+
3
+ Test cases can be extracted from a source file or directory as a collection of 'file:line' items that
4
+ can then be conveniently arranged for consumption by some other tool (e.g. Cucumber).
5
+
6
+
7
+ Scenario: Extraction from a file
8
+ Given the following feature file "a_test.feature":
9
+ """
10
+ Feature: A test feature
11
+
12
+ Scenario: Test 1
13
+ * some steps
14
+
15
+ Scenario Outline: Test 2
16
+ * some steps
17
+ Examples: Block 1
18
+ | param | value |
19
+ | a | 1 |
20
+ | b | 2 |
21
+ Examples: Block 2
22
+ | param | value |
23
+ | c | 3 |
24
+ """
25
+ When test cases are extracted from it
26
+ Then the following test cases are found
27
+ | path/to/a_test.feature:3 |
28
+ | path/to/a_test.feature:10 |
29
+ | path/to/a_test.feature:11 |
30
+ | path/to/a_test.feature:14 |
31
+
32
+ Scenario: Extraction from 'empty' files
33
+ Given the following feature file "empty.feature":
34
+ """
35
+ Feature: Nothing here yet
36
+ """
37
+ And the following feature file "really_empty.feature":
38
+ """
39
+ """
40
+ When test cases are extracted from them
41
+ Then no test cases are found
42
+
43
+ Scenario: Extraction from a directory
44
+ Given the directory "test_directory"
45
+ And the following feature file "a_test.feature":
46
+ """
47
+ Feature: A test feature
48
+
49
+ Scenario Outline: Test 1
50
+ * some steps
51
+ Examples:
52
+ | param | value |
53
+ | a | 1 |
54
+ | b | 2 |
55
+ """
56
+ And the directory "test_directory/nested_directory"
57
+ And the following feature file "another_test.feature":
58
+ """
59
+ Feature: Another test feature
60
+
61
+ Scenario: Test 2
62
+ * some steps
63
+ """
64
+ When test cases are extracted from "test_directory"
65
+ Then the following test cases are found
66
+ | path/to/test_directory/a_test.feature:7 |
67
+ | path/to/test_directory/a_test.feature:8 |
68
+ | path/to/test_directory/nested_directory/another_test.feature:3 |
69
+
70
+ Scenario: Extraction from 'empty' directories
71
+ Given the directory "test_directory"
72
+ And the following feature file "empty.feature":
73
+ """
74
+ Feature: WIP
75
+ """
76
+ And the directory "test_directory/empty_directory"
77
+ When test cases are extracted from "test_directory"
78
+ Then no test cases are found
@@ -0,0 +1,130 @@
1
+ Feature: Test case filtering
2
+
3
+ The test cases that are extracted by the slicer can be narrowed down by applying various filters
4
+ that will eliminate results. Exclusive filters will filter out any test case that matches the
5
+ filter, whereas inclusive filters will filter out non-matching test cases. Filters can be strings,
6
+ regular expressions, or a custom code block and they can be combined as desired.
7
+
8
+
9
+ Background:
10
+ Given the directory "test_directory"
11
+ And the following feature file "a_test.feature":
12
+ """
13
+ @feature_tag_1
14
+ Feature: The first feature
15
+
16
+ @test_tag_1
17
+ Scenario: a scenario
18
+ * a step
19
+
20
+ @test_tag_2
21
+ Scenario Outline: an outline
22
+ * a step
23
+ @example_tag_1
24
+ Examples: example set 1
25
+ | param |
26
+ | value 1 |
27
+ @example_tag_2
28
+ Examples: example set 2
29
+ | param |
30
+ | value 2 |
31
+ """
32
+ And the directory "test_directory/nested_directory"
33
+ And the following feature file "another_test.feature":
34
+ """
35
+ @feature_tag_2
36
+ Feature: The second feature
37
+
38
+ @test_tag_3
39
+ Scenario: another scenario
40
+ * a step
41
+ """
42
+ And the following feature file "a_third_test.feature":
43
+ """
44
+ @feature_tag_3
45
+ Feature: The third feature
46
+
47
+ @test_tag_4
48
+ Scenario Outline: another outline
49
+ * a step
50
+ @example_tag_3
51
+ Examples: example set 3
52
+ | param |
53
+ | value 3 |
54
+ @example_tag_4
55
+ Examples: example set 4
56
+ | param |
57
+ | value 4 |
58
+ """
59
+
60
+ Scenario: Filtering by excluded tags
61
+ When test cases are extracted from "test_directory" using the following exclusive tag filters:
62
+ | @feature_tag_1 |
63
+ | /example/ |
64
+ Then the following test cases are found
65
+ | path/to/test_directory/a_test.feature:5 |
66
+ | path/to/test_directory/nested_directory/another_test.feature:5 |
67
+ | path/to/test_directory/nested_directory/a_third_test.feature:10 |
68
+ | path/to/test_directory/nested_directory/a_third_test.feature:14 |
69
+
70
+ Scenario: Filtering by included tags
71
+ When test cases are extracted from "test_directory" using the following inclusive tag filters:
72
+ | @feature_tag_1 |
73
+ | /example/ |
74
+ Then the following test cases are found
75
+ | path/to/test_directory/a_test.feature:14 |
76
+ | path/to/test_directory/a_test.feature:18 |
77
+
78
+ Scenario: Filter by included path
79
+ When test cases are extracted from "test_directory" using the following inclusive path filters:
80
+ | path/to/test_directory/nested_directory/another_test.feature |
81
+ | /third/ |
82
+ Then the following test cases are found
83
+ | path/to/test_directory/nested_directory/another_test.feature:5 |
84
+ | path/to/test_directory/nested_directory/a_third_test.feature:10 |
85
+ | path/to/test_directory/nested_directory/a_third_test.feature:14 |
86
+
87
+ Scenario: Filter by excluded path
88
+ When test cases are extracted from "test_directory" using the following exclusive path filters:
89
+ | path/to/test_directory/nested_directory/another_test.feature |
90
+ | /third/ |
91
+ Then the following test cases are found
92
+ | path/to/test_directory/a_test.feature:5 |
93
+ | path/to/test_directory/a_test.feature:14 |
94
+ | path/to/test_directory/a_test.feature:18 |
95
+
96
+ Scenario: Custom filtering
97
+
98
+ A custom filter will filter out any test case for which the provided code block evaluates to
99
+ true. There are not separate exclusive and inclusive versions of custom filters since a negation
100
+ can be included in the code block to achieve the same effect.
101
+
102
+ When test cases are extracted from "test_directory" using the following custom filter:
103
+ """
104
+ { |test_case|
105
+ test_case.is_a?(CukeModeler::Scenario) and
106
+ test_case.get_ancestor(:feature).name =~ /first/
107
+ }
108
+ """
109
+ Then the following test cases are found
110
+ | path/to/test_directory/a_test.feature:14 |
111
+ | path/to/test_directory/a_test.feature:18 |
112
+ | path/to/test_directory/nested_directory/another_test.feature:5 |
113
+ | path/to/test_directory/nested_directory/a_third_test.feature:10 |
114
+ | path/to/test_directory/nested_directory/a_third_test.feature:14 |
115
+
116
+ Scenario: Combining filters
117
+ Given the following tag filters:
118
+ | filter type | filter |
119
+ | included | @feature_tag_1 |
120
+ And the following path filters:
121
+ | filter type | filter |
122
+ | excluded | /nested/ |
123
+ And the following custom filter:
124
+ """
125
+ { |test_case| test_case.is_a?(CukeModeler::Scenario) }
126
+ """
127
+ When test cases are extracted from "test_directory"
128
+ Then the following test cases are found
129
+ | path/to/test_directory/a_test.feature:14 |
130
+ | path/to/test_directory/a_test.feature:18 |
@@ -0,0 +1,56 @@
1
+ Feature: Validation
2
+
3
+ The slicing process will gracefully handle some of the more common problem use cases.
4
+
5
+
6
+ Scenario: Valid filters
7
+ * The following filter types are valid:
8
+ | included_tags |
9
+ | excluded_tags |
10
+ | included_paths |
11
+ | excluded_paths |
12
+
13
+ Scenario: Using an unknown filter type
14
+ Given a slicer
15
+ When it tries to extract test cases using an unknown filter type
16
+ Then an error indicating that the filter type is unknown will be triggered
17
+
18
+ Scenario: Using an invalid filter
19
+
20
+ Note: Filters can only be strings, regular expressions, or collections thereof.
21
+
22
+ Given a slicer
23
+ When it tries to extract test cases using an invalid filter
24
+ Then an error indicating that the filter is invalid will be triggered
25
+
26
+ Scenario: Trying to slice a non-existent file
27
+ Given the file "a_test.feature" does not exist
28
+ When test cases are extracted from it
29
+ Then an error indicating that the file does not exist will be triggered
30
+
31
+ Scenario: Trying to slice a non-existent directory
32
+ Given the directory "test_directory" does not exist
33
+ When test cases are extracted from it
34
+ Then an error indicating that the directory does not exist will be triggered
35
+
36
+ Scenario: Trying to slice an invalid feature file
37
+ And the following feature file:
38
+ """
39
+ This does not look like a feature file
40
+ """
41
+ When test cases are extracted from it
42
+ Then an error indicating that the feature file is invalid will be triggered
43
+
44
+ Scenario: Non-feature files in sliced directories are ignored
45
+ Given the directory "test_directory"
46
+ Given the following feature file "a_test.feature":
47
+ """
48
+ Feature: A test feature
49
+
50
+ Scenario: Test 1
51
+ * some steps
52
+ """
53
+ And the file "not_a_feature.file"
54
+ When test cases are extracted from "test_directory"
55
+ Then the following test cases are found
56
+ | path/to/test_directory/a_test.feature:3 |
@@ -0,0 +1,10 @@
1
+ require 'cuke_modeler'
2
+
3
+ require "cuke_slicer/version"
4
+ require "cuke_slicer/slicer"
5
+
6
+
7
+ # Top level module under which the gem code lives.
8
+ module CukeSlicer
9
+ # Your code goes here...
10
+ end
@@ -0,0 +1,249 @@
1
+ module CukeSlicer
2
+
3
+ # The object responsible for slicing up a Cucumber test suite into discrete test cases.
4
+ class Slicer
5
+
6
+ # Slices up the given location into individual test cases.
7
+ #
8
+ # The location chosen for slicing can be a file or directory path. Optional filters can be provided in order to
9
+ # ignore certain kinds of test cases. See #known_filters for the available option types. Most options are either a
10
+ # string or regular expression, although arrays of the same can be given instead if more than one filter is desired.
11
+ #
12
+ # A block can be provided as a filter which can allow for maximal filtering flexibility. Note, however, that this
13
+ # exposes the underlying modeling objects and knowledge of how they work is then required to make good use of the
14
+ # filter.
15
+ #
16
+ # @param target [String] the location that will be sliced up
17
+ # @param filters [Hash] the filters that will be applied to the sliced test cases
18
+ def slice(target, filters = {}, &block)
19
+ validate_target(target)
20
+ validate_filters(filters)
21
+
22
+
23
+ begin
24
+ target = File.directory?(target) ? CukeModeler::Directory.new(target) : CukeModeler::FeatureFile.new(target)
25
+ rescue Gherkin::Lexer::LexingError
26
+ raise(ArgumentError, "A syntax or lexing problem was encountered while trying to parse #{target}")
27
+ end
28
+
29
+ if target.is_a?(CukeModeler::Directory)
30
+ sliced_tests = extract_test_cases_from_directory(target, filters, &block)
31
+ else
32
+ sliced_tests = extract_test_cases_from_file(target, filters, &block)
33
+ end
34
+
35
+ sliced_tests
36
+ end
37
+
38
+ # The filtering options that are currently supported by the slicer.
39
+ def self.known_filters
40
+ [:excluded_tags,
41
+ :included_tags,
42
+ :excluded_paths,
43
+ :included_paths]
44
+ end
45
+
46
+
47
+ private
48
+
49
+
50
+ def validate_target(target)
51
+ raise(ArgumentError, "File or directory '#{target}' does not exist") unless File.exists?(target.to_s)
52
+ end
53
+
54
+ def validate_filters(filter_sets)
55
+ filter_sets.each do |filter_type, filter_value|
56
+ raise(ArgumentError, "Unknown filter '#{filter_type}'") unless self.class.known_filters.include?(filter_type)
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)
222
+ end
223
+
224
+ def or_filter_match(element, filters)
225
+ filters.any? do |filter|
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
246
+ end
247
+
248
+ end
249
+ end