minitest-distributed 0.1.2 → 0.2.2
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 +4 -4
- data/.rubocop.yml +4 -0
- data/Gemfile +2 -2
- data/README.md +35 -13
- data/bin/setup +0 -2
- data/lib/minitest/distributed/configuration.rb +66 -4
- data/lib/minitest/distributed/coordinators/coordinator_interface.rb +3 -0
- data/lib/minitest/distributed/coordinators/memory_coordinator.rb +30 -9
- data/lib/minitest/distributed/coordinators/redis_coordinator.rb +259 -154
- data/lib/minitest/distributed/enqueued_runnable.rb +197 -40
- data/lib/minitest/distributed/filters/exclude_file_filter.rb +18 -0
- data/lib/minitest/distributed/filters/exclude_filter.rb +4 -4
- data/lib/minitest/distributed/filters/file_filter_base.rb +29 -0
- data/lib/minitest/distributed/filters/filter_interface.rb +3 -3
- data/lib/minitest/distributed/filters/include_file_filter.rb +18 -0
- data/lib/minitest/distributed/filters/include_filter.rb +4 -4
- data/lib/minitest/distributed/reporters/distributed_progress_reporter.rb +13 -5
- data/lib/minitest/distributed/reporters/distributed_summary_reporter.rb +49 -10
- data/lib/minitest/distributed/reporters/junitxml_reporter.rb +150 -0
- data/lib/minitest/distributed/reporters/redis_coordinator_warnings_reporter.rb +11 -16
- data/lib/minitest/distributed/result_aggregate.rb +38 -9
- data/lib/minitest/distributed/result_type.rb +76 -2
- data/lib/minitest/distributed/test_selector.rb +12 -6
- data/lib/minitest/distributed/version.rb +1 -1
- data/lib/minitest/distributed.rb +3 -0
- data/lib/minitest/distributed_plugin.rb +1 -25
- data/lib/minitest/junitxml_plugin.rb +21 -0
- data/sorbet/rbi/minitest.rbi +29 -11
- data/sorbet/rbi/redis.rbi +19 -4
- metadata +11 -7
- data/.travis.yml +0 -6
@@ -3,86 +3,243 @@
|
|
3
3
|
|
4
4
|
module Minitest
|
5
5
|
module Distributed
|
6
|
+
class PendingExecution < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :worker_id, String
|
10
|
+
const :entry_id, String
|
11
|
+
const :elapsed_time_ms, Integer
|
12
|
+
const :attempt, Integer
|
13
|
+
|
14
|
+
sig { returns(String) }
|
15
|
+
def attempt_id
|
16
|
+
"#{entry_id}/#{attempt}"
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { params(xpending_result: T::Hash[String, T.untyped]).returns(T.attached_class) }
|
20
|
+
def self.from_xpending(xpending_result)
|
21
|
+
new(
|
22
|
+
worker_id: xpending_result.fetch('consumer'),
|
23
|
+
entry_id: xpending_result.fetch('entry_id'),
|
24
|
+
elapsed_time_ms: xpending_result.fetch('elapsed'),
|
25
|
+
attempt: xpending_result.fetch('count'),
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# This module defines some helper methods to deal with Minitest::Runnable
|
31
|
+
module DefinedRunnable
|
32
|
+
extend T::Sig
|
33
|
+
|
34
|
+
sig { params(name: String).returns(T.class_of(Minitest::Runnable)) }
|
35
|
+
def self.find_class(name)
|
36
|
+
name.split('::')
|
37
|
+
.reduce(Object) { |ns, const| ns.const_get(const) } # rubocop:disable Sorbet/ConstantsFromStrings
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(runnable: Minitest::Runnable).returns(String) }
|
41
|
+
def self.identifier(runnable)
|
42
|
+
"#{T.must(runnable.class.name)}##{runnable.name}"
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(identifier: String).returns(Minitest::Runnable) }
|
46
|
+
def self.from_identifier(identifier)
|
47
|
+
class_name, method_name = identifier.split('#', 2)
|
48
|
+
find_class(T.must(class_name)).new(T.must(method_name))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
6
52
|
class EnqueuedRunnable < T::Struct
|
7
|
-
class
|
53
|
+
class Result < T::Struct
|
54
|
+
class Commit
|
55
|
+
extend T::Sig
|
56
|
+
|
57
|
+
sig { params(block: T.proc.returns(T::Boolean)).void }
|
58
|
+
def initialize(&block)
|
59
|
+
@block = block
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { returns(T::Boolean) }
|
63
|
+
def success?
|
64
|
+
@success = T.let(@success, T.nilable(T::Boolean))
|
65
|
+
@success ||= @block.call
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { returns(T::Boolean) }
|
69
|
+
def failure?
|
70
|
+
!success?
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { returns(Commit) }
|
74
|
+
def self.success
|
75
|
+
@success = T.let(@success, T.nilable(Commit))
|
76
|
+
@success ||= new { true }
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { returns(Commit) }
|
80
|
+
def self.failure
|
81
|
+
@failure = T.let(@failure, T.nilable(Commit))
|
82
|
+
@failure ||= new { false }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
8
86
|
extend T::Sig
|
9
87
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
88
|
+
const :enqueued_runnable, EnqueuedRunnable
|
89
|
+
const :initial_result, Minitest::Result
|
90
|
+
const :commit, Commit
|
91
|
+
|
92
|
+
sig { returns(String) }
|
93
|
+
def entry_id
|
94
|
+
enqueued_runnable.entry_id
|
17
95
|
end
|
18
96
|
|
19
|
-
sig {
|
20
|
-
def
|
21
|
-
|
22
|
-
class_name: T.must(runnable.class.name),
|
23
|
-
method_name: runnable.name,
|
24
|
-
)
|
97
|
+
sig { returns(T::Boolean) }
|
98
|
+
def final?
|
99
|
+
!requeue?
|
25
100
|
end
|
26
101
|
|
27
|
-
sig {
|
28
|
-
def
|
29
|
-
|
102
|
+
sig { returns(T::Boolean) }
|
103
|
+
def requeue?
|
104
|
+
ResultType.of(initial_result) == ResultType::Requeued
|
30
105
|
end
|
31
106
|
|
32
|
-
sig {
|
33
|
-
def
|
34
|
-
|
107
|
+
sig { returns(Minitest::Result) }
|
108
|
+
def committed_result
|
109
|
+
@committed_result = T.let(@committed_result, T.nilable(Minitest::Result))
|
110
|
+
@committed_result ||= if final? && commit.failure?
|
111
|
+
# If a runnable result is final, but the acked failed, we will discard the result.
|
112
|
+
Minitest::Discard.wrap(initial_result, test_timeout_seconds: enqueued_runnable.test_timeout_seconds)
|
113
|
+
else
|
114
|
+
initial_result
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class << self
|
120
|
+
extend T::Sig
|
121
|
+
|
122
|
+
sig do
|
123
|
+
params(
|
124
|
+
claims: T::Array[[String, T::Hash[String, String]]],
|
125
|
+
pending_messages: T::Hash[String, PendingExecution],
|
126
|
+
configuration: Configuration,
|
127
|
+
).returns(T::Array[T.attached_class])
|
128
|
+
end
|
129
|
+
def from_redis_stream_claim(claims, pending_messages = {}, configuration:)
|
130
|
+
claims.map do |entry_id, runnable_method_info|
|
131
|
+
# `attempt` will be set to the current attempt of a different worker that has timed out.
|
132
|
+
# The attempt we are going to try will be the next one, so add one.
|
133
|
+
attempt = pending_messages.key?(entry_id) ? pending_messages.fetch(entry_id).attempt + 1 : 1
|
134
|
+
|
35
135
|
new(
|
36
136
|
class_name: runnable_method_info.fetch('class_name'),
|
37
137
|
method_name: runnable_method_info.fetch('method_name'),
|
38
|
-
|
138
|
+
entry_id: entry_id,
|
139
|
+
attempt: attempt,
|
140
|
+
max_attempts: configuration.max_attempts,
|
141
|
+
test_timeout_seconds: configuration.test_timeout_seconds,
|
39
142
|
)
|
40
143
|
end
|
41
144
|
end
|
42
|
-
|
43
|
-
sig { params(name: String).returns(T.class_of(Minitest::Runnable)) }
|
44
|
-
def find_runnable_class(name)
|
45
|
-
name.split('::')
|
46
|
-
.reduce(Object) { |ns, const| ns.const_get(const) } # rubocop:disable Sorbet/ConstantsFromStrings
|
47
|
-
end
|
48
145
|
end
|
49
146
|
|
50
147
|
extend T::Sig
|
51
148
|
|
52
149
|
const :class_name, String
|
53
150
|
const :method_name, String
|
54
|
-
const :
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
prop :canned_failure, T.nilable(Minitest::Assertion), dont_store: true
|
151
|
+
const :entry_id, String, factory: -> { SecureRandom.uuid }, dont_store: true
|
152
|
+
const :attempt, Integer, default: 1, dont_store: true
|
153
|
+
const :max_attempts, Integer, dont_store: true
|
154
|
+
const :test_timeout_seconds, Float, dont_store: true
|
59
155
|
|
60
156
|
sig { returns(String) }
|
61
157
|
def identifier
|
62
158
|
"#{class_name}##{method_name}"
|
63
159
|
end
|
64
160
|
|
161
|
+
sig { returns(String) }
|
162
|
+
def attempt_id
|
163
|
+
"#{entry_id}/#{attempt}"
|
164
|
+
end
|
165
|
+
|
65
166
|
sig { returns(T.class_of(Minitest::Runnable)) }
|
66
167
|
def runnable_class
|
67
|
-
|
168
|
+
DefinedRunnable.find_class(class_name)
|
68
169
|
end
|
69
170
|
|
70
171
|
sig { returns(Minitest::Runnable) }
|
71
|
-
def
|
172
|
+
def instantiate_runnable
|
72
173
|
runnable_class.new(method_name)
|
73
174
|
end
|
74
175
|
|
176
|
+
sig { returns(T::Boolean) }
|
177
|
+
def attempts_exhausted?
|
178
|
+
attempt > max_attempts
|
179
|
+
end
|
180
|
+
|
181
|
+
sig { returns(T::Boolean) }
|
182
|
+
def final_attempt?
|
183
|
+
attempt == max_attempts
|
184
|
+
end
|
185
|
+
|
186
|
+
sig { returns(Minitest::Result) }
|
187
|
+
def attempts_exhausted_result
|
188
|
+
assertion = Minitest::AttemptsExhausted.new(<<~EOM.chomp)
|
189
|
+
This test takes too long to run (> #{test_timeout_seconds}s).
|
190
|
+
|
191
|
+
We have tried running this test #{max_attempts} on different workers, but every time the worker has not reported back a result within #{test_timeout_seconds}s.
|
192
|
+
Try to make the test faster, or increase the test timeout.
|
193
|
+
EOM
|
194
|
+
assertion.set_backtrace(caller)
|
195
|
+
|
196
|
+
runnable = instantiate_runnable
|
197
|
+
runnable.time = 0.0
|
198
|
+
runnable.failures = [assertion]
|
199
|
+
|
200
|
+
Minitest::Result.from(runnable)
|
201
|
+
end
|
202
|
+
|
203
|
+
sig do
|
204
|
+
params(
|
205
|
+
initial_result: Minitest::Result,
|
206
|
+
block: T.proc.params(arg0: Minitest::Result).returns(EnqueuedRunnable::Result::Commit)
|
207
|
+
).returns(EnqueuedRunnable::Result)
|
208
|
+
end
|
209
|
+
def commit_result(initial_result, &block)
|
210
|
+
EnqueuedRunnable::Result.new(
|
211
|
+
enqueued_runnable: self,
|
212
|
+
initial_result: initial_result,
|
213
|
+
commit: block.call(initial_result),
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
75
217
|
sig { returns(Minitest::Result) }
|
76
218
|
def run
|
77
|
-
if
|
78
|
-
|
79
|
-
canned_runnable.time = 0.0
|
80
|
-
canned_runnable.failures << canned_failure
|
81
|
-
Minitest::Result.from(canned_runnable)
|
219
|
+
if attempts_exhausted?
|
220
|
+
attempts_exhausted_result
|
82
221
|
else
|
83
|
-
Minitest.run_one_method(runnable_class, method_name)
|
222
|
+
result = Minitest.run_one_method(runnable_class, method_name)
|
223
|
+
result_type = ResultType.of(result)
|
224
|
+
if (result_type == ResultType::Error || result_type == ResultType::Failed) && !final_attempt?
|
225
|
+
Minitest::Requeue.wrap(result, attempt: attempt, max_attempts: max_attempts)
|
226
|
+
else
|
227
|
+
result
|
228
|
+
end
|
84
229
|
end
|
85
230
|
end
|
231
|
+
|
232
|
+
sig { returns(T.self_type) }
|
233
|
+
def next_attempt
|
234
|
+
self.class.new(
|
235
|
+
class_name: class_name,
|
236
|
+
method_name: method_name,
|
237
|
+
entry_id: entry_id,
|
238
|
+
attempt: attempt + 1,
|
239
|
+
max_attempts: max_attempts,
|
240
|
+
test_timeout_seconds: test_timeout_seconds,
|
241
|
+
)
|
242
|
+
end
|
86
243
|
end
|
87
244
|
end
|
88
245
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Distributed
|
6
|
+
module Filters
|
7
|
+
class ExcludeFileFilter < FileFilterBase
|
8
|
+
extend T::Sig
|
9
|
+
include FilterInterface
|
10
|
+
|
11
|
+
sig { override.params(runnable: Minitest::Runnable).returns(T::Array[Runnable]) }
|
12
|
+
def call(runnable)
|
13
|
+
tests.include?(DefinedRunnable.identifier(runnable)) ? [] : [runnable]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -19,13 +19,13 @@ module Minitest
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
sig { override.params(
|
23
|
-
def call(
|
22
|
+
sig { override.params(runnable: Minitest::Runnable).returns(T::Array[Runnable]) }
|
23
|
+
def call(runnable)
|
24
24
|
# rubocop:disable Style/CaseEquality
|
25
|
-
if filter ===
|
25
|
+
if filter === runnable.name || filter === DefinedRunnable.identifier(runnable)
|
26
26
|
[]
|
27
27
|
else
|
28
|
-
[
|
28
|
+
[runnable]
|
29
29
|
end
|
30
30
|
# rubocop:enable Style/CaseEquality
|
31
31
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Distributed
|
6
|
+
module Filters
|
7
|
+
class FileFilterBase
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(Pathname) }
|
11
|
+
attr_reader :file
|
12
|
+
|
13
|
+
sig { params(file: Pathname).void }
|
14
|
+
def initialize(file)
|
15
|
+
@file = file
|
16
|
+
@tests = T.let(nil, T.nilable(T::Set[String]))
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { returns(T::Set[String]) }
|
20
|
+
def tests
|
21
|
+
@tests ||= begin
|
22
|
+
tests = File.readlines(@file, chomp: true)
|
23
|
+
Set.new(tests)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -9,7 +9,7 @@ module Minitest
|
|
9
9
|
# array of runnables.
|
10
10
|
#
|
11
11
|
# - If it returns an empty array, the runnable will not be run.
|
12
|
-
# - If it returns a single
|
12
|
+
# - If it returns a single element array with the passed ion runnable to make no changes.
|
13
13
|
# - It can return an array of enumerables to expand the number of runnables in this test run,
|
14
14
|
# We use this for grinding tests, for instance.
|
15
15
|
module FilterInterface
|
@@ -17,8 +17,8 @@ module Minitest
|
|
17
17
|
extend T::Helpers
|
18
18
|
interface!
|
19
19
|
|
20
|
-
sig { abstract.params(
|
21
|
-
def call(
|
20
|
+
sig { abstract.params(runnable: Minitest::Runnable).returns(T::Array[Minitest::Runnable]) }
|
21
|
+
def call(runnable); end
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Distributed
|
6
|
+
module Filters
|
7
|
+
class IncludeFileFilter < FileFilterBase
|
8
|
+
extend T::Sig
|
9
|
+
include FilterInterface
|
10
|
+
|
11
|
+
sig { override.params(runnable: Minitest::Runnable).returns(T::Array[Runnable]) }
|
12
|
+
def call(runnable)
|
13
|
+
tests.include?(DefinedRunnable.identifier(runnable)) ? [runnable] : []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -19,11 +19,11 @@ module Minitest
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
sig { override.params(
|
23
|
-
def call(
|
22
|
+
sig { override.params(runnable: Minitest::Runnable).returns(T::Array[Minitest::Runnable]) }
|
23
|
+
def call(runnable)
|
24
24
|
# rubocop:disable Style/CaseEquality
|
25
|
-
if filter ===
|
26
|
-
[
|
25
|
+
if filter === runnable.name || filter === DefinedRunnable.identifier(runnable)
|
26
|
+
[runnable]
|
27
27
|
else
|
28
28
|
[]
|
29
29
|
end
|
@@ -20,6 +20,7 @@ module Minitest
|
|
20
20
|
end
|
21
21
|
@coordinator = T.let(options[:distributed].coordinator, Coordinators::CoordinatorInterface)
|
22
22
|
@window_line_width = T.let(nil, T.nilable(Integer))
|
23
|
+
@show_progress = T.let(options[:distributed].progress, T::Boolean)
|
23
24
|
end
|
24
25
|
|
25
26
|
sig { override.void }
|
@@ -37,7 +38,7 @@ module Minitest
|
|
37
38
|
|
38
39
|
sig { override.params(klass: T.class_of(Runnable), name: String).void }
|
39
40
|
def prerecord(klass, name)
|
40
|
-
if
|
41
|
+
if show_progress?
|
41
42
|
clear_current_line
|
42
43
|
io.print("[#{results.acks}/#{results.size}] #{klass}##{name}".slice(0...window_line_width))
|
43
44
|
end
|
@@ -45,14 +46,14 @@ module Minitest
|
|
45
46
|
|
46
47
|
sig { override.params(result: Minitest::Result).void }
|
47
48
|
def record(result)
|
48
|
-
clear_current_line if
|
49
|
+
clear_current_line if show_progress?
|
49
50
|
|
50
51
|
case (result_type = ResultType.of(result))
|
51
52
|
when ResultType::Passed
|
52
53
|
# TODO: warn for tests that are slower than the test timeout.
|
53
|
-
when ResultType::Skipped
|
54
|
+
when ResultType::Skipped, ResultType::Discarded
|
54
55
|
io.puts("#{result}\n") if options[:verbose]
|
55
|
-
when ResultType::Error, ResultType::Failed
|
56
|
+
when ResultType::Error, ResultType::Failed, ResultType::Requeued
|
56
57
|
io.puts("#{result}\n")
|
57
58
|
else
|
58
59
|
T.absurd(result_type)
|
@@ -61,11 +62,16 @@ module Minitest
|
|
61
62
|
|
62
63
|
sig { override.void }
|
63
64
|
def report
|
64
|
-
clear_current_line if
|
65
|
+
clear_current_line if show_progress?
|
65
66
|
end
|
66
67
|
|
67
68
|
private
|
68
69
|
|
70
|
+
sig { returns(T::Boolean) }
|
71
|
+
def show_progress?
|
72
|
+
@show_progress
|
73
|
+
end
|
74
|
+
|
69
75
|
sig { void }
|
70
76
|
def clear_current_line
|
71
77
|
io.print("\r" + (' ' * window_line_width) + "\r")
|
@@ -76,6 +82,8 @@ module Minitest
|
|
76
82
|
@window_line_width ||= begin
|
77
83
|
_height, width = io.winsize
|
78
84
|
width > 0 ? width : 80
|
85
|
+
rescue Errno::ENOTTY
|
86
|
+
80
|
79
87
|
end
|
80
88
|
end
|
81
89
|
|
@@ -7,14 +7,10 @@ module Minitest
|
|
7
7
|
class DistributedSummaryReporter < Minitest::Reporter
|
8
8
|
extend T::Sig
|
9
9
|
|
10
|
-
sig { returns(Coordinators::CoordinatorInterface) }
|
11
|
-
attr_reader :coordinator
|
12
|
-
|
13
10
|
sig { params(io: IO, options: T::Hash[Symbol, T.untyped]).void }
|
14
11
|
def initialize(io, options)
|
15
12
|
super
|
16
13
|
io.sync = true
|
17
|
-
@coordinator = T.let(options[:distributed].coordinator, Coordinators::CoordinatorInterface)
|
18
14
|
@start_time = T.let(0.0, Float)
|
19
15
|
end
|
20
16
|
|
@@ -26,21 +22,64 @@ module Minitest
|
|
26
22
|
|
27
23
|
sig { override.void }
|
28
24
|
def report
|
29
|
-
|
25
|
+
print_discard_warning if local_results.discards > 0
|
26
|
+
|
27
|
+
if configuration.coordinator.aborted?
|
28
|
+
io.puts("Cannot retry a run that was cut short during the previous attempt.")
|
29
|
+
io.puts
|
30
|
+
elsif combined_results.abort?
|
31
|
+
io.puts("The run was cut short after reaching the limit of #{configuration.max_failures} test failures.")
|
32
|
+
io.puts
|
33
|
+
end
|
30
34
|
|
31
|
-
|
32
|
-
combined_results = coordinator.combined_results
|
35
|
+
formatted_duration = format("(in %0.3fs)", Minitest.clock_time - @start_time)
|
33
36
|
if combined_results == local_results
|
34
|
-
io.puts("Results: #{combined_results} #{
|
37
|
+
io.puts("Results: #{combined_results} #{formatted_duration}")
|
35
38
|
else
|
36
|
-
io.puts("This worker: #{local_results} #{
|
39
|
+
io.puts("This worker: #{local_results} #{formatted_duration}")
|
37
40
|
io.puts("Combined results: #{combined_results}")
|
38
41
|
end
|
39
42
|
end
|
40
43
|
|
41
44
|
sig { override.returns(T::Boolean) }
|
42
45
|
def passed?
|
43
|
-
coordinator.
|
46
|
+
return false if configuration.coordinator.aborted?
|
47
|
+
|
48
|
+
# Generally, we want the workers to fail that had at least one failed or errored
|
49
|
+
# test. We have to trust that another worker will fail (and fail the build) if it
|
50
|
+
# encountered a failed test. We trust that the other worker will do this correctly,
|
51
|
+
# but we do verify that the statistics for the complete run are valid,
|
52
|
+
# to have some protection against unknown edge cases and bugs.
|
53
|
+
local_results.passed? && combined_results.valid?
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
sig { void }
|
59
|
+
def print_discard_warning
|
60
|
+
io.puts(<<~WARNING)
|
61
|
+
WARNING: This worker was not able to ack all the tests it ran with the coordinator,
|
62
|
+
and had to discard the results of those tests. This means that some of your tests may
|
63
|
+
take too long to run. Make sure that all your tests complete well within #{configuration.test_timeout_seconds}s.
|
64
|
+
|
65
|
+
WARNING
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { returns(ResultAggregate) }
|
69
|
+
def local_results
|
70
|
+
@local_results = T.let(@local_results, T.nilable(ResultAggregate))
|
71
|
+
@local_results ||= configuration.coordinator.local_results
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { returns(ResultAggregate) }
|
75
|
+
def combined_results
|
76
|
+
@combined_results = T.let(@combined_results, T.nilable(ResultAggregate))
|
77
|
+
@combined_results ||= configuration.coordinator.combined_results
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { returns(Configuration) }
|
81
|
+
def configuration
|
82
|
+
T.let(options[:distributed], Configuration)
|
44
83
|
end
|
45
84
|
end
|
46
85
|
end
|