cuke_slicer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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