forking_test_runner 1.4.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24a22080e7feec720e83c73e48241d6984b0c7d769cce74f7687f563f1b1e75d
4
- data.tar.gz: 2a0b5a4bb46cad79e4f7de3cb9b37bdd1307d7b9754b27397686349278be9ae6
3
+ metadata.gz: 5941690d477f2dcded481eb639078719303e399fce77bf0f3dfd4a8d29281466
4
+ data.tar.gz: 4d007910130e2d2f7444df0bad571f23d623dbe63609e3f5389bdd2ebfa838ce
5
5
  SHA512:
6
- metadata.gz: ca2e09cb43119ea4c62260fa8487d7c93e1b3a935a2dbbc1c5a219166c7891cdec55f8b95cf30610f83319334efb17739e505f9132b4dbdc6e7b78e59346e3fb
7
- data.tar.gz: 3385e2157a115e09810ff55cb50d5bb24e07be53effc1bf4b7362b434215ed6c4111ce26a379955dfbb540bf27a7e2ab954fff97080e8c6d9cc782e0ea8a222e
6
+ metadata.gz: bc9419382a7dadaa81fad1b498d2ed3b5cd6b8c4bfa4727b1bddee0203b3ed1a87133218fd4610e2adf94043cb2be07ac733fc92613ffc9d83cfef05468c330c
7
+ data.tar.gz: 247bec8782245d23d487e32e2b65a1bd8eee0f04458abf8f66c14cec083de865f4cc107af370b39f837dcc674f18d3c67075eb293616ea2a4a6f79457b14d2ef
@@ -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
- group, group_count = find_group_args
79
- tests = find_tests_for_group(group, group_count, tests, runtime_log)
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 #{tests.size} test files"
31
+ puts "Running #{all_tests.size} test files"
83
32
  else
84
- puts "Running tests #{tests.map(&:first).join(" ")}"
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 = tests.map do |file, expected|
96
- puts "#{CLEAR} >>> #{file} "
97
- time, success, output = benchmark { run_test(file) }
98
-
99
- puts output if !success && @options.fetch(:quiet)
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
- if !success || !@options.fetch(:quiet)
106
- puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
107
- end
43
+ reraise_clean_ar_error { load_test_env }
108
44
 
109
- [file, time, expected, output, success]
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 { |_,_,_,_,r| r ? 0 : 1 }. # failures should be last so they are easy to find
117
- map { |f,_,_,_,r| "#{f}: #{r ? "OK" : "Fail"}"}
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(&:last).all?
66
+ success = results.map { |r| r[3] }.all?
122
67
 
123
- puts colorize(success, summarize_results(results.map { |r| r[3] }))
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,expected| time - expected }.inject(:+).to_f
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 do
166
- result = yield
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
- if @options.fetch(:group) && @options.fetch(:groups)
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 = @options.fetch(:group)
204
- group_count = @options.fetch(:groups)
180
+ [group.split(",").map { |g| Integer(g) }, groups]
205
181
  else
206
- group = 1
207
- group_count = 1
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
- [group, group_count]
196
+ CoverageCapture.capture! if @options.fetch(:merge_coverage)
211
197
  end
212
198
 
213
- def load_test_env(helper=nil)
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 = helper || (@options.fetch(:rspec) ? "spec/spec_helper" : "test/test_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 fork_with_captured_output(tee_to_stdout)
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 tee_to_stdout
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
- output = change_program_name_to file do
275
- fork_with_captured_output(!@options.fetch(:quiet)) do
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 ar?
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?, output]
291
+ [$?.success?, stdout]
286
292
  end
287
293
 
288
294
  def change_program_name_to(name)
289
- old, $0 = $0, name
290
- yield
291
- ensure
292
- $0 = old
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 ar?
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
- gem 'minitest'
321
- if Gem.loaded_specs["minitest"].version.segments.first == 4 # 4.x
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
- require(file.start_with?('/') ? file : "./#{file}")
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
@@ -1,3 +1,3 @@
1
1
  module ForkingTestRunner
2
- VERSION = "1.4.0"
2
+ VERSION = "1.7.0"
3
3
  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.0
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: 2019-02-25 00:00:00.000000000 Z
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.0.0
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
- rubyforge_project:
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