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 +5 -5
- data/lib/forking_test_runner/cli.rb +94 -0
- data/lib/forking_test_runner/coverage_capture.rb +71 -0
- data/lib/forking_test_runner/version.rb +1 -1
- data/lib/forking_test_runner.rb +184 -208
- metadata +6 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6d802108391cd0edbb4c9a9628ca56d447dbc07992f07cf8c1ffc1e61d770d5a
|
4
|
+
data.tar.gz: cabbf4e263465b03e1bc47435b15ce5186c3478f409095f57d2197e49ca5e0e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/forking_test_runner.rb
CHANGED
@@ -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
|
-
|
15
|
+
attr_accessor :before_fork_callbacks, :after_fork_callbacks
|
72
16
|
|
73
|
-
|
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
|
-
|
78
|
-
|
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 #{
|
35
|
+
puts "Running #{all_tests.size} test files"
|
82
36
|
else
|
83
|
-
puts "Running tests #{
|
37
|
+
puts "Running tests #{all_tests.map(&:first).join(" ")}"
|
84
38
|
end
|
85
39
|
|
86
|
-
|
87
|
-
|
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 =
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
105
|
-
puts "#{CLEAR} <<< #{file} ---- #{success ? "OK" : "Failed"}"
|
106
|
-
end
|
50
|
+
reraise_clean_ar_error { load_test_env }
|
107
51
|
|
108
|
-
|
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 { |_,_,_,_
|
116
|
-
map { |f,_,_,_
|
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
|
73
|
+
success = results.map { |r| r[3] }.all?
|
121
74
|
|
122
|
-
puts colorize(success, summarize_results(results.map { |r| r[
|
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
|
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
|
165
|
-
|
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
|
-
|
179
|
-
|
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
|
-
|
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
|
198
|
-
group_count = @options.fetch(:groups)
|
189
|
+
[group.split(",").map { |g| Integer(g) }, groups]
|
199
190
|
else
|
200
|
-
|
201
|
-
|
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
|
-
|
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
|
223
|
+
def load_test_helper
|
224
|
+
disable_test_autorun
|
208
225
|
require 'rspec/core' if @options.fetch(:rspec)
|
209
|
-
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
|
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
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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?,
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
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
|
-
|
315
|
-
|
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
|
-
|
373
|
+
load file
|
342
374
|
end
|
343
375
|
end
|
344
376
|
end
|
345
377
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
381
|
-
|
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
|
-
|
415
|
-
|
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.
|
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:
|
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.
|
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
|
-
|
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
|