parallel_tests 2.21.3 → 4.2.1
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.
- checksums.yaml +4 -4
- data/Readme.md +73 -39
- data/bin/parallel_cucumber +2 -1
- data/bin/parallel_rspec +2 -1
- data/bin/parallel_spinach +2 -1
- data/bin/parallel_test +2 -1
- data/lib/parallel_tests/cli.rb +188 -100
- data/lib/parallel_tests/cucumber/failures_logger.rb +1 -1
- data/lib/parallel_tests/cucumber/features_with_steps.rb +32 -0
- data/lib/parallel_tests/cucumber/runner.rb +10 -7
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +18 -15
- data/lib/parallel_tests/cucumber/scenarios.rb +34 -36
- data/lib/parallel_tests/gherkin/io.rb +2 -3
- data/lib/parallel_tests/gherkin/listener.rb +10 -12
- data/lib/parallel_tests/gherkin/runner.rb +29 -35
- data/lib/parallel_tests/gherkin/runtime_logger.rb +13 -12
- data/lib/parallel_tests/grouper.rb +88 -28
- data/lib/parallel_tests/pids.rb +6 -5
- data/lib/parallel_tests/railtie.rb +1 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +2 -2
- data/lib/parallel_tests/rspec/logger_base.rb +9 -7
- data/lib/parallel_tests/rspec/runner.rb +32 -24
- data/lib/parallel_tests/rspec/runtime_logger.rb +12 -11
- data/lib/parallel_tests/rspec/summary_logger.rb +2 -3
- data/lib/parallel_tests/spinach/runner.rb +6 -2
- data/lib/parallel_tests/tasks.rb +153 -65
- data/lib/parallel_tests/test/runner.rb +114 -49
- data/lib/parallel_tests/test/runtime_logger.rb +18 -47
- data/lib/parallel_tests/version.rb +2 -1
- data/lib/parallel_tests.rb +14 -19
- metadata +15 -11
data/lib/parallel_tests/cli.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'optparse'
|
2
3
|
require 'tempfile'
|
3
4
|
require 'parallel_tests'
|
4
5
|
require 'shellwords'
|
6
|
+
require 'pathname'
|
5
7
|
|
6
8
|
module ParallelTests
|
7
9
|
class CLI
|
@@ -13,12 +15,12 @@ module ParallelTests
|
|
13
15
|
ENV['DISABLE_SPRING'] ||= '1'
|
14
16
|
|
15
17
|
num_processes = ParallelTests.determine_number_of_processes(options[:count])
|
16
|
-
num_processes
|
18
|
+
num_processes *= (options[:multiply] || 1)
|
17
19
|
|
18
20
|
options[:first_is_1] ||= first_is_1?
|
19
21
|
|
20
22
|
if options[:execute]
|
21
|
-
|
23
|
+
execute_command_in_parallel(options[:execute], num_processes, options)
|
22
24
|
else
|
23
25
|
run_tests_in_parallel(num_processes, options)
|
24
26
|
end
|
@@ -30,9 +32,23 @@ module ParallelTests
|
|
30
32
|
@graceful_shutdown_attempted ||= false
|
31
33
|
Kernel.exit if @graceful_shutdown_attempted
|
32
34
|
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
35
|
+
# In a shell, all sub-processes also get an interrupt, so they shut themselves down.
|
36
|
+
# In a background process this does not happen and we need to do it ourselves.
|
37
|
+
# We cannot always send the interrupt since then the sub-processes would get interrupted twice when in foreground
|
38
|
+
# and that messes with interrupt handling.
|
39
|
+
#
|
40
|
+
# (can simulate detached with `(bundle exec parallel_rspec test/a_spec.rb -n 2 &)`)
|
41
|
+
# also the integration test "passes on int signal to child processes" is detached.
|
42
|
+
#
|
43
|
+
# On windows getpgid does not work so we resort to always killing which is the smaller bug.
|
44
|
+
#
|
45
|
+
# The ParallelTests::Pids `synchronize` method can't be called directly from a trap,
|
46
|
+
# using Thread workaround https://github.com/ddollar/foreman/issues/332
|
47
|
+
Thread.new do
|
48
|
+
if Gem.win_platform? || ((child_pid = ParallelTests.pids.all.first) && Process.getpgid(child_pid) != Process.pid)
|
49
|
+
ParallelTests.stop_all_processes
|
50
|
+
end
|
51
|
+
end
|
36
52
|
|
37
53
|
@graceful_shutdown_attempted = true
|
38
54
|
end
|
@@ -40,16 +56,13 @@ module ParallelTests
|
|
40
56
|
def execute_in_parallel(items, num_processes, options)
|
41
57
|
Tempfile.open 'parallel_tests-lock' do |lock|
|
42
58
|
ParallelTests.with_pid_file do
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
puts
|
59
|
+
simulate_output_for_ci options[:serialize_stdout] do
|
60
|
+
Parallel.map(items, in_threads: num_processes) do |item|
|
61
|
+
result = yield(item)
|
62
|
+
reprint_output(result, lock.path) if options[:serialize_stdout]
|
63
|
+
ParallelTests.stop_all_processes if options[:fail_fast] && result[:exit_status] != 0
|
64
|
+
result
|
50
65
|
end
|
51
|
-
reprint_output(result, lock.path) if options[:serialize_stdout]
|
52
|
-
result
|
53
66
|
end
|
54
67
|
end
|
55
68
|
end
|
@@ -58,33 +71,45 @@ module ParallelTests
|
|
58
71
|
def run_tests_in_parallel(num_processes, options)
|
59
72
|
test_results = nil
|
60
73
|
|
61
|
-
|
74
|
+
run_tests_proc = -> do
|
62
75
|
groups = @runner.tests_in_groups(options[:files], num_processes, options)
|
63
|
-
groups.reject!
|
76
|
+
groups.reject!(&:empty?)
|
64
77
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
run_tests(group, groups_to_run.index(group), 1, options)
|
70
|
-
end
|
71
|
-
else
|
72
|
-
report_number_of_tests(groups)
|
78
|
+
if options[:only_group]
|
79
|
+
groups = options[:only_group].map { |i| groups[i - 1] }.compact
|
80
|
+
num_processes = 1
|
81
|
+
end
|
73
82
|
|
74
|
-
|
75
|
-
|
76
|
-
|
83
|
+
report_number_of_tests(groups) unless options[:quiet]
|
84
|
+
test_results = execute_in_parallel(groups, groups.size, options) do |group|
|
85
|
+
run_tests(group, groups.index(group), num_processes, options)
|
77
86
|
end
|
87
|
+
report_results(test_results, options) unless options[:quiet]
|
88
|
+
end
|
78
89
|
|
79
|
-
|
90
|
+
if options[:quiet]
|
91
|
+
run_tests_proc.call
|
92
|
+
else
|
93
|
+
report_time_taken(&run_tests_proc)
|
80
94
|
end
|
81
95
|
|
82
|
-
|
96
|
+
if any_test_failed?(test_results)
|
97
|
+
warn final_fail_message
|
98
|
+
|
99
|
+
# return the highest exit status to allow sub-processes to send things other than 1
|
100
|
+
exit_status = if options[:highest_exit_status]
|
101
|
+
test_results.map { |data| data.fetch(:exit_status) }.max
|
102
|
+
else
|
103
|
+
1
|
104
|
+
end
|
105
|
+
|
106
|
+
exit exit_status
|
107
|
+
end
|
83
108
|
end
|
84
109
|
|
85
110
|
def run_tests(group, process_number, num_processes, options)
|
86
111
|
if group.empty?
|
87
|
-
{:
|
112
|
+
{ stdout: '', exit_status: 0, command: nil, seed: nil }
|
88
113
|
else
|
89
114
|
@runner.run_tests(group, process_number, num_processes, options)
|
90
115
|
end
|
@@ -92,6 +117,7 @@ module ParallelTests
|
|
92
117
|
|
93
118
|
def reprint_output(result, lockfile)
|
94
119
|
lock(lockfile) do
|
120
|
+
$stdout.puts
|
95
121
|
$stdout.puts result[:stdout]
|
96
122
|
$stdout.flush
|
97
123
|
end
|
@@ -99,18 +125,16 @@ module ParallelTests
|
|
99
125
|
|
100
126
|
def lock(lockfile)
|
101
127
|
File.open(lockfile) do |lock|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
lock.flock File::LOCK_UN
|
108
|
-
end
|
128
|
+
lock.flock File::LOCK_EX
|
129
|
+
yield
|
130
|
+
ensure
|
131
|
+
# This shouldn't be necessary, but appears to be
|
132
|
+
lock.flock File::LOCK_UN
|
109
133
|
end
|
110
134
|
end
|
111
135
|
|
112
136
|
def report_results(test_results, options)
|
113
|
-
results = @runner.find_results(test_results.map { |result| result[:stdout] }*"")
|
137
|
+
results = @runner.find_results(test_results.map { |result| result[:stdout] } * "")
|
114
138
|
puts ""
|
115
139
|
puts @runner.summarize_results(results)
|
116
140
|
|
@@ -121,13 +145,12 @@ module ParallelTests
|
|
121
145
|
failing_sets = test_results.reject { |r| r[:exit_status] == 0 }
|
122
146
|
return if failing_sets.none?
|
123
147
|
|
124
|
-
if options[:verbose]
|
148
|
+
if options[:verbose] || options[:verbose_command]
|
125
149
|
puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n"
|
126
150
|
failing_sets.each do |failing_set|
|
127
151
|
command = failing_set[:command]
|
128
|
-
command = command.gsub(/;export [A-Z_]+;/, ' ') # remove ugly export statements
|
129
152
|
command = @runner.command_with_seed(command, failing_set[:seed]) if failing_set[:seed]
|
130
|
-
|
153
|
+
@runner.print_command(command, failing_set[:env] || {})
|
131
154
|
end
|
132
155
|
end
|
133
156
|
end
|
@@ -135,20 +158,31 @@ module ParallelTests
|
|
135
158
|
def report_number_of_tests(groups)
|
136
159
|
name = @runner.test_file_name
|
137
160
|
num_processes = groups.size
|
138
|
-
num_tests = groups.map(&:size).
|
161
|
+
num_tests = groups.map(&:size).sum
|
139
162
|
tests_per_process = (num_processes == 0 ? 0 : num_tests / num_processes)
|
140
|
-
puts "#{num_processes}
|
163
|
+
puts "#{pluralize(num_processes, 'process')} for #{pluralize(num_tests, name)}, ~ #{pluralize(tests_per_process, name)} per process"
|
164
|
+
end
|
165
|
+
|
166
|
+
def pluralize(n, singular)
|
167
|
+
if n == 1
|
168
|
+
"1 #{singular}"
|
169
|
+
elsif singular.end_with?('s', 'sh', 'ch', 'x', 'z')
|
170
|
+
"#{n} #{singular}es"
|
171
|
+
else
|
172
|
+
"#{n} #{singular}s"
|
173
|
+
end
|
141
174
|
end
|
142
175
|
|
143
|
-
#exit with correct status code so rake parallel:test && echo 123 works
|
176
|
+
# exit with correct status code so rake parallel:test && echo 123 works
|
144
177
|
def any_test_failed?(test_results)
|
145
178
|
test_results.any? { |result| result[:exit_status] != 0 }
|
146
179
|
end
|
147
180
|
|
148
181
|
def parse_options!(argv)
|
182
|
+
newline_padding = " " * 37
|
149
183
|
options = {}
|
150
184
|
OptionParser.new do |opts|
|
151
|
-
opts.banner =
|
185
|
+
opts.banner = <<~BANNER
|
152
186
|
Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
|
153
187
|
|
154
188
|
[optional] Only selected files & folders:
|
@@ -161,65 +195,100 @@ module ParallelTests
|
|
161
195
|
BANNER
|
162
196
|
opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
|
163
197
|
opts.on("-p", "--pattern [PATTERN]", "run tests matching this regex pattern") { |pattern| options[:pattern] = /#{pattern}/ }
|
164
|
-
opts.on("--
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
198
|
+
opts.on("--exclude-pattern", "--exclude-pattern [PATTERN]", "exclude tests matching this regex pattern") { |pattern| options[:exclude_pattern] = /#{pattern}/ }
|
199
|
+
opts.on(
|
200
|
+
"--group-by [TYPE]",
|
201
|
+
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
|
202
|
+
group tests by:
|
203
|
+
found - order of finding files
|
204
|
+
steps - number of cucumber/spinach steps
|
205
|
+
scenarios - individual cucumber scenarios
|
206
|
+
filesize - by size of the file
|
207
|
+
runtime - info from runtime log
|
208
|
+
default - runtime when runtime log is filled otherwise filesize
|
172
209
|
TEXT
|
173
|
-
|
174
|
-
opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run")
|
210
|
+
) { |type| options[:group_by] = type.to_sym }
|
211
|
+
opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") do |multiply|
|
212
|
+
options[:multiply] = multiply
|
213
|
+
end
|
175
214
|
|
176
|
-
opts.on("-s [PATTERN]", "--single [PATTERN]",
|
177
|
-
|
215
|
+
opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in the same process") do |pattern|
|
216
|
+
(options[:single_process] ||= []) << /#{pattern}/
|
217
|
+
end
|
178
218
|
|
179
|
-
|
180
|
-
options[:
|
219
|
+
opts.on("-i", "--isolate", "Do not run any other tests in the group used by --single(-s)") do
|
220
|
+
options[:isolate] = true
|
181
221
|
end
|
182
222
|
|
183
|
-
opts.on(
|
184
|
-
"
|
223
|
+
opts.on(
|
224
|
+
"--isolate-n [PROCESSES]",
|
225
|
+
Integer,
|
226
|
+
"Use 'isolate' singles with number of processes, default: 1."
|
227
|
+
) { |n| options[:isolate_count] = n }
|
185
228
|
|
186
|
-
|
229
|
+
opts.on("--highest-exit-status", "Exit with the highest exit status provided by test run(s)") do
|
230
|
+
options[:highest_exit_status] = true
|
187
231
|
end
|
188
232
|
|
189
|
-
opts.on(
|
233
|
+
opts.on(
|
234
|
+
"--specify-groups [SPECS]",
|
235
|
+
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
|
236
|
+
Use 'specify-groups' if you want to specify multiple specs running in multiple
|
237
|
+
processes in a specific formation. Commas indicate specs in the same process,
|
238
|
+
pipes indicate specs in a new process. Cannot use with --single, --isolate, or
|
239
|
+
--isolate-n. Ex.
|
240
|
+
$ parallel_test -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
|
241
|
+
Process 1 will contain 1_spec.rb and 2_spec.rb
|
242
|
+
Process 2 will contain 3_spec.rb
|
243
|
+
Process 3 will contain all other specs
|
244
|
+
TEXT
|
245
|
+
) { |groups| options[:specify_groups] = groups }
|
246
|
+
|
247
|
+
opts.on("--only-group INT[,INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
|
190
248
|
|
191
|
-
opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |
|
192
|
-
opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = arg
|
249
|
+
opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) }
|
250
|
+
opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = Shellwords.shellsplit(arg) }
|
193
251
|
opts.on("-t", "--type [TYPE]", "test(default) / rspec / cucumber / spinach") do |type|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
abort
|
199
|
-
end
|
252
|
+
@runner = load_runner(type)
|
253
|
+
rescue NameError, LoadError => e
|
254
|
+
puts "Runner for `#{type}` type has not been found! (#{e})"
|
255
|
+
abort
|
200
256
|
end
|
201
|
-
opts.on(
|
202
|
-
|
203
|
-
|
204
|
-
|
257
|
+
opts.on(
|
258
|
+
"--suffix [PATTERN]",
|
259
|
+
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
|
260
|
+
override built in test file pattern (should match suffix):
|
261
|
+
'_spec\.rb$' - matches rspec files
|
262
|
+
'_(test|spec).rb$' - matches test or spec files
|
205
263
|
TEXT
|
206
|
-
|
264
|
+
) { |pattern| options[:suffix] = /#{pattern}/ }
|
207
265
|
opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true }
|
208
266
|
opts.on("--prefix-output-with-test-env-number", "Prefixes test env number to the output when not using --serialize-stdout") { options[:prefix_output_with_test_env_number] = true }
|
209
267
|
opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true }
|
210
268
|
opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true }
|
211
269
|
opts.on("--no-symlinks", "Do not traverse symbolic links to find test files") { options[:symlinks] = false }
|
212
|
-
opts.on('--ignore-tags [PATTERN]', 'When counting steps ignore scenarios with tags that match this pattern')
|
270
|
+
opts.on('--ignore-tags [PATTERN]', 'When counting steps ignore scenarios with tags that match this pattern') { |arg| options[:ignore_tag_pattern] = arg }
|
213
271
|
opts.on("--nice", "execute test commands with low priority.") { options[:nice] = true }
|
214
272
|
opts.on("--runtime-log [PATH]", "Location of previously recorded test runtimes") { |path| options[:runtime_log] = path }
|
215
273
|
opts.on("--allowed-missing [INT]", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent }
|
216
274
|
opts.on("--unknown-runtime [FLOAT]", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
|
217
275
|
opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
|
218
|
-
opts.on("--
|
219
|
-
opts.on("
|
220
|
-
opts.on("-
|
276
|
+
opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
|
277
|
+
opts.on("--verbose", "Print debug output") { options[:verbose] = true }
|
278
|
+
opts.on("--verbose-command", "Displays the command that will be executed by each process and when there are failures displays the command executed by each process that failed") { options[:verbose_command] = true }
|
279
|
+
opts.on("--quiet", "Print only tests output") { options[:quiet] = true }
|
280
|
+
opts.on("-v", "--version", "Show Version") do
|
281
|
+
puts ParallelTests::VERSION
|
282
|
+
exit 0
|
283
|
+
end
|
284
|
+
opts.on("-h", "--help", "Show this.") do
|
285
|
+
puts opts
|
286
|
+
exit 0
|
287
|
+
end
|
221
288
|
end.parse!(argv)
|
222
289
|
|
290
|
+
raise "Both options are mutually exclusive: verbose & quiet" if options[:verbose] && options[:quiet]
|
291
|
+
|
223
292
|
if options[:count] == 0
|
224
293
|
options.delete(:count)
|
225
294
|
options[:non_parallel] = true
|
@@ -227,40 +296,53 @@ module ParallelTests
|
|
227
296
|
|
228
297
|
files, remaining = extract_file_paths(argv)
|
229
298
|
unless options[:execute]
|
230
|
-
|
231
|
-
|
299
|
+
if files.empty?
|
300
|
+
default_test_folder = @runner.default_test_folder
|
301
|
+
if File.directory?(default_test_folder)
|
302
|
+
files = [default_test_folder]
|
303
|
+
else
|
304
|
+
abort "Pass files or folders to run"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
options[:files] = files.map { |file_path| Pathname.new(file_path).cleanpath.to_s }
|
232
308
|
end
|
233
309
|
|
234
310
|
append_test_options(options, remaining)
|
235
311
|
|
236
312
|
options[:group_by] ||= :filesize if options[:only_group]
|
237
313
|
|
238
|
-
|
314
|
+
if options[:group_by] == :found && options[:single_process]
|
315
|
+
raise "--group-by found and --single-process are not supported"
|
316
|
+
end
|
239
317
|
allowed = [:filesize, :runtime, :found]
|
240
318
|
if !allowed.include?(options[:group_by]) && options[:only_group]
|
241
319
|
raise "--group-by #{allowed.join(" or ")} is required for --only-group"
|
242
320
|
end
|
243
321
|
|
322
|
+
if options[:specify_groups] && (options.keys & [:single_process, :isolate, :isolate_count]).any?
|
323
|
+
raise "Can't pass --specify-groups with any of these keys: --single, --isolate, or --isolate-n"
|
324
|
+
end
|
325
|
+
|
244
326
|
options
|
245
327
|
end
|
246
328
|
|
247
329
|
def extract_file_paths(argv)
|
248
330
|
dash_index = argv.rindex("--")
|
249
331
|
file_args_at = (dash_index || -1) + 1
|
250
|
-
[argv[file_args_at
|
332
|
+
[argv[file_args_at..], argv[0...(dash_index || 0)]]
|
251
333
|
end
|
252
334
|
|
253
335
|
def extract_test_options(argv)
|
254
336
|
dash_index = argv.index("--") || -1
|
255
|
-
argv[dash_index+1
|
337
|
+
argv[dash_index + 1..]
|
256
338
|
end
|
257
339
|
|
258
340
|
def append_test_options(options, argv)
|
259
341
|
new_opts = extract_test_options(argv)
|
260
342
|
return if new_opts.empty?
|
261
343
|
|
262
|
-
|
263
|
-
options[:test_options]
|
344
|
+
options[:test_options] ||= []
|
345
|
+
options[:test_options] += new_opts
|
264
346
|
end
|
265
347
|
|
266
348
|
def load_runner(type)
|
@@ -270,9 +352,9 @@ module ParallelTests
|
|
270
352
|
klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
|
271
353
|
end
|
272
354
|
|
273
|
-
def
|
355
|
+
def execute_command_in_parallel(command, num_processes, options)
|
274
356
|
runs = if options[:only_group]
|
275
|
-
options[:only_group].map{|g| g - 1}
|
357
|
+
options[:only_group].map { |g| g - 1 }
|
276
358
|
else
|
277
359
|
(0...num_processes).to_a
|
278
360
|
end
|
@@ -291,22 +373,21 @@ module ParallelTests
|
|
291
373
|
abort if results.any? { |r| r[:exit_status] != 0 }
|
292
374
|
end
|
293
375
|
|
294
|
-
def report_time_taken
|
295
|
-
seconds = ParallelTests.delta
|
376
|
+
def report_time_taken(&block)
|
377
|
+
seconds = ParallelTests.delta(&block).to_i
|
296
378
|
puts "\nTook #{seconds} seconds#{detailed_duration(seconds)}"
|
297
379
|
end
|
298
380
|
|
299
381
|
def detailed_duration(seconds)
|
300
|
-
parts = [
|
382
|
+
parts = [seconds / 3600, seconds % 3600 / 60, seconds % 60].drop_while(&:zero?)
|
301
383
|
return if parts.size < 2
|
302
384
|
parts = parts.map { |i| "%02d" % i }.join(':').sub(/^0/, '')
|
303
385
|
" (#{parts})"
|
304
386
|
end
|
305
387
|
|
306
388
|
def final_fail_message
|
307
|
-
fail_message = "
|
389
|
+
fail_message = "Tests Failed"
|
308
390
|
fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
|
309
|
-
|
310
391
|
fail_message
|
311
392
|
end
|
312
393
|
|
@@ -320,13 +401,20 @@ module ParallelTests
|
|
320
401
|
end
|
321
402
|
|
322
403
|
# CI systems often fail when there is no output for a long time, so simulate some output
|
323
|
-
def simulate_output_for_ci
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
404
|
+
def simulate_output_for_ci(simulate)
|
405
|
+
if simulate
|
406
|
+
progress_indicator = Thread.new do
|
407
|
+
interval = Float(ENV['PARALLEL_TEST_HEARTBEAT_INTERVAL'] || 60)
|
408
|
+
loop do
|
409
|
+
sleep interval
|
410
|
+
print '.'
|
411
|
+
end
|
329
412
|
end
|
413
|
+
test_results = yield
|
414
|
+
progress_indicator.exit
|
415
|
+
test_results
|
416
|
+
else
|
417
|
+
yield
|
330
418
|
end
|
331
419
|
end
|
332
420
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
begin
|
3
|
+
gem "cuke_modeler", "~> 3.0"
|
4
|
+
require 'cuke_modeler'
|
5
|
+
rescue LoadError
|
6
|
+
raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
|
7
|
+
end
|
8
|
+
|
9
|
+
module ParallelTests
|
10
|
+
module Cucumber
|
11
|
+
class FeaturesWithSteps
|
12
|
+
class << self
|
13
|
+
def all(tests, options)
|
14
|
+
ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
|
15
|
+
# format of hash will be FILENAME => NUM_STEPS
|
16
|
+
steps_per_file = tests.each_with_object({}) do |file, steps|
|
17
|
+
feature = ::CukeModeler::FeatureFile.new(file).feature
|
18
|
+
|
19
|
+
# skip feature if it matches tag regex
|
20
|
+
next if feature.tags.grep(ignore_tag_pattern).any?
|
21
|
+
|
22
|
+
# count the number of steps in the file
|
23
|
+
# will only include a feature if the regex does not match
|
24
|
+
all_steps = feature.scenarios.map { |a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
|
25
|
+
steps[file] = all_steps.sum
|
26
|
+
end
|
27
|
+
steps_per_file.sort_by { |_, value| -value }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,16 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "parallel_tests/gherkin/runner"
|
2
3
|
|
3
4
|
module ParallelTests
|
4
5
|
module Cucumber
|
5
6
|
class Runner < ParallelTests::Gherkin::Runner
|
6
|
-
SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios
|
7
|
-
SCENARIO_REGEX =
|
7
|
+
SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/.freeze
|
8
|
+
SCENARIO_REGEX = %r{^cucumber features/.+:\d+}.freeze
|
8
9
|
|
9
10
|
class << self
|
10
11
|
def name
|
11
12
|
'cucumber'
|
12
13
|
end
|
13
14
|
|
15
|
+
def default_test_folder
|
16
|
+
'features'
|
17
|
+
end
|
18
|
+
|
14
19
|
def line_is_result?(line)
|
15
20
|
super || line =~ SCENARIO_REGEX || line =~ SCENARIOS_RESULTS_BOUNDARY_REGEX
|
16
21
|
end
|
@@ -21,9 +26,7 @@ module ParallelTests
|
|
21
26
|
scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first)
|
22
27
|
scenario_groups.each do |header, group|
|
23
28
|
scenarios = group.flatten.grep(SCENARIO_REGEX)
|
24
|
-
if scenarios.any?
|
25
|
-
output << ([header] + scenarios).join("\n")
|
26
|
-
end
|
29
|
+
output << ([header] + scenarios).join("\n") if scenarios.any?
|
27
30
|
end
|
28
31
|
|
29
32
|
output << super
|
@@ -32,8 +35,8 @@ module ParallelTests
|
|
32
35
|
end
|
33
36
|
|
34
37
|
def command_with_seed(cmd, seed)
|
35
|
-
clean = cmd
|
36
|
-
|
38
|
+
clean = remove_command_arguments(cmd, '--order')
|
39
|
+
[*clean, '--order', "random:#{seed}"]
|
37
40
|
end
|
38
41
|
end
|
39
42
|
end
|
@@ -1,36 +1,34 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
module ParallelTests
|
4
3
|
module Cucumber
|
5
4
|
module Formatters
|
6
5
|
class ScenarioLineLogger
|
7
6
|
attr_reader :scenarios
|
8
7
|
|
9
|
-
def initialize(tag_expression =
|
8
|
+
def initialize(tag_expression = nil)
|
10
9
|
@scenarios = []
|
11
10
|
@tag_expression = tag_expression
|
12
11
|
end
|
13
12
|
|
14
13
|
def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
|
15
|
-
scenario_tags = feature_element
|
14
|
+
scenario_tags = feature_element.tags.map(&:name)
|
16
15
|
scenario_tags = feature_tags + scenario_tags
|
17
|
-
if feature_element
|
18
|
-
test_line = feature_element
|
16
|
+
if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
|
17
|
+
test_line = feature_element.source_line
|
19
18
|
|
20
19
|
# We don't accept the feature_element if the current tags are not valid
|
21
|
-
return unless
|
20
|
+
return unless matches_tags?(scenario_tags)
|
22
21
|
# or if it is not at the correct location
|
23
22
|
return if line_numbers.any? && !line_numbers.include?(test_line)
|
24
23
|
|
25
|
-
@scenarios << [uri, feature_element
|
24
|
+
@scenarios << [uri, feature_element.source_line].join(":")
|
26
25
|
else # :ScenarioOutline
|
27
|
-
feature_element
|
28
|
-
example_tags = example
|
26
|
+
feature_element.examples.each do |example|
|
27
|
+
example_tags = example.tags.map(&:name)
|
29
28
|
example_tags = scenario_tags + example_tags
|
30
|
-
next unless
|
31
|
-
rows
|
32
|
-
|
33
|
-
test_line = row[:location][:line]
|
29
|
+
next unless matches_tags?(example_tags)
|
30
|
+
example.rows[1..].each do |row|
|
31
|
+
test_line = row.source_line
|
34
32
|
next if line_numbers.any? && !line_numbers.include?(test_line)
|
35
33
|
|
36
34
|
@scenarios << [uri, test_line].join(':')
|
@@ -39,7 +37,12 @@ module ParallelTests
|
|
39
37
|
end
|
40
38
|
end
|
41
39
|
|
42
|
-
def method_missing(*
|
40
|
+
def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def matches_tags?(tags)
|
45
|
+
@tag_expression.nil? || @tag_expression.evaluate(tags)
|
43
46
|
end
|
44
47
|
end
|
45
48
|
end
|