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