forking_test_runner 1.4.0 → 1.5.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 +124 -208
- 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: fc4844e1ccea576d0e7992efedab19587cb803ec04df7db7f82061253bada8fe
|
4
|
+
data.tar.gz: 2a8a12c8446e35c410417ac5271cd401d525d508a9b0db2c1c0674bd81d9275f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67e48c4c7e3ac01664bfb0b21bf8339875a1fed6b8a40a75f546cd849ad165df3346afaf641035317ff0be8d553ab745f2cc7e14fb3d747c5849db01d5bab1d9
|
7
|
+
data.tar.gz: 59c63fcf19c1a472804504987d31bfe01e85805453db87104068279227a7b83391e9cc74323f412033d1fbe83a5a9b4304805b249f275539ba2367a3478e2460
|
data/lib/forking_test_runner.rb
CHANGED
@@ -1,130 +1,74 @@
|
|
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
|
+
groups.map { |group| find_tests_for_group(group, group_count, tests, runtime_log) }
|
25
|
+
end
|
80
26
|
|
27
|
+
# say what we are running
|
28
|
+
all_tests = test_groups.flatten(1)
|
81
29
|
if @options.fetch(:quiet)
|
82
|
-
puts "Running #{
|
30
|
+
puts "Running #{all_tests.size} test files"
|
83
31
|
else
|
84
|
-
puts "Running tests #{
|
32
|
+
puts "Running tests #{all_tests.map(&:first).join(" ")}"
|
85
33
|
end
|
86
34
|
|
87
|
-
if ar?
|
88
|
-
preload_fixtures
|
89
|
-
ActiveRecord::Base.connection.disconnect!
|
90
|
-
end
|
91
|
-
|
92
|
-
Coverage.capture_coverage! if @options.fetch(:merge_coverage)
|
93
|
-
|
94
35
|
# 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
|
36
|
+
results = with_lock do |lock|
|
37
|
+
Parallel.map_with_index(test_groups, in_processes: parallel || 0) do |tests, env_index|
|
38
|
+
if parallel
|
39
|
+
ENV["TEST_ENV_NUMBER"] = (env_index == 0 ? '' : (env_index + 1).to_s) # NOTE: does not support first_is_1 option
|
40
|
+
end
|
104
41
|
|
105
|
-
|
106
|
-
puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
|
107
|
-
end
|
42
|
+
reraise_clean_ar_error { load_test_env }
|
108
43
|
|
109
|
-
|
44
|
+
tests.map do |file, expected|
|
45
|
+
print_started file unless parallel
|
46
|
+
result = [file, expected, *benchmark { run_test(file) }]
|
47
|
+
sync_stdout lock do
|
48
|
+
print_started file if parallel
|
49
|
+
print_finished *result
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end.flatten(1)
|
110
54
|
end
|
111
55
|
|
112
56
|
unless @options.fetch(:quiet)
|
113
57
|
# pretty print the results
|
114
58
|
puts "\nResults:"
|
115
59
|
puts results.
|
116
|
-
sort_by { |_,_,_,_
|
117
|
-
map { |f,_,_,_
|
60
|
+
sort_by { |_,_,_,r,_| r ? 0 : 1 }. # failures should be last so they are easy to find
|
61
|
+
map { |f,_,_,r,_| "#{f}: #{r ? "OK" : "Fail"}"}
|
118
62
|
puts
|
119
63
|
end
|
120
64
|
|
121
|
-
success = results.map
|
65
|
+
success = results.map { |r| r[3] }.all?
|
122
66
|
|
123
|
-
puts colorize(success, summarize_results(results.map { |r| r[
|
67
|
+
puts colorize(success, summarize_results(results.map { |r| r[4] }))
|
124
68
|
|
125
69
|
if runtime_log
|
126
70
|
# show how long they ran vs expected
|
127
|
-
diff = results.map { |_,time
|
71
|
+
diff = results.map { |_, expected, time| time - expected }.inject(:+).to_f
|
128
72
|
puts "Time: #{diff.round(2)} diff to expected"
|
129
73
|
end
|
130
74
|
|
@@ -140,6 +84,38 @@ module ForkingTestRunner
|
|
140
84
|
|
141
85
|
private
|
142
86
|
|
87
|
+
def with_lock(&block)
|
88
|
+
return yield unless @options.fetch(:parallel)
|
89
|
+
Tempfile.open"forking-test-runner-lock", &block
|
90
|
+
end
|
91
|
+
|
92
|
+
def sync_stdout(lock)
|
93
|
+
return yield unless @options.fetch(:parallel)
|
94
|
+
begin
|
95
|
+
lock.flock(File::LOCK_EX)
|
96
|
+
yield
|
97
|
+
ensure
|
98
|
+
lock.flock(File::LOCK_UN)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def print_started(file)
|
103
|
+
puts "#{CLEAR} >>> #{file}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def print_finished(file, expected, time, success, stdout)
|
107
|
+
# print stdout if it was not shown before, but needs to be shown
|
108
|
+
puts stdout if (!success && @options.fetch(:quiet)) || (@options.fetch(:parallel) && !@options.fetch(:quiet))
|
109
|
+
|
110
|
+
if @options.fetch(:runtime_log) && !@options.fetch(:quiet)
|
111
|
+
puts "Time: expected #{expected.round(2)}, actual #{time.round(2)}"
|
112
|
+
end
|
113
|
+
|
114
|
+
if !success || !@options.fetch(:quiet)
|
115
|
+
puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
143
119
|
def colorize(green, string)
|
144
120
|
if $stdout.tty?
|
145
121
|
"\e[#{green ? 32 : 31}m#{string}\e[0m"
|
@@ -162,15 +138,13 @@ module ForkingTestRunner
|
|
162
138
|
|
163
139
|
def benchmark
|
164
140
|
result = false
|
165
|
-
time = Benchmark.realtime
|
166
|
-
|
167
|
-
end
|
168
|
-
return [time, result].flatten
|
141
|
+
time = Benchmark.realtime { result = yield }
|
142
|
+
[time, *result]
|
169
143
|
end
|
170
144
|
|
171
145
|
# log runtime via dumping or curling it into the runtime log location
|
172
146
|
def record_test_runtime(mode, results, log)
|
173
|
-
data = results.map { |test, time| "#{test}:#{time.round(2)}" }.join("\n") << "\n"
|
147
|
+
data = results.map { |test, _, time| "#{test}:#{time.round(2)}" }.join("\n") << "\n"
|
174
148
|
|
175
149
|
case mode
|
176
150
|
when 'simple'
|
@@ -198,21 +172,47 @@ module ForkingTestRunner
|
|
198
172
|
end
|
199
173
|
|
200
174
|
def find_group_args
|
201
|
-
|
175
|
+
group = @options.fetch(:group)
|
176
|
+
groups = @options.fetch(:groups)
|
177
|
+
if group && groups
|
202
178
|
# delete options we want while leaving others as they are (-v / --seed etc)
|
203
|
-
group
|
204
|
-
group_count = @options.fetch(:groups)
|
179
|
+
[group.split(",").map { |g| Integer(g) }, groups]
|
205
180
|
else
|
206
|
-
|
207
|
-
|
181
|
+
[[1], 1]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def load_test_env
|
186
|
+
CoverageCapture.activate! if @options.fetch(:merge_coverage)
|
187
|
+
|
188
|
+
load_test_helper
|
189
|
+
|
190
|
+
if active_record?
|
191
|
+
preload_fixtures
|
192
|
+
ActiveRecord::Base.connection.disconnect!
|
208
193
|
end
|
209
194
|
|
210
|
-
|
195
|
+
CoverageCapture.capture! if @options.fetch(:merge_coverage)
|
211
196
|
end
|
212
197
|
|
213
|
-
def
|
198
|
+
def reraise_clean_ar_error
|
199
|
+
return yield unless @options.fetch(:parallel)
|
200
|
+
|
201
|
+
e = begin
|
202
|
+
yield
|
203
|
+
nil
|
204
|
+
rescue
|
205
|
+
$!
|
206
|
+
end
|
207
|
+
|
208
|
+
# needs to be done outside of the rescue block to avoid inheriting the cause
|
209
|
+
raise RuntimeError, "Re-raised error from test helper: #{e.message}", e.backtrace if e
|
210
|
+
end
|
211
|
+
|
212
|
+
def load_test_helper
|
213
|
+
disable_test_autorun
|
214
214
|
require 'rspec/core' if @options.fetch(:rspec)
|
215
|
-
helper =
|
215
|
+
helper = @options.fetch(:helper) || (@options.fetch(:rspec) ? "spec/spec_helper" : "test/test_helper")
|
216
216
|
require "./#{helper}"
|
217
217
|
end
|
218
218
|
|
@@ -221,9 +221,8 @@ module ForkingTestRunner
|
|
221
221
|
def preload_fixtures
|
222
222
|
return if @options.fetch(:no_fixtures)
|
223
223
|
|
224
|
-
fixtures = (ActiveSupport::VERSION::MAJOR == 3 ? ActiveRecord::Fixtures : ActiveRecord::FixtureSet)
|
225
|
-
|
226
224
|
# reuse our pre-loaded fixtures even if we have a different connection
|
225
|
+
fixtures = ActiveRecord::FixtureSet
|
227
226
|
fixtures_eigenclass = class << fixtures; self; end
|
228
227
|
fixtures_eigenclass.send(:define_method, :cache_for_connection) do |_connection|
|
229
228
|
fixtures.class_variable_get(:@@all_cached_fixtures)[:unique]
|
@@ -247,7 +246,7 @@ module ForkingTestRunner
|
|
247
246
|
toggle_test_autorun true, file
|
248
247
|
end
|
249
248
|
|
250
|
-
def
|
249
|
+
def fork_with_captured_stdout
|
251
250
|
rpipe, wpipe = IO.pipe
|
252
251
|
|
253
252
|
child = fork do
|
@@ -263,7 +262,7 @@ 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)
|
@@ -271,10 +270,10 @@ module ForkingTestRunner
|
|
271
270
|
end
|
272
271
|
|
273
272
|
def run_test(file)
|
274
|
-
|
275
|
-
|
273
|
+
stdout = change_program_name_to file do
|
274
|
+
fork_with_captured_stdout do
|
276
275
|
SimpleCov.pid = Process.pid if defined?(SimpleCov) && SimpleCov.respond_to?(:pid=) # trick simplecov into reporting in this fork
|
277
|
-
if
|
276
|
+
if active_record?
|
278
277
|
key = (ActiveRecord::VERSION::STRING >= "4.1.0" ? :test : "test")
|
279
278
|
ActiveRecord::Base.establish_connection key
|
280
279
|
end
|
@@ -282,14 +281,17 @@ module ForkingTestRunner
|
|
282
281
|
end
|
283
282
|
end
|
284
283
|
|
285
|
-
[$?.success?,
|
284
|
+
[$?.success?, stdout]
|
286
285
|
end
|
287
286
|
|
288
287
|
def change_program_name_to(name)
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
288
|
+
return yield if @options.fetch(:parallel)
|
289
|
+
begin
|
290
|
+
old, $0 = $0, name
|
291
|
+
yield
|
292
|
+
ensure
|
293
|
+
$0 = old
|
294
|
+
end
|
293
295
|
end
|
294
296
|
|
295
297
|
def find_tests_for_group(group, group_count, tests, runtime_log)
|
@@ -310,21 +312,15 @@ module ForkingTestRunner
|
|
310
312
|
group.map { |test| [test, (tests[test] if group_by == :runtime)] }
|
311
313
|
end
|
312
314
|
|
313
|
-
def
|
315
|
+
def active_record?
|
314
316
|
!@options.fetch(:no_ar) && defined?(ActiveRecord::Base)
|
315
317
|
end
|
316
318
|
|
317
319
|
def minitest_class
|
318
320
|
@minitest_class ||= begin
|
319
321
|
require 'bundler/setup'
|
320
|
-
|
321
|
-
|
322
|
-
require 'minitest/unit'
|
323
|
-
MiniTest::Unit
|
324
|
-
else
|
325
|
-
require 'minitest'
|
326
|
-
Minitest
|
327
|
-
end
|
322
|
+
require 'minitest'
|
323
|
+
Minitest
|
328
324
|
end
|
329
325
|
end
|
330
326
|
|
@@ -348,85 +344,5 @@ module ForkingTestRunner
|
|
348
344
|
end
|
349
345
|
end
|
350
346
|
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
347
|
end
|
432
348
|
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.5.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: 2019-02
|
11
|
+
date: 2019-09-02 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.0.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
|