forking_test_runner 1.2.0 → 1.13.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
- 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