forking_test_runner 1.4.0 → 1.7.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.
- checksums.yaml +4 -4
- data/lib/forking_test_runner.rb +134 -211
- data/lib/forking_test_runner/cli.rb +94 -0
- data/lib/forking_test_runner/coverage_capture.rb +71 -0
- data/lib/forking_test_runner/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5941690d477f2dcded481eb639078719303e399fce77bf0f3dfd4a8d29281466
|
4
|
+
data.tar.gz: 4d007910130e2d2f7444df0bad571f23d623dbe63609e3f5389bdd2ebfa838ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc9419382a7dadaa81fad1b498d2ed3b5cd6b8c4bfa4727b1bddee0203b3ed1a87133218fd4610e2adf94043cb2be07ac733fc92613ffc9d83cfef05468c330c
|
7
|
+
data.tar.gz: 247bec8782245d23d487e32e2b65a1bd8eee0f04458abf8f66c14cec083de865f4cc107af370b39f837dcc674f18d3c67075eb293616ea2a4a6f79457b14d2ef
|
data/lib/forking_test_runner.rb
CHANGED
@@ -1,130 +1,75 @@
|
|
1
1
|
require 'benchmark'
|
2
2
|
require 'optparse'
|
3
3
|
require 'forking_test_runner/version'
|
4
|
+
require 'forking_test_runner/coverage_capture'
|
5
|
+
require 'forking_test_runner/cli'
|
6
|
+
require 'parallel'
|
7
|
+
require 'tempfile'
|
4
8
|
|
5
9
|
module ForkingTestRunner
|
6
10
|
CLEAR = "------"
|
7
11
|
|
8
|
-
module CoverageCapture
|
9
|
-
def capture_coverage!
|
10
|
-
@capture_coverage = peek_result.dup
|
11
|
-
end
|
12
|
-
|
13
|
-
# override to add pre-fork captured coverage when someone asks for the results
|
14
|
-
def result
|
15
|
-
original = super
|
16
|
-
return original unless @capture_coverage
|
17
|
-
CoverageCapture.merge_coverage(original, @capture_coverage)
|
18
|
-
end
|
19
|
-
|
20
|
-
class << self
|
21
|
-
def merge_coverage(a, b)
|
22
|
-
merged = a.dup
|
23
|
-
b.each do |file, coverage|
|
24
|
-
orig = merged[file]
|
25
|
-
merged[file] = if orig
|
26
|
-
if coverage.is_a?(Array)
|
27
|
-
merge_lines_coverage(orig, coverage)
|
28
|
-
else
|
29
|
-
{
|
30
|
-
lines: merge_lines_coverage(orig.fetch(:lines), coverage.fetch(:lines)),
|
31
|
-
branches: merge_branches_coverage(orig.fetch(:branches), coverage.fetch(:branches))
|
32
|
-
}
|
33
|
-
end
|
34
|
-
else
|
35
|
-
coverage
|
36
|
-
end
|
37
|
-
end
|
38
|
-
merged
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
# assuming b has same or more keys since it comes from a fork
|
44
|
-
# [nil,1,0] + [nil,nil,2] -> [nil,1,2]
|
45
|
-
def merge_lines_coverage(a, b)
|
46
|
-
b.each_with_index.map do |b_count, i|
|
47
|
-
a_count = a[i]
|
48
|
-
(a_count.nil? && b_count.nil?) ? nil : a_count.to_i + b_count.to_i
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# assuming b has same or more keys since it comes from a fork
|
53
|
-
# {foo: {bar: 0, baz: 1}} + {foo: {bar: 1, baz: 0}} -> {foo: {bar: 1, baz: 1}}
|
54
|
-
def merge_branches_coverage(a, b)
|
55
|
-
b.each_with_object({}) do |(branch, v), all|
|
56
|
-
vb = v.dup
|
57
|
-
if part = a[branch]
|
58
|
-
part.each do |nested, a_count|
|
59
|
-
vb[nested] = a_count + vb[nested].to_i
|
60
|
-
end
|
61
|
-
end
|
62
|
-
all[branch] = vb
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
12
|
class << self
|
69
13
|
def cli(argv)
|
70
|
-
@options, tests = parse_options(argv)
|
71
|
-
|
72
|
-
disable_test_autorun
|
73
|
-
|
74
|
-
load_test_env(@options.fetch(:helper))
|
14
|
+
@options, tests = CLI.parse_options(argv)
|
75
15
|
|
76
16
|
# figure out what we need to run
|
77
17
|
runtime_log = @options.fetch(:runtime_log)
|
78
|
-
|
79
|
-
|
18
|
+
groups, group_count = find_group_args
|
19
|
+
parallel = @options.fetch(:parallel)
|
20
|
+
test_groups =
|
21
|
+
if parallel && !@options.fetch(:group)
|
22
|
+
Array.new(parallel) { |i| find_tests_for_group(i + 1, parallel, tests, runtime_log) }
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Use the same amount of processors as groups" if parallel && parallel != groups.count
|
25
|
+
groups.map { |group| find_tests_for_group(group, group_count, tests, runtime_log) }
|
26
|
+
end
|
80
27
|
|
28
|
+
# say what we are running
|
29
|
+
all_tests = test_groups.flatten(1)
|
81
30
|
if @options.fetch(:quiet)
|
82
|
-
puts "Running #{
|
31
|
+
puts "Running #{all_tests.size} test files"
|
83
32
|
else
|
84
|
-
puts "Running tests #{
|
85
|
-
end
|
86
|
-
|
87
|
-
if ar?
|
88
|
-
preload_fixtures
|
89
|
-
ActiveRecord::Base.connection.disconnect!
|
33
|
+
puts "Running tests #{all_tests.map(&:first).join(" ")}"
|
90
34
|
end
|
91
35
|
|
92
|
-
Coverage.capture_coverage! if @options.fetch(:merge_coverage)
|
93
|
-
|
94
36
|
# run all the tests
|
95
|
-
results =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if runtime_log && !@options.fetch(:quiet)
|
102
|
-
puts "Time: expected #{expected.round(2)}, actual #{time.round(2)}"
|
103
|
-
end
|
37
|
+
results = with_lock do |lock|
|
38
|
+
Parallel.map_with_index(test_groups, in_processes: parallel || 0) do |tests, env_index|
|
39
|
+
if parallel
|
40
|
+
ENV["TEST_ENV_NUMBER"] = (env_index == 0 ? '' : (env_index + 1).to_s) # NOTE: does not support first_is_1 option
|
41
|
+
end
|
104
42
|
|
105
|
-
|
106
|
-
puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
|
107
|
-
end
|
43
|
+
reraise_clean_ar_error { load_test_env }
|
108
44
|
|
109
|
-
|
45
|
+
tests.map do |file, expected|
|
46
|
+
print_started file unless parallel
|
47
|
+
result = [file, expected, *benchmark { run_test(file) }]
|
48
|
+
sync_stdout lock do
|
49
|
+
print_started file if parallel
|
50
|
+
print_finished *result
|
51
|
+
end
|
52
|
+
result
|
53
|
+
end
|
54
|
+
end.flatten(1)
|
110
55
|
end
|
111
56
|
|
112
57
|
unless @options.fetch(:quiet)
|
113
58
|
# pretty print the results
|
114
59
|
puts "\nResults:"
|
115
60
|
puts results.
|
116
|
-
sort_by { |_,_,_,_
|
117
|
-
map { |f,_,_,_
|
61
|
+
sort_by { |_,_,_,r,_| r ? 0 : 1 }. # failures should be last so they are easy to find
|
62
|
+
map { |f,_,_,r,_| "#{f}: #{r ? "OK" : "Fail"}"}
|
118
63
|
puts
|
119
64
|
end
|
120
65
|
|
121
|
-
success = results.map
|
66
|
+
success = results.map { |r| r[3] }.all?
|
122
67
|
|
123
|
-
puts colorize(success, summarize_results(results.map { |r| r[
|
68
|
+
puts colorize(success, summarize_results(results.map { |r| r[4] }))
|
124
69
|
|
125
70
|
if runtime_log
|
126
71
|
# show how long they ran vs expected
|
127
|
-
diff = results.map { |_,time
|
72
|
+
diff = results.map { |_, expected, time| time - expected }.inject(:+).to_f
|
128
73
|
puts "Time: #{diff.round(2)} diff to expected"
|
129
74
|
end
|
130
75
|
|
@@ -140,6 +85,38 @@ module ForkingTestRunner
|
|
140
85
|
|
141
86
|
private
|
142
87
|
|
88
|
+
def with_lock(&block)
|
89
|
+
return yield unless @options.fetch(:parallel)
|
90
|
+
Tempfile.open"forking-test-runner-lock", &block
|
91
|
+
end
|
92
|
+
|
93
|
+
def sync_stdout(lock)
|
94
|
+
return yield unless @options.fetch(:parallel)
|
95
|
+
begin
|
96
|
+
lock.flock(File::LOCK_EX)
|
97
|
+
yield
|
98
|
+
ensure
|
99
|
+
lock.flock(File::LOCK_UN)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def print_started(file)
|
104
|
+
puts "#{CLEAR} >>> #{file}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def print_finished(file, expected, time, success, stdout)
|
108
|
+
# print stdout if it was not shown before, but needs to be shown
|
109
|
+
puts stdout if (!success && @options.fetch(:quiet)) || (@options.fetch(:parallel) && !@options.fetch(:quiet))
|
110
|
+
|
111
|
+
if @options.fetch(:runtime_log) && !@options.fetch(:quiet)
|
112
|
+
puts "Time: expected #{expected.round(2)}, actual #{time.round(2)}"
|
113
|
+
end
|
114
|
+
|
115
|
+
if !success || !@options.fetch(:quiet)
|
116
|
+
puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
143
120
|
def colorize(green, string)
|
144
121
|
if $stdout.tty?
|
145
122
|
"\e[#{green ? 32 : 31}m#{string}\e[0m"
|
@@ -162,15 +139,13 @@ module ForkingTestRunner
|
|
162
139
|
|
163
140
|
def benchmark
|
164
141
|
result = false
|
165
|
-
time = Benchmark.realtime
|
166
|
-
|
167
|
-
end
|
168
|
-
return [time, result].flatten
|
142
|
+
time = Benchmark.realtime { result = yield }
|
143
|
+
[time, *result]
|
169
144
|
end
|
170
145
|
|
171
146
|
# log runtime via dumping or curling it into the runtime log location
|
172
147
|
def record_test_runtime(mode, results, log)
|
173
|
-
data = results.map { |test, time| "#{test}:#{time.round(2)}" }.join("\n") << "\n"
|
148
|
+
data = results.map { |test, _, time| "#{test}:#{time.round(2)}" }.join("\n") << "\n"
|
174
149
|
|
175
150
|
case mode
|
176
151
|
when 'simple'
|
@@ -198,21 +173,47 @@ module ForkingTestRunner
|
|
198
173
|
end
|
199
174
|
|
200
175
|
def find_group_args
|
201
|
-
|
176
|
+
group = @options.fetch(:group)
|
177
|
+
groups = @options.fetch(:groups)
|
178
|
+
if group && groups
|
202
179
|
# delete options we want while leaving others as they are (-v / --seed etc)
|
203
|
-
group
|
204
|
-
group_count = @options.fetch(:groups)
|
180
|
+
[group.split(",").map { |g| Integer(g) }, groups]
|
205
181
|
else
|
206
|
-
|
207
|
-
|
182
|
+
[[1], 1]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def load_test_env
|
187
|
+
CoverageCapture.activate! if @options.fetch(:merge_coverage)
|
188
|
+
|
189
|
+
load_test_helper
|
190
|
+
|
191
|
+
if active_record?
|
192
|
+
preload_fixtures
|
193
|
+
ActiveRecord::Base.connection.disconnect!
|
208
194
|
end
|
209
195
|
|
210
|
-
|
196
|
+
CoverageCapture.capture! if @options.fetch(:merge_coverage)
|
211
197
|
end
|
212
198
|
|
213
|
-
def
|
199
|
+
def reraise_clean_ar_error
|
200
|
+
return yield unless @options.fetch(:parallel)
|
201
|
+
|
202
|
+
e = begin
|
203
|
+
yield
|
204
|
+
nil
|
205
|
+
rescue
|
206
|
+
$!
|
207
|
+
end
|
208
|
+
|
209
|
+
# needs to be done outside of the rescue block to avoid inheriting the cause
|
210
|
+
raise RuntimeError, "Re-raised error from test helper: #{e.message}", e.backtrace if e
|
211
|
+
end
|
212
|
+
|
213
|
+
def load_test_helper
|
214
|
+
disable_test_autorun
|
214
215
|
require 'rspec/core' if @options.fetch(:rspec)
|
215
|
-
helper =
|
216
|
+
helper = @options.fetch(:helper) || (@options.fetch(:rspec) ? "spec/spec_helper" : "test/test_helper")
|
216
217
|
require "./#{helper}"
|
217
218
|
end
|
218
219
|
|
@@ -221,9 +222,8 @@ module ForkingTestRunner
|
|
221
222
|
def preload_fixtures
|
222
223
|
return if @options.fetch(:no_fixtures)
|
223
224
|
|
224
|
-
fixtures = (ActiveSupport::VERSION::MAJOR == 3 ? ActiveRecord::Fixtures : ActiveRecord::FixtureSet)
|
225
|
-
|
226
225
|
# reuse our pre-loaded fixtures even if we have a different connection
|
226
|
+
fixtures = ActiveRecord::FixtureSet
|
227
227
|
fixtures_eigenclass = class << fixtures; self; end
|
228
228
|
fixtures_eigenclass.send(:define_method, :cache_for_connection) do |_connection|
|
229
229
|
fixtures.class_variable_get(:@@all_cached_fixtures)[:unique]
|
@@ -247,13 +247,12 @@ module ForkingTestRunner
|
|
247
247
|
toggle_test_autorun true, file
|
248
248
|
end
|
249
249
|
|
250
|
-
def
|
250
|
+
def fork_with_captured_stdout
|
251
251
|
rpipe, wpipe = IO.pipe
|
252
252
|
|
253
253
|
child = fork do
|
254
254
|
rpipe.close
|
255
|
-
$stdout.reopen(wpipe)
|
256
|
-
|
255
|
+
preserve_tty { $stdout.reopen(wpipe) }
|
257
256
|
yield
|
258
257
|
end
|
259
258
|
|
@@ -263,18 +262,25 @@ module ForkingTestRunner
|
|
263
262
|
|
264
263
|
while ch = rpipe.read(1)
|
265
264
|
buffer << ch
|
266
|
-
$stdout.write(ch) if
|
265
|
+
$stdout.write(ch) if !@options.fetch(:quiet) && !@options.fetch(:parallel) # tee
|
267
266
|
end
|
268
267
|
|
269
268
|
Process.wait(child)
|
270
269
|
buffer
|
271
270
|
end
|
272
271
|
|
272
|
+
# not tested via CI
|
273
|
+
def preserve_tty
|
274
|
+
was_tty = $stdout.tty?
|
275
|
+
yield
|
276
|
+
def $stdout.tty?; true; end if was_tty
|
277
|
+
end
|
278
|
+
|
273
279
|
def run_test(file)
|
274
|
-
|
275
|
-
|
280
|
+
stdout = change_program_name_to file do
|
281
|
+
fork_with_captured_stdout do
|
276
282
|
SimpleCov.pid = Process.pid if defined?(SimpleCov) && SimpleCov.respond_to?(:pid=) # trick simplecov into reporting in this fork
|
277
|
-
if
|
283
|
+
if active_record?
|
278
284
|
key = (ActiveRecord::VERSION::STRING >= "4.1.0" ? :test : "test")
|
279
285
|
ActiveRecord::Base.establish_connection key
|
280
286
|
end
|
@@ -282,14 +288,17 @@ module ForkingTestRunner
|
|
282
288
|
end
|
283
289
|
end
|
284
290
|
|
285
|
-
[$?.success?,
|
291
|
+
[$?.success?, stdout]
|
286
292
|
end
|
287
293
|
|
288
294
|
def change_program_name_to(name)
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
295
|
+
return yield if @options.fetch(:parallel)
|
296
|
+
begin
|
297
|
+
old, $0 = $0, name
|
298
|
+
yield
|
299
|
+
ensure
|
300
|
+
$0 = old
|
301
|
+
end
|
293
302
|
end
|
294
303
|
|
295
304
|
def find_tests_for_group(group, group_count, tests, runtime_log)
|
@@ -310,21 +319,15 @@ module ForkingTestRunner
|
|
310
319
|
group.map { |test| [test, (tests[test] if group_by == :runtime)] }
|
311
320
|
end
|
312
321
|
|
313
|
-
def
|
322
|
+
def active_record?
|
314
323
|
!@options.fetch(:no_ar) && defined?(ActiveRecord::Base)
|
315
324
|
end
|
316
325
|
|
317
326
|
def minitest_class
|
318
327
|
@minitest_class ||= begin
|
319
328
|
require 'bundler/setup'
|
320
|
-
|
321
|
-
|
322
|
-
require 'minitest/unit'
|
323
|
-
MiniTest::Unit
|
324
|
-
else
|
325
|
-
require 'minitest'
|
326
|
-
Minitest
|
327
|
-
end
|
329
|
+
require 'minitest'
|
330
|
+
Minitest
|
328
331
|
end
|
329
332
|
end
|
330
333
|
|
@@ -344,89 +347,9 @@ module ForkingTestRunner
|
|
344
347
|
|
345
348
|
if value
|
346
349
|
minitest_class.autorun
|
347
|
-
|
350
|
+
load file
|
348
351
|
end
|
349
352
|
end
|
350
353
|
end
|
351
|
-
|
352
|
-
# we remove the args we understand and leave the rest alone
|
353
|
-
# so minitest / rspec can read their own options (--seed / -v ...)
|
354
|
-
# - keep our options clear / unambiguous to avoid overriding
|
355
|
-
# - read all serial non-flag arguments as tests and leave only unknown options behind
|
356
|
-
# - use .fetch everywhere to make sure nothing is misspelled
|
357
|
-
# GOOD: test --ours --theirs
|
358
|
-
# OK: --ours test --theirs
|
359
|
-
# BAD: --theirs test --ours
|
360
|
-
def parse_options(argv)
|
361
|
-
arguments = [
|
362
|
-
[:rspec, "--rspec", "RSpec mode"],
|
363
|
-
[:helper, "--helper", "Helper file to load before tests start", String],
|
364
|
-
[:quiet, "--quiet", "Quiet"],
|
365
|
-
[:no_fixtures, "--no-fixtures", "Do not load fixtures"],
|
366
|
-
[:no_ar, "--no-ar", "Disable ActiveRecord logic"],
|
367
|
-
[:merge_coverage, "--merge-coverage", "Merge base code coverage into indvidual files coverage, great for SingleCov"],
|
368
|
-
[
|
369
|
-
:record_runtime,
|
370
|
-
"--record-runtime=MODE",
|
371
|
-
"\n Record test runtime:\n" <<
|
372
|
-
" simple = write to disk at --runtime-log)\n" <<
|
373
|
-
" amend = write from multiple remote workers via http://github.com/grosser/amend, needs TRAVIS_REPO_SLUG & TRAVIS_BUILD_NUMBER",
|
374
|
-
String
|
375
|
-
],
|
376
|
-
[:runtime_log, "--runtime-log=FILE", "File to store runtime log in or runtime.log", String],
|
377
|
-
[:group, "--group=NUM", "What group this is (use with --groups / starts at 1)", Integer],
|
378
|
-
[:groups, "--groups=NUM", "How many groups there are in total (use with --group)", Integer],
|
379
|
-
[:version, "--version", "Show version"],
|
380
|
-
[:help, "--help", "Show help"]
|
381
|
-
]
|
382
|
-
|
383
|
-
options = arguments.each_with_object({}) do |(setting, flag, _, type), all|
|
384
|
-
all[setting] = delete_argv(flag.split('=', 2)[0], argv, type: type)
|
385
|
-
end
|
386
|
-
|
387
|
-
# show version
|
388
|
-
if options.fetch(:version)
|
389
|
-
puts VERSION
|
390
|
-
exit 0
|
391
|
-
end
|
392
|
-
|
393
|
-
# # show help
|
394
|
-
if options[:help]
|
395
|
-
parser = OptionParser.new("forking-test-runner folder [options]", 32, '') do |opts|
|
396
|
-
arguments.each do |_, flag, desc, type|
|
397
|
-
opts.on(flag, desc, type)
|
398
|
-
end
|
399
|
-
end
|
400
|
-
puts parser
|
401
|
-
exit 0
|
402
|
-
end
|
403
|
-
|
404
|
-
# check if we can use merge_coverage
|
405
|
-
if options.fetch(:merge_coverage)
|
406
|
-
abort "merge_coverage does not work on ruby prior to 2.3" if RUBY_VERSION < "2.3.0"
|
407
|
-
require 'coverage'
|
408
|
-
klass = (class << Coverage; self; end)
|
409
|
-
klass.prepend CoverageCapture
|
410
|
-
end
|
411
|
-
|
412
|
-
# all remaining non-flag options until the next flag must be tests
|
413
|
-
next_flag = argv.index { |arg| arg.start_with?("-") } || argv.size
|
414
|
-
tests = argv.slice!(0, next_flag)
|
415
|
-
abort "No tests or folders found in arguments" if tests.empty?
|
416
|
-
tests.each { |t| abort "Unable to find #{t}" unless File.exist?(t) }
|
417
|
-
|
418
|
-
[options, tests]
|
419
|
-
end
|
420
|
-
|
421
|
-
def delete_argv(name, argv, type: nil)
|
422
|
-
return unless index = argv.index(name)
|
423
|
-
argv.delete_at(index)
|
424
|
-
if type
|
425
|
-
found = argv.delete_at(index) || raise("Missing argument for #{name}")
|
426
|
-
send(type.name, found) # case found
|
427
|
-
else
|
428
|
-
true
|
429
|
-
end
|
430
|
-
end
|
431
354
|
end
|
432
355
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module ForkingTestRunner
|
2
|
+
# read and delete options we support and pass the rest through to the underlying test runner (-v / --seed etc)
|
3
|
+
module CLI
|
4
|
+
OPTIONS = [
|
5
|
+
[:rspec, "--rspec", "RSpec mode"],
|
6
|
+
[:helper, "--helper", "Helper file to load before tests start", String],
|
7
|
+
[:quiet, "--quiet", "Quiet"],
|
8
|
+
[:no_fixtures, "--no-fixtures", "Do not load fixtures"],
|
9
|
+
[:no_ar, "--no-ar", "Disable ActiveRecord logic"],
|
10
|
+
[:merge_coverage, "--merge-coverage", "Merge base code coverage into indvidual files coverage, great for SingleCov"],
|
11
|
+
[
|
12
|
+
:record_runtime,
|
13
|
+
"--record-runtime=MODE",
|
14
|
+
"\n Record test runtime:\n" <<
|
15
|
+
" simple = write to disk at --runtime-log)\n" <<
|
16
|
+
" amend = write from multiple remote workers via http://github.com/grosser/amend, needs TRAVIS_REPO_SLUG & TRAVIS_BUILD_NUMBER",
|
17
|
+
String
|
18
|
+
],
|
19
|
+
[:runtime_log, "--runtime-log=FILE", "File to store runtime log in or runtime.log", String],
|
20
|
+
[:parallel, "--parallel=NUM", "Number of parallel groups to run at once", Integer],
|
21
|
+
[:group, "--group=NUM[,NUM]", "What group this is (use with --groups / starts at 1)", String],
|
22
|
+
[:groups, "--groups=NUM", "How many groups there are in total (use with --group)", Integer],
|
23
|
+
[:version, "--version", "Show version"],
|
24
|
+
[:help, "--help", "Show help"]
|
25
|
+
]
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def parse_options(argv)
|
29
|
+
options = OPTIONS.each_with_object({}) do |(setting, flag, _, type), all|
|
30
|
+
all[setting] = delete_argv(flag.split('=', 2)[0], argv, type: type)
|
31
|
+
end
|
32
|
+
|
33
|
+
# show version
|
34
|
+
if options.fetch(:version)
|
35
|
+
puts VERSION
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
|
39
|
+
# show help
|
40
|
+
if options[:help]
|
41
|
+
puts help
|
42
|
+
exit 0
|
43
|
+
end
|
44
|
+
|
45
|
+
# check if we can use merge_coverage
|
46
|
+
if options.fetch(:merge_coverage)
|
47
|
+
abort "merge_coverage does not work on ruby prior to 2.3" if RUBY_VERSION < "2.3.0"
|
48
|
+
end
|
49
|
+
|
50
|
+
if !!options.fetch(:group) ^ !!options.fetch(:groups)
|
51
|
+
abort "use --group and --groups together"
|
52
|
+
end
|
53
|
+
|
54
|
+
# all remaining non-flag options until the next flag must be tests
|
55
|
+
next_flag = argv.index { |arg| arg.start_with?("-") } || argv.size
|
56
|
+
tests = argv.slice!(0, next_flag)
|
57
|
+
abort "No tests or folders found in arguments" if tests.empty?
|
58
|
+
tests.each { |t| abort "Unable to find #{t}" unless File.exist?(t) }
|
59
|
+
|
60
|
+
[options, tests]
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# fake parser that will print nicely
|
66
|
+
def help
|
67
|
+
OptionParser.new("forking-test-runner folder [options]", 32, '') do |opts|
|
68
|
+
OPTIONS.each do |_, flag, desc, type|
|
69
|
+
opts.on(flag, desc, type)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# we remove the args we understand and leave the rest alone
|
75
|
+
# so minitest / rspec can read their own options (--seed / -v ...)
|
76
|
+
# - keep our options clear / unambiguous to avoid overriding
|
77
|
+
# - read all serial non-flag arguments as tests and leave only unknown options behind
|
78
|
+
# - use .fetch everywhere to make sure nothing is misspelled
|
79
|
+
# GOOD: test --ours --theirs
|
80
|
+
# OK: --ours test --theirs
|
81
|
+
# BAD: --theirs test --ours
|
82
|
+
def delete_argv(name, argv, type: nil)
|
83
|
+
return unless index = argv.index(name)
|
84
|
+
argv.delete_at(index)
|
85
|
+
if type
|
86
|
+
found = argv.delete_at(index) || raise("Missing argument for #{name}")
|
87
|
+
send(type.name, found) # case found
|
88
|
+
else
|
89
|
+
true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ForkingTestRunner
|
2
|
+
module CoverageCapture
|
3
|
+
# override Coverage.result to add pre-fork captured coverage
|
4
|
+
def result
|
5
|
+
return super unless captured = CoverageCapture.coverage
|
6
|
+
CoverageCapture.merge_coverage(super, captured)
|
7
|
+
end
|
8
|
+
|
9
|
+
# deprecated, single_cov checks for this, so leave it here
|
10
|
+
def capture_coverage!
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :coverage
|
15
|
+
|
16
|
+
def activate!
|
17
|
+
require 'coverage'
|
18
|
+
(class << Coverage; self; end).prepend self
|
19
|
+
end
|
20
|
+
|
21
|
+
def capture!
|
22
|
+
self.coverage = Coverage.peek_result.dup
|
23
|
+
end
|
24
|
+
|
25
|
+
def merge_coverage(a, b)
|
26
|
+
merged = a.dup
|
27
|
+
b.each do |file, coverage|
|
28
|
+
orig = merged[file]
|
29
|
+
merged[file] = if orig
|
30
|
+
if coverage.is_a?(Array)
|
31
|
+
merge_lines_coverage(orig, coverage)
|
32
|
+
else
|
33
|
+
{
|
34
|
+
lines: merge_lines_coverage(orig.fetch(:lines), coverage.fetch(:lines)),
|
35
|
+
branches: merge_branches_coverage(orig.fetch(:branches), coverage.fetch(:branches))
|
36
|
+
}
|
37
|
+
end
|
38
|
+
else
|
39
|
+
coverage
|
40
|
+
end
|
41
|
+
end
|
42
|
+
merged
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# assuming b has same or more keys since it comes from a fork
|
48
|
+
# [nil,1,0] + [nil,nil,2] -> [nil,1,2]
|
49
|
+
def merge_lines_coverage(a, b)
|
50
|
+
b.each_with_index.map do |b_count, i|
|
51
|
+
a_count = a[i]
|
52
|
+
(a_count.nil? && b_count.nil?) ? nil : a_count.to_i + b_count.to_i
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# assuming b has same or more keys since it comes from a fork
|
57
|
+
# {foo: {bar: 0, baz: 1}} + {foo: {bar: 1, baz: 0}} -> {foo: {bar: 1, baz: 1}}
|
58
|
+
def merge_branches_coverage(a, b)
|
59
|
+
b.each_with_object({}) do |(branch, v), all|
|
60
|
+
vb = v.dup
|
61
|
+
if part = a[branch]
|
62
|
+
part.each do |nested, a_count|
|
63
|
+
vb[nested] = a_count + vb[nested].to_i
|
64
|
+
end
|
65
|
+
end
|
66
|
+
all[branch] = vb
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forking_test_runner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parallel_tests
|
@@ -118,6 +118,8 @@ files:
|
|
118
118
|
- MIT-LICENSE
|
119
119
|
- bin/forking-test-runner
|
120
120
|
- lib/forking_test_runner.rb
|
121
|
+
- lib/forking_test_runner/cli.rb
|
122
|
+
- lib/forking_test_runner/coverage_capture.rb
|
121
123
|
- lib/forking_test_runner/version.rb
|
122
124
|
homepage: https://github.com/grosser/forking_test_runner
|
123
125
|
licenses:
|
@@ -131,15 +133,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
131
133
|
requirements:
|
132
134
|
- - ">="
|
133
135
|
- !ruby/object:Gem::Version
|
134
|
-
version: 2.
|
136
|
+
version: 2.3.0
|
135
137
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
138
|
requirements:
|
137
139
|
- - ">="
|
138
140
|
- !ruby/object:Gem::Version
|
139
141
|
version: '0'
|
140
142
|
requirements: []
|
141
|
-
|
142
|
-
rubygems_version: 2.7.6
|
143
|
+
rubygems_version: 3.1.3
|
143
144
|
signing_key:
|
144
145
|
specification_version: 4
|
145
146
|
summary: Run every test in a fork to avoid pollution and get clean output per test
|