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 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