ci-queue 0.81.0 → 0.83.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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +109 -0
- data/lib/ci/queue/build_record.rb +22 -10
- data/lib/ci/queue/class_resolver.rb +38 -0
- data/lib/ci/queue/configuration.rb +62 -1
- data/lib/ci/queue/file_loader.rb +101 -0
- data/lib/ci/queue/queue_entry.rb +56 -0
- data/lib/ci/queue/redis/_entry_helpers.lua +10 -0
- data/lib/ci/queue/redis/acknowledge.lua +10 -7
- data/lib/ci/queue/redis/base.rb +34 -8
- data/lib/ci/queue/redis/build_record.rb +89 -22
- data/lib/ci/queue/redis/grind_record.rb +17 -13
- data/lib/ci/queue/redis/heartbeat.lua +9 -4
- data/lib/ci/queue/redis/monitor.rb +19 -5
- data/lib/ci/queue/redis/requeue.lua +19 -11
- data/lib/ci/queue/redis/reserve.lua +47 -8
- data/lib/ci/queue/redis/reserve_lost.lua +5 -1
- data/lib/ci/queue/redis/supervisor.rb +3 -3
- data/lib/ci/queue/redis/worker.rb +216 -23
- data/lib/ci/queue/redis.rb +0 -1
- data/lib/ci/queue/version.rb +1 -1
- data/lib/ci/queue.rb +27 -0
- data/lib/minitest/queue/build_status_recorder.rb +32 -14
- data/lib/minitest/queue/grind_recorder.rb +3 -3
- data/lib/minitest/queue/junit_reporter.rb +2 -2
- data/lib/minitest/queue/lazy_entry_resolver.rb +55 -0
- data/lib/minitest/queue/lazy_test_discovery.rb +169 -0
- data/lib/minitest/queue/local_requeue_reporter.rb +11 -0
- data/lib/minitest/queue/order_reporter.rb +9 -2
- data/lib/minitest/queue/queue_population_strategy.rb +176 -0
- data/lib/minitest/queue/runner.rb +117 -27
- data/lib/minitest/queue/test_data.rb +14 -1
- data/lib/minitest/queue/worker_profile_reporter.rb +77 -0
- data/lib/minitest/queue.rb +271 -6
- metadata +10 -3
- data/lib/ci/queue/redis/key_shortener.rb +0 -53
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'ci/queue/static'
|
|
3
3
|
require 'concurrent/set'
|
|
4
|
+
require 'concurrent/map'
|
|
4
5
|
|
|
5
6
|
module CI
|
|
6
7
|
module Queue
|
|
@@ -13,11 +14,13 @@ module CI
|
|
|
13
14
|
self.max_sleep_time = 2
|
|
14
15
|
|
|
15
16
|
class Worker < Base
|
|
16
|
-
|
|
17
|
+
attr_accessor :entry_resolver
|
|
18
|
+
attr_reader :first_reserve_at
|
|
17
19
|
|
|
18
20
|
def initialize(redis, config)
|
|
19
21
|
@reserved_tests = Concurrent::Set.new
|
|
20
22
|
@shutdown_required = false
|
|
23
|
+
@first_reserve_at = nil
|
|
21
24
|
super(redis, config)
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -27,15 +30,65 @@ module CI
|
|
|
27
30
|
|
|
28
31
|
def populate(tests, random: Random.new)
|
|
29
32
|
@index = tests.map { |t| [t.id, t] }.to_h
|
|
30
|
-
|
|
31
|
-
push(
|
|
33
|
+
entries = Queue.shuffle(tests, random).map { |test| queue_entry_for(test) }
|
|
34
|
+
push(entries)
|
|
32
35
|
self
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
def stream_populate(tests, random: Random.new, batch_size: 10_000)
|
|
39
|
+
batch_size = batch_size.to_i
|
|
40
|
+
batch_size = 1 if batch_size < 1
|
|
41
|
+
|
|
42
|
+
value = key('setup', worker_id)
|
|
43
|
+
_, status = redis.pipelined do |pipeline|
|
|
44
|
+
pipeline.set(key('master-status'), value, nx: true)
|
|
45
|
+
pipeline.get(key('master-status'))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if @master = (value == status)
|
|
49
|
+
@total = 0
|
|
50
|
+
puts "Worker elected as leader, streaming tests to the queue."
|
|
51
|
+
|
|
52
|
+
duration = measure do
|
|
53
|
+
start_streaming!
|
|
54
|
+
buffer = []
|
|
55
|
+
|
|
56
|
+
tests.each do |test|
|
|
57
|
+
buffer << test
|
|
58
|
+
|
|
59
|
+
if buffer.size >= batch_size
|
|
60
|
+
push_batch(buffer, random)
|
|
61
|
+
buffer.clear
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
push_batch(buffer, random) unless buffer.empty?
|
|
66
|
+
finalize_streaming
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
puts "Streamed #{@total} tests in #{duration.round(2)}s."
|
|
70
|
+
$stdout.flush
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
register
|
|
74
|
+
redis.expire(key('workers'), config.redis_ttl)
|
|
75
|
+
self
|
|
76
|
+
rescue *CONNECTION_ERRORS
|
|
77
|
+
raise if @master
|
|
78
|
+
end
|
|
79
|
+
|
|
35
80
|
def populated?
|
|
36
81
|
!!defined?(@index)
|
|
37
82
|
end
|
|
38
83
|
|
|
84
|
+
def total
|
|
85
|
+
return @total if defined?(@total) && @total
|
|
86
|
+
|
|
87
|
+
redis.get(key('total')).to_i
|
|
88
|
+
rescue *CONNECTION_ERRORS
|
|
89
|
+
@total || 0
|
|
90
|
+
end
|
|
91
|
+
|
|
39
92
|
def shutdown!
|
|
40
93
|
@shutdown_required = true
|
|
41
94
|
end
|
|
@@ -51,13 +104,18 @@ module CI
|
|
|
51
104
|
DEFAULT_SLEEP_SECONDS = 0.5
|
|
52
105
|
|
|
53
106
|
def poll
|
|
54
|
-
wait_for_master
|
|
107
|
+
wait_for_master(timeout: config.queue_init_timeout, allow_streaming: true)
|
|
55
108
|
attempt = 0
|
|
56
109
|
until shutdown_required? || config.circuit_breakers.any?(&:open?) || exhausted? || max_test_failed?
|
|
57
|
-
if
|
|
110
|
+
if entry = reserve
|
|
58
111
|
attempt = 0
|
|
59
|
-
yield
|
|
112
|
+
yield resolve_entry(entry)
|
|
60
113
|
else
|
|
114
|
+
if still_streaming?
|
|
115
|
+
raise LostMaster, "Streaming stalled for more than #{config.lazy_load_streaming_timeout}s" if streaming_stale?
|
|
116
|
+
sleep 0.1
|
|
117
|
+
next
|
|
118
|
+
end
|
|
61
119
|
# Adding exponential backoff to avoid hammering Redis
|
|
62
120
|
# we just stay online here in case a test gets retried or times out so we can afford to wait
|
|
63
121
|
sleep_time = [DEFAULT_SLEEP_SECONDS * (2 ** attempt), Redis.max_sleep_time].min
|
|
@@ -89,6 +147,7 @@ module CI
|
|
|
89
147
|
def retry_queue
|
|
90
148
|
failures = build.failed_tests.to_set
|
|
91
149
|
log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1)
|
|
150
|
+
log = log.map { |entry| queue_entry_test_id(entry) }
|
|
92
151
|
log.select! { |id| failures.include?(id) }
|
|
93
152
|
log.uniq!
|
|
94
153
|
log.reverse!
|
|
@@ -103,23 +162,38 @@ module CI
|
|
|
103
162
|
@build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config)
|
|
104
163
|
end
|
|
105
164
|
|
|
165
|
+
def file_loader
|
|
166
|
+
@file_loader ||= CI::Queue::FileLoader.new
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def worker_queue_length
|
|
170
|
+
redis.llen(key('worker', worker_id, 'queue'))
|
|
171
|
+
rescue *CONNECTION_ERRORS
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
106
175
|
def report_worker_error(error)
|
|
107
176
|
build.report_worker_error(error)
|
|
108
177
|
end
|
|
109
178
|
|
|
110
179
|
def acknowledge(test_key, error: nil, pipeline: redis)
|
|
111
|
-
|
|
180
|
+
test_id = normalize_test_id(test_key)
|
|
181
|
+
assert_reserved!(test_id)
|
|
182
|
+
entry = reserved_entries.fetch(test_id, queue_entry_for(test_key))
|
|
183
|
+
unreserve_entry(test_id)
|
|
112
184
|
eval_script(
|
|
113
185
|
:acknowledge,
|
|
114
|
-
keys: [key('running'), key('processed'), key('owners'), key('error-reports')],
|
|
115
|
-
argv: [
|
|
186
|
+
keys: [key('running'), key('processed'), key('owners'), key('error-reports'), key('requeued-by')],
|
|
187
|
+
argv: [entry, test_id, error.to_s, config.redis_ttl],
|
|
116
188
|
pipeline: pipeline,
|
|
117
189
|
) == 1
|
|
118
190
|
end
|
|
119
191
|
|
|
120
192
|
def requeue(test, offset: Redis.requeue_offset)
|
|
121
|
-
|
|
122
|
-
|
|
193
|
+
test_id = normalize_test_id(test)
|
|
194
|
+
assert_reserved!(test_id)
|
|
195
|
+
entry = reserved_entries.fetch(test_id, queue_entry_for(test))
|
|
196
|
+
unreserve_entry(test_id)
|
|
123
197
|
global_max_requeues = config.global_max_requeues(total)
|
|
124
198
|
|
|
125
199
|
requeued = config.max_requeues > 0 && global_max_requeues > 0 && eval_script(
|
|
@@ -132,11 +206,16 @@ module CI
|
|
|
132
206
|
key('worker', worker_id, 'queue'),
|
|
133
207
|
key('owners'),
|
|
134
208
|
key('error-reports'),
|
|
209
|
+
key('requeued-by'),
|
|
135
210
|
],
|
|
136
|
-
argv: [config.max_requeues, global_max_requeues,
|
|
211
|
+
argv: [config.max_requeues, global_max_requeues, entry, test_id, offset, config.redis_ttl],
|
|
137
212
|
) == 1
|
|
138
213
|
|
|
139
|
-
|
|
214
|
+
unless requeued
|
|
215
|
+
reserved_tests << test_id
|
|
216
|
+
reserved_entries[test_id] = entry
|
|
217
|
+
reserved_entry_ids[entry] = test_id
|
|
218
|
+
end
|
|
140
219
|
requeued
|
|
141
220
|
end
|
|
142
221
|
|
|
@@ -157,19 +236,131 @@ module CI
|
|
|
157
236
|
@reserved_tests ||= Concurrent::Set.new
|
|
158
237
|
end
|
|
159
238
|
|
|
239
|
+
def reserved_entries
|
|
240
|
+
@reserved_entries ||= Concurrent::Map.new
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def reserved_entry_ids
|
|
244
|
+
@reserved_entry_ids ||= Concurrent::Map.new
|
|
245
|
+
end
|
|
246
|
+
|
|
160
247
|
def worker_id
|
|
161
248
|
config.worker_id
|
|
162
249
|
end
|
|
163
250
|
|
|
164
|
-
def
|
|
165
|
-
unless reserved_tests.
|
|
166
|
-
raise ReservationError, "Acknowledged #{
|
|
251
|
+
def assert_reserved!(test_id)
|
|
252
|
+
unless reserved_tests.include?(test_id)
|
|
253
|
+
raise ReservationError, "Acknowledged #{test_id.inspect} but only #{reserved_tests.map(&:inspect).join(", ")} reserved"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def reserve_entry(entry)
|
|
258
|
+
test_id = queue_entry_test_id(entry)
|
|
259
|
+
reserved_tests << test_id
|
|
260
|
+
reserved_entries[test_id] = entry
|
|
261
|
+
reserved_entry_ids[entry] = test_id
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def unreserve_entry(test_id)
|
|
265
|
+
entry = reserved_entries.delete(test_id)
|
|
266
|
+
reserved_tests.delete(test_id)
|
|
267
|
+
reserved_entry_ids.delete(entry) if entry
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def normalize_test_id(test_key)
|
|
271
|
+
key = test_key.respond_to?(:id) ? test_key.id : test_key
|
|
272
|
+
if key.is_a?(String)
|
|
273
|
+
cached = reserved_entry_ids[key]
|
|
274
|
+
return cached if cached
|
|
275
|
+
end
|
|
276
|
+
queue_entry_test_id(key)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def queue_entry_test_id(entry)
|
|
280
|
+
CI::Queue::QueueEntry.test_id(entry)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def queue_entry_for(test)
|
|
284
|
+
return test.queue_entry if test.respond_to?(:queue_entry)
|
|
285
|
+
return test.id if test.respond_to?(:id)
|
|
286
|
+
|
|
287
|
+
test
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def resolve_entry(entry)
|
|
291
|
+
test_id = reserved_entry_ids[entry] || queue_entry_test_id(entry)
|
|
292
|
+
if populated?
|
|
293
|
+
return index[test_id] if index.key?(test_id)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
return entry_resolver.call(entry) if entry_resolver
|
|
297
|
+
|
|
298
|
+
entry
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def still_streaming?
|
|
302
|
+
master_status == 'streaming'
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def streaming_stale?
|
|
306
|
+
timeout = config.lazy_load_streaming_timeout.to_i
|
|
307
|
+
updated_at = redis.get(key('streaming-updated-at'))
|
|
308
|
+
return true unless updated_at
|
|
309
|
+
|
|
310
|
+
(CI::Queue.time_now.to_f - updated_at.to_f) > timeout
|
|
311
|
+
rescue *CONNECTION_ERRORS
|
|
312
|
+
false
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def start_streaming!
|
|
316
|
+
timeout = config.lazy_load_streaming_timeout.to_i
|
|
317
|
+
with_redis_timeout(5) do
|
|
318
|
+
redis.multi do |transaction|
|
|
319
|
+
transaction.set(key('total'), 0)
|
|
320
|
+
transaction.set(key('master-status'), 'streaming')
|
|
321
|
+
transaction.set(key('streaming-updated-at'), CI::Queue.time_now.to_f)
|
|
322
|
+
transaction.expire(key('streaming-updated-at'), timeout)
|
|
323
|
+
transaction.expire(key('queue'), config.redis_ttl)
|
|
324
|
+
transaction.expire(key('total'), config.redis_ttl)
|
|
325
|
+
transaction.expire(key('master-status'), config.redis_ttl)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def push_batch(tests, random)
|
|
331
|
+
# Use plain shuffle instead of Queue.shuffle — the custom shuffler expects
|
|
332
|
+
# test objects with .id, but streaming entries are pre-formatted strings.
|
|
333
|
+
entries = tests.shuffle(random: random).map { |test| queue_entry_for(test) }
|
|
334
|
+
return if entries.empty?
|
|
335
|
+
|
|
336
|
+
@total += entries.size
|
|
337
|
+
timeout = config.lazy_load_streaming_timeout.to_i
|
|
338
|
+
redis.multi do |transaction|
|
|
339
|
+
transaction.lpush(key('queue'), entries)
|
|
340
|
+
transaction.incrby(key('total'), entries.size)
|
|
341
|
+
transaction.set(key('master-status'), 'streaming')
|
|
342
|
+
transaction.set(key('streaming-updated-at'), CI::Queue.time_now.to_f)
|
|
343
|
+
transaction.expire(key('streaming-updated-at'), timeout)
|
|
344
|
+
transaction.expire(key('queue'), config.redis_ttl)
|
|
345
|
+
transaction.expire(key('total'), config.redis_ttl)
|
|
346
|
+
transaction.expire(key('master-status'), config.redis_ttl)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def finalize_streaming
|
|
351
|
+
redis.multi do |transaction|
|
|
352
|
+
transaction.set(key('master-status'), 'ready')
|
|
353
|
+
transaction.expire(key('master-status'), config.redis_ttl)
|
|
354
|
+
transaction.del(key('streaming-updated-at'))
|
|
167
355
|
end
|
|
168
356
|
end
|
|
169
357
|
|
|
170
358
|
def reserve
|
|
171
|
-
(try_to_reserve_lost_test || try_to_reserve_test).tap do |
|
|
172
|
-
|
|
359
|
+
(try_to_reserve_lost_test || try_to_reserve_test).tap do |entry|
|
|
360
|
+
if entry
|
|
361
|
+
@first_reserve_at ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
362
|
+
reserve_entry(entry)
|
|
363
|
+
end
|
|
173
364
|
end
|
|
174
365
|
end
|
|
175
366
|
|
|
@@ -182,8 +373,10 @@ module CI
|
|
|
182
373
|
key('processed'),
|
|
183
374
|
key('worker', worker_id, 'queue'),
|
|
184
375
|
key('owners'),
|
|
376
|
+
key('requeued-by'),
|
|
377
|
+
key('workers'),
|
|
185
378
|
],
|
|
186
|
-
argv: [CI::Queue.time_now.to_f],
|
|
379
|
+
argv: [CI::Queue.time_now.to_f, Redis.requeue_offset],
|
|
187
380
|
)
|
|
188
381
|
end
|
|
189
382
|
|
|
@@ -198,7 +391,7 @@ module CI
|
|
|
198
391
|
key('worker', worker_id, 'queue'),
|
|
199
392
|
key('owners'),
|
|
200
393
|
],
|
|
201
|
-
argv: [CI::Queue.time_now.to_f, timeout],
|
|
394
|
+
argv: [CI::Queue.time_now.to_f, timeout, CI::Queue::QueueEntry::DELIMITER],
|
|
202
395
|
)
|
|
203
396
|
|
|
204
397
|
if lost_test
|
|
@@ -208,8 +401,8 @@ module CI
|
|
|
208
401
|
lost_test
|
|
209
402
|
end
|
|
210
403
|
|
|
211
|
-
def push(
|
|
212
|
-
@total =
|
|
404
|
+
def push(entries)
|
|
405
|
+
@total = entries.size
|
|
213
406
|
|
|
214
407
|
# We set a unique value (worker_id) and read it back to make "SET if Not eXists" idempotent in case of a retry.
|
|
215
408
|
value = key('setup', worker_id)
|
|
@@ -227,7 +420,7 @@ module CI
|
|
|
227
420
|
with_redis_timeout(5) do
|
|
228
421
|
redis.without_reconnect do
|
|
229
422
|
redis.multi do |transaction|
|
|
230
|
-
transaction.lpush(key('queue'),
|
|
423
|
+
transaction.lpush(key('queue'), entries) unless entries.empty?
|
|
231
424
|
transaction.set(key('total'), @total)
|
|
232
425
|
transaction.set(key('master-status'), 'ready')
|
|
233
426
|
|
data/lib/ci/queue/redis.rb
CHANGED
data/lib/ci/queue/version.rb
CHANGED
data/lib/ci/queue.rb
CHANGED
|
@@ -14,6 +14,9 @@ require 'ci/queue/static'
|
|
|
14
14
|
require 'ci/queue/file'
|
|
15
15
|
require 'ci/queue/grind'
|
|
16
16
|
require 'ci/queue/bisect'
|
|
17
|
+
require 'ci/queue/queue_entry'
|
|
18
|
+
require 'ci/queue/class_resolver'
|
|
19
|
+
require 'ci/queue/file_loader'
|
|
17
20
|
|
|
18
21
|
module CI
|
|
19
22
|
module Queue
|
|
@@ -22,6 +25,18 @@ module CI
|
|
|
22
25
|
attr_accessor :shuffler, :requeueable
|
|
23
26
|
|
|
24
27
|
Error = Class.new(StandardError)
|
|
28
|
+
ClassNotFoundError = Class.new(Error)
|
|
29
|
+
|
|
30
|
+
class FileLoadError < Error
|
|
31
|
+
attr_reader :file_path, :original_error
|
|
32
|
+
|
|
33
|
+
def initialize(file_path, original_error)
|
|
34
|
+
@file_path = file_path
|
|
35
|
+
@original_error = original_error
|
|
36
|
+
super("Failed to load #{file_path}: #{original_error.class}: #{original_error.message}")
|
|
37
|
+
set_backtrace(original_error.backtrace)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
25
40
|
|
|
26
41
|
module Warnings
|
|
27
42
|
RESERVED_LOST_TEST = :RESERVED_LOST_TEST
|
|
@@ -48,6 +63,17 @@ module CI
|
|
|
48
63
|
end
|
|
49
64
|
end
|
|
50
65
|
|
|
66
|
+
def debug?
|
|
67
|
+
return @debug if defined?(@debug)
|
|
68
|
+
|
|
69
|
+
value = ENV['CI_QUEUE_DEBUG']
|
|
70
|
+
@debug = !!(value && !value.strip.empty? && !%w[0 false].include?(value.strip.downcase))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reset_debug!
|
|
74
|
+
remove_instance_variable(:@debug) if defined?(@debug)
|
|
75
|
+
end
|
|
76
|
+
|
|
51
77
|
def from_uri(url, config)
|
|
52
78
|
uri = URI(url)
|
|
53
79
|
implementation = case uri.scheme
|
|
@@ -65,3 +91,4 @@ module CI
|
|
|
65
91
|
end
|
|
66
92
|
end
|
|
67
93
|
end
|
|
94
|
+
|
|
@@ -38,28 +38,46 @@ module Minitest
|
|
|
38
38
|
super
|
|
39
39
|
|
|
40
40
|
self.total_time = Minitest.clock_time - start_time
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
elsif test.error?
|
|
46
|
-
self.errors += 1
|
|
47
|
-
elsif test.failure
|
|
48
|
-
self.failures += 1
|
|
49
|
-
end
|
|
41
|
+
|
|
42
|
+
# Determine what type of result this is and record it
|
|
43
|
+
test_id = "#{test.klass}##{test.name}"
|
|
44
|
+
delta = delta_for(test)
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
build.record_error("#{test.klass}##{test.name}", dump(test), stats: stats)
|
|
46
|
+
acknowledged = if (test.failure || test.error?) && !test.skipped?
|
|
47
|
+
build.record_error(test_id, dump(test), stat_delta: delta)
|
|
54
48
|
elsif test.requeued?
|
|
55
|
-
build.record_requeue(
|
|
49
|
+
build.record_requeue(test_id)
|
|
56
50
|
else
|
|
57
|
-
build.record_success(
|
|
51
|
+
build.record_success(test_id, skip_flaky_record: test.skipped?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if acknowledged
|
|
55
|
+
if (test.failure || test.error?) && !test.skipped?
|
|
56
|
+
test.error? ? self.errors += 1 : self.failures += 1
|
|
57
|
+
elsif test.requeued?
|
|
58
|
+
self.requeues += 1
|
|
59
|
+
elsif test.skipped?
|
|
60
|
+
self.skips += 1
|
|
61
|
+
end
|
|
62
|
+
# Apply delta to Redis (record_success returns true when ack'd or when we replaced a failure)
|
|
63
|
+
build.record_stats_delta(delta)
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
private
|
|
62
68
|
|
|
69
|
+
def delta_for(test)
|
|
70
|
+
h = { 'assertions' => (test.assertions || 0).to_i, 'errors' => 0, 'failures' => 0, 'skips' => 0, 'requeues' => 0, 'total_time' => test.time.to_f }
|
|
71
|
+
if (test.failure || test.error?) && !test.skipped?
|
|
72
|
+
test.error? ? h['errors'] = 1 : h['failures'] = 1
|
|
73
|
+
elsif test.requeued?
|
|
74
|
+
h['requeues'] = 1
|
|
75
|
+
elsif test.skipped?
|
|
76
|
+
h['skips'] = 1
|
|
77
|
+
end
|
|
78
|
+
h
|
|
79
|
+
end
|
|
80
|
+
|
|
63
81
|
def dump(test)
|
|
64
82
|
ErrorReport.new(self.class.failure_formatter.new(test).to_h).dump
|
|
65
83
|
end
|
|
@@ -32,12 +32,12 @@ module Minitest
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def record_test(test)
|
|
35
|
-
stats = self.class.counters
|
|
36
35
|
if (test.failure || test.error?) && !test.skipped?
|
|
37
|
-
build.record_error(dump(test)
|
|
36
|
+
build.record_error(dump(test))
|
|
38
37
|
else
|
|
39
|
-
build.record_success
|
|
38
|
+
build.record_success
|
|
40
39
|
end
|
|
40
|
+
build.record_stats(self.class.counters)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def increment_counter(test)
|
|
@@ -136,9 +136,9 @@ module Minitest
|
|
|
136
136
|
result[:time] = 0
|
|
137
137
|
tests.each do |test|
|
|
138
138
|
result[:"#{result(test)}_count"] += 1
|
|
139
|
-
result[:assertion_count] += test.assertions
|
|
139
|
+
result[:assertion_count] += test.assertions || 0
|
|
140
140
|
result[:test_count] += 1
|
|
141
|
-
result[:time] += test.time
|
|
141
|
+
result[:time] += test.time || 0
|
|
142
142
|
end
|
|
143
143
|
result
|
|
144
144
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Queue
|
|
5
|
+
class LazyEntryResolver
|
|
6
|
+
def initialize(loader:, resolver:)
|
|
7
|
+
@loader = loader
|
|
8
|
+
@resolver = resolver
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(entry)
|
|
12
|
+
parsed = CI::Queue::QueueEntry.parse(entry)
|
|
13
|
+
class_name, method_name = parsed.fetch(:test_id).split('#', 2)
|
|
14
|
+
if CI::Queue::QueueEntry.load_error_payload?(parsed[:file_path])
|
|
15
|
+
payload = CI::Queue::QueueEntry.decode_load_error(parsed[:file_path])
|
|
16
|
+
if payload
|
|
17
|
+
error = StandardError.new("#{payload['error_class']}: #{payload['error_message']}")
|
|
18
|
+
error.set_backtrace(payload['backtrace']) if payload['backtrace']
|
|
19
|
+
load_error = CI::Queue::FileLoadError.new(payload['file_path'], error)
|
|
20
|
+
return Minitest::Queue::LazySingleExample.new(
|
|
21
|
+
class_name,
|
|
22
|
+
method_name,
|
|
23
|
+
payload['file_path'],
|
|
24
|
+
loader: @loader,
|
|
25
|
+
resolver: @resolver,
|
|
26
|
+
load_error: load_error,
|
|
27
|
+
queue_entry: entry,
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
error = StandardError.new("Corrupt load error payload for #{class_name}##{method_name}")
|
|
31
|
+
load_error = CI::Queue::FileLoadError.new("unknown", error)
|
|
32
|
+
return Minitest::Queue::LazySingleExample.new(
|
|
33
|
+
class_name,
|
|
34
|
+
method_name,
|
|
35
|
+
nil,
|
|
36
|
+
loader: @loader,
|
|
37
|
+
resolver: @resolver,
|
|
38
|
+
load_error: load_error,
|
|
39
|
+
queue_entry: entry,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Minitest::Queue::LazySingleExample.new(
|
|
45
|
+
class_name,
|
|
46
|
+
method_name,
|
|
47
|
+
parsed[:file_path],
|
|
48
|
+
loader: @loader,
|
|
49
|
+
resolver: @resolver,
|
|
50
|
+
queue_entry: entry,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|