ci-queue 0.1.0 → 0.2.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/.gitignore +1 -0
- data/.travis.yml +3 -1
- data/README.md +3 -0
- data/Rakefile +1 -1
- data/ci-queue.gemspec +1 -0
- data/lib/ci/queue/file.rb +2 -2
- data/lib/ci/queue/redis.rb +1 -0
- data/lib/ci/queue/redis/base.rb +8 -5
- data/lib/ci/queue/redis/retry.rb +29 -0
- data/lib/ci/queue/redis/worker.rb +91 -25
- data/lib/ci/queue/static.rb +29 -1
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue.rb +76 -5
- data/lib/minitest/reporters/failure_formatter.rb +65 -0
- data/lib/minitest/reporters/queue_reporter.rb +48 -0
- data/lib/minitest/reporters/redis_reporter.rb +202 -0
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61155b4429b71670e473e39cf41dbac081c24727
|
4
|
+
data.tar.gz: 60d3c11057a5d7412554f51f10fe8beeefb4a483
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d59e89e8a27f00177f32ee55d7115b93617e21a9cfa2161a211da558831aeddadd4739242a93e9f12e183458839e6766903084b03814730848127a5febd9e21b
|
7
|
+
data.tar.gz: 377c2123f5a51743a78abd13b5fd5c72f3bada7a1f230ea6712cef43275365e03a166f54c01097d111f2fd390b8f71d756732768fb70cc711d40c087867ee804
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# CI::Queue
|
2
2
|
|
3
|
+
[](https://rubygems.org/gems/ci-queue)
|
4
|
+
[](https://travis-ci.org/Shopify/ci-queue)
|
5
|
+
|
3
6
|
Distribute tests over many workers using a queue.
|
4
7
|
|
5
8
|
## Why a queue?
|
data/Rakefile
CHANGED
data/ci-queue.gemspec
CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
27
27
|
spec.add_development_dependency 'redis', '~> 3.3'
|
28
28
|
spec.add_development_dependency 'simplecov', '~> 0.12'
|
29
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.1'
|
29
30
|
end
|
data/lib/ci/queue/file.rb
CHANGED
@@ -3,8 +3,8 @@ require 'ci/queue/static'
|
|
3
3
|
module CI
|
4
4
|
module Queue
|
5
5
|
class File < Static
|
6
|
-
def initialize(path)
|
7
|
-
super(::File.readlines(path).map(&:strip).reject(&:empty?))
|
6
|
+
def initialize(path, **args)
|
7
|
+
super(::File.readlines(path).map(&:strip).reject(&:empty?), **args)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
data/lib/ci/queue/redis.rb
CHANGED
data/lib/ci/queue/redis/base.rb
CHANGED
@@ -4,7 +4,7 @@ module CI
|
|
4
4
|
class Base
|
5
5
|
def initialize(redis:, build_id:)
|
6
6
|
@redis = redis
|
7
|
-
@
|
7
|
+
@build_id = build_id
|
8
8
|
end
|
9
9
|
|
10
10
|
def empty?
|
@@ -44,10 +44,10 @@ module CI
|
|
44
44
|
|
45
45
|
private
|
46
46
|
|
47
|
-
attr_reader :redis
|
47
|
+
attr_reader :redis, :build_id
|
48
48
|
|
49
49
|
def key(*args)
|
50
|
-
[
|
50
|
+
['build', build_id, *args].join(':')
|
51
51
|
end
|
52
52
|
|
53
53
|
def master_status
|
@@ -55,9 +55,12 @@ module CI
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def eval_script(script, *args)
|
58
|
+
redis.evalsha(load_script(script), *args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_script(script)
|
58
62
|
@scripts_cache ||= {}
|
59
|
-
|
60
|
-
redis.evalsha(sha, *args)
|
63
|
+
@scripts_cache[script] ||= redis.script(:load, script)
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CI
|
2
|
+
module Queue
|
3
|
+
module Redis
|
4
|
+
class Retry < Static
|
5
|
+
def initialize(tests, redis:, build_id:, worker_id:, **args)
|
6
|
+
@redis = redis
|
7
|
+
@build_id = build_id
|
8
|
+
@worker_id = worker_id
|
9
|
+
super(tests, **args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def minitest_reporters
|
13
|
+
require 'minitest/reporters/redis_reporter'
|
14
|
+
@minitest_reporters ||= [
|
15
|
+
Minitest::Reporters::RedisReporter::Worker.new(
|
16
|
+
redis: redis,
|
17
|
+
build_id: build_id,
|
18
|
+
worker_id: worker_id,
|
19
|
+
)
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :redis, :build_id, :worker_id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,10 +1,17 @@
|
|
1
|
+
require 'ci/queue/static'
|
2
|
+
|
1
3
|
module CI
|
2
4
|
module Queue
|
3
5
|
module Redis
|
6
|
+
ReservationError = Class.new(StandardError)
|
7
|
+
|
4
8
|
class Worker < Base
|
5
9
|
attr_reader :total
|
6
10
|
|
7
|
-
def initialize(tests, redis:, build_id:, worker_id:, timeout:)
|
11
|
+
def initialize(tests, redis:, build_id:, worker_id:, timeout:, max_requeues: 0, requeue_tolerance: 0.0)
|
12
|
+
@reserved_test = nil
|
13
|
+
@max_requeues = max_requeues
|
14
|
+
@global_max_requeues = (tests.size * requeue_tolerance).ceil
|
8
15
|
@shutdown_required = false
|
9
16
|
super(redis: redis, build_id: build_id)
|
10
17
|
@worker_id = worker_id
|
@@ -26,19 +33,90 @@ module CI
|
|
26
33
|
|
27
34
|
def poll
|
28
35
|
wait_for_master
|
29
|
-
|
30
|
-
|
31
|
-
|
36
|
+
until shutdown_required? || empty?
|
37
|
+
if test = reserve
|
38
|
+
yield test
|
39
|
+
else
|
40
|
+
sleep 0.05
|
41
|
+
end
|
32
42
|
end
|
33
43
|
end
|
34
44
|
|
35
|
-
def retry_queue
|
36
|
-
|
45
|
+
def retry_queue(**args)
|
46
|
+
Retry.new(
|
47
|
+
redis.lrange(key('worker', worker_id, 'queue'), 0, -1).reverse.uniq,
|
48
|
+
redis: redis,
|
49
|
+
build_id: build_id,
|
50
|
+
worker_id: worker_id,
|
51
|
+
**args
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def minitest_reporters
|
56
|
+
require 'minitest/reporters/redis_reporter'
|
57
|
+
@minitest_reporters ||= [
|
58
|
+
Minitest::Reporters::RedisReporter::Worker.new(
|
59
|
+
redis: redis,
|
60
|
+
build_id: build_id,
|
61
|
+
worker_id: worker_id,
|
62
|
+
)
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
def acknowledge(test, success)
|
67
|
+
if @reserved_test == test
|
68
|
+
@reserved_test = nil
|
69
|
+
else
|
70
|
+
raise ReservationError, "Acknowledged #{test.inspect} but #{@reserved_test.inspect} was reserved"
|
71
|
+
end
|
72
|
+
|
73
|
+
if !success && should_requeue?(test)
|
74
|
+
requeue(test)
|
75
|
+
false
|
76
|
+
else
|
77
|
+
ack(test)
|
78
|
+
true
|
79
|
+
end
|
37
80
|
end
|
38
81
|
|
39
82
|
private
|
40
83
|
|
41
|
-
attr_reader :worker_id, :timeout
|
84
|
+
attr_reader :worker_id, :timeout, :max_requeues, :global_max_requeues
|
85
|
+
|
86
|
+
def should_requeue?(test)
|
87
|
+
individual_requeues, global_requeues = redis.multi do
|
88
|
+
redis.hincrby(key('requeues-count'), test, 1)
|
89
|
+
redis.hincrby(key('requeues-count'), '___total___'.freeze, 1)
|
90
|
+
end
|
91
|
+
|
92
|
+
if individual_requeues.to_i > max_requeues || global_requeues.to_i > global_max_requeues
|
93
|
+
redis.multi do
|
94
|
+
redis.hincrby(key('requeues-count'), test, -1)
|
95
|
+
redis.hincrby(key('requeues-count'), '___total___'.freeze, -1)
|
96
|
+
end
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
def requeue(test)
|
104
|
+
load_script(ACKNOWLEDGE)
|
105
|
+
redis.multi do
|
106
|
+
redis.decr(key('processed'))
|
107
|
+
redis.rpush(key('queue'), test)
|
108
|
+
ack(test)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def reserve
|
113
|
+
if @reserved_test
|
114
|
+
raise ReservationError, "#{@reserved_test.inspect} is already reserved. " \
|
115
|
+
"You have to acknowledge it before you can reserve another one"
|
116
|
+
end
|
117
|
+
|
118
|
+
@reserved_test = (try_to_reserve_lost_test || try_to_reserve_test)
|
119
|
+
end
|
42
120
|
|
43
121
|
RESERVE_TEST = %{
|
44
122
|
local queue_key = KEYS[1]
|
@@ -53,14 +131,8 @@ module CI
|
|
53
131
|
return nil
|
54
132
|
end
|
55
133
|
}
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
if test = eval_script(RESERVE_TEST, keys: [key('queue'), key('running')], argv: [Time.now.to_f])
|
60
|
-
return test
|
61
|
-
else
|
62
|
-
reserve_lost_test
|
63
|
-
end
|
134
|
+
def try_to_reserve_test
|
135
|
+
eval_script(RESERVE_TEST, keys: [key('queue'), key('running')], argv: [Time.now.to_f])
|
64
136
|
end
|
65
137
|
|
66
138
|
RESERVE_LOST_TEST = %{
|
@@ -76,14 +148,8 @@ module CI
|
|
76
148
|
return nil
|
77
149
|
end
|
78
150
|
}
|
79
|
-
def
|
80
|
-
|
81
|
-
if test = eval_script(RESERVE_LOST_TEST, keys: [key('running')], argv: [Time.now.to_f, timeout])
|
82
|
-
return test
|
83
|
-
end
|
84
|
-
sleep 0.1
|
85
|
-
end
|
86
|
-
nil
|
151
|
+
def try_to_reserve_lost_test
|
152
|
+
eval_script(RESERVE_LOST_TEST, keys: [key('running')], argv: [Time.now.to_f, timeout])
|
87
153
|
end
|
88
154
|
|
89
155
|
ACKNOWLEDGE = %{
|
@@ -95,9 +161,9 @@ module CI
|
|
95
161
|
redis.call('incr', processed_count_key)
|
96
162
|
end
|
97
163
|
}
|
98
|
-
def
|
164
|
+
def ack(test)
|
99
165
|
eval_script(ACKNOWLEDGE, keys: [key('running'), key('processed')], argv: [test])
|
100
|
-
redis.lpush(key(
|
166
|
+
redis.lpush(key('worker', worker_id, 'queue'), test)
|
101
167
|
end
|
102
168
|
|
103
169
|
def push(tests)
|
data/lib/ci/queue/static.rb
CHANGED
@@ -3,10 +3,12 @@ module CI
|
|
3
3
|
class Static
|
4
4
|
attr_reader :progress, :total
|
5
5
|
|
6
|
-
def initialize(tests)
|
6
|
+
def initialize(tests, max_requeues: 0, requeue_tolerance: 0.0)
|
7
7
|
@queue = tests
|
8
8
|
@progress = 0
|
9
9
|
@total = tests.size
|
10
|
+
@max_requeues = max_requeues
|
11
|
+
@global_max_requeues = (tests.size * requeue_tolerance).ceil
|
10
12
|
end
|
11
13
|
|
12
14
|
def to_a
|
@@ -27,6 +29,32 @@ module CI
|
|
27
29
|
def empty?
|
28
30
|
@queue.empty?
|
29
31
|
end
|
32
|
+
|
33
|
+
def acknowledge(test, success)
|
34
|
+
if !success && should_requeue?(test)
|
35
|
+
requeue(test)
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :max_requeues, :global_max_requeues
|
45
|
+
|
46
|
+
def should_requeue?(test)
|
47
|
+
requeues[test] < max_requeues && requeues.values.inject(0, :+) < global_max_requeues
|
48
|
+
end
|
49
|
+
|
50
|
+
def requeue(test)
|
51
|
+
requeues[test] += 1
|
52
|
+
@queue.unshift(test)
|
53
|
+
end
|
54
|
+
|
55
|
+
def requeues
|
56
|
+
@requeues ||= Hash.new(0)
|
57
|
+
end
|
30
58
|
end
|
31
59
|
end
|
32
60
|
end
|
data/lib/ci/queue/version.rb
CHANGED
data/lib/minitest/queue.rb
CHANGED
@@ -1,11 +1,77 @@
|
|
1
1
|
require 'minitest'
|
2
2
|
|
3
|
+
gem 'minitest-reporters', '~> 1.1'
|
4
|
+
require 'minitest/reporters'
|
5
|
+
|
3
6
|
module Minitest
|
7
|
+
class Requeue < Skip
|
8
|
+
attr_reader :failure
|
9
|
+
|
10
|
+
def initialize(failure)
|
11
|
+
super()
|
12
|
+
@failure = failure
|
13
|
+
end
|
14
|
+
|
15
|
+
def result_label
|
16
|
+
"Requeued"
|
17
|
+
end
|
18
|
+
|
19
|
+
def backtrace
|
20
|
+
failure.backtrace
|
21
|
+
end
|
22
|
+
|
23
|
+
def error
|
24
|
+
failure.error
|
25
|
+
end
|
26
|
+
|
27
|
+
def message
|
28
|
+
failure.message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Requeueing
|
33
|
+
# Make requeues acts as skips for reporters not aware of the difference.
|
34
|
+
def skipped?
|
35
|
+
super || requeued?
|
36
|
+
end
|
37
|
+
|
38
|
+
def requeued?
|
39
|
+
Requeue === failure
|
40
|
+
end
|
41
|
+
|
42
|
+
def requeue!
|
43
|
+
self.failures.unshift(Requeue.new(self.failures.shift))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
4
47
|
module Queue
|
5
|
-
|
48
|
+
attr_reader :queue
|
49
|
+
|
50
|
+
def queue=(queue)
|
51
|
+
@queue = queue
|
52
|
+
if queue.respond_to?(:minitest_reporters)
|
53
|
+
self.queue_reporters = queue.minitest_reporters
|
54
|
+
else
|
55
|
+
self.queue_reporters = []
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def queue_reporters=(reporters)
|
60
|
+
@queue_reporters ||= []
|
61
|
+
Reporters.reporters = ((Reporters.reporters || []) - @queue_reporters) + reporters
|
62
|
+
@queue_reporters = reporters
|
63
|
+
end
|
6
64
|
|
7
65
|
SuiteNotFound = Class.new(StandardError)
|
8
66
|
|
67
|
+
def loaded_tests
|
68
|
+
MiniTest::Test.runnables.flat_map do |suite|
|
69
|
+
suite.runnable_methods.map do |method|
|
70
|
+
"#{suite}##{method}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
9
75
|
def __run(*args)
|
10
76
|
if queue
|
11
77
|
run_from_queue(*args)
|
@@ -17,11 +83,15 @@ module Minitest
|
|
17
83
|
def run_from_queue(reporter, *)
|
18
84
|
runnable_classes = Minitest::Runnable.runnables.map { |s| [s.name, s] }.to_h
|
19
85
|
|
20
|
-
queue.poll do |
|
21
|
-
class_name,
|
86
|
+
queue.poll do |test_name|
|
87
|
+
class_name, method_name = test_name.split("#".freeze, 2)
|
22
88
|
|
23
|
-
if
|
24
|
-
Minitest
|
89
|
+
if klass = runnable_classes[class_name]
|
90
|
+
result = Minitest.run_one_method(klass, method_name)
|
91
|
+
unless queue.acknowledge(test_name, result.passed? || result.skipped?)
|
92
|
+
result.requeue!
|
93
|
+
end
|
94
|
+
reporter.record(result)
|
25
95
|
else
|
26
96
|
raise SuiteNotFound, "Couldn't find suite matching: #{msg.inspect}"
|
27
97
|
end
|
@@ -31,3 +101,4 @@ module Minitest
|
|
31
101
|
end
|
32
102
|
|
33
103
|
MiniTest.singleton_class.prepend(MiniTest::Queue)
|
104
|
+
MiniTest::Test.prepend(MiniTest::Requeueing)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ansi'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Reporters
|
6
|
+
class FailureFormatter < SimpleDelegator
|
7
|
+
include ANSI::Code
|
8
|
+
|
9
|
+
def initialize(test)
|
10
|
+
@test = test
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
[
|
16
|
+
header,
|
17
|
+
body,
|
18
|
+
"\n"
|
19
|
+
].flatten.compact.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
test_and_module_name: "#{test.class}##{test.name}",
|
25
|
+
test_name: test.name,
|
26
|
+
output: to_s,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :test
|
33
|
+
|
34
|
+
def header
|
35
|
+
"#{red(status)} #{test.class}##{test.name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def status
|
39
|
+
if test.error?
|
40
|
+
'ERROR'
|
41
|
+
elsif test.failure
|
42
|
+
'FAIL'
|
43
|
+
else
|
44
|
+
raise ArgumentError, "Couldn't infer test status"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def body
|
49
|
+
error = test.failure
|
50
|
+
message = if error.is_a?(MiniTest::UnexpectedError)
|
51
|
+
"#{error.exception.class}: #{error.exception.message}"
|
52
|
+
else
|
53
|
+
error.exception.message
|
54
|
+
end
|
55
|
+
|
56
|
+
backtrace = Minitest.filter_backtrace(error.backtrace).map { |line| ' ' + relativize(line) }
|
57
|
+
[yellow(message), *backtrace].join("\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def relativize(trace_line)
|
61
|
+
trace_line.sub(/\A#{Regexp.escape("#{Dir.pwd}/")}/, '')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'minitest/reporters'
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
module Reporters
|
5
|
+
class QueueReporter < BaseReporter
|
6
|
+
include ANSI::Code
|
7
|
+
attr_accessor :requeues
|
8
|
+
|
9
|
+
def initialize(*)
|
10
|
+
self.requeues = 0
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def report
|
15
|
+
self.requeues = results.count(&:requeued?)
|
16
|
+
super
|
17
|
+
print_report
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def print_report
|
23
|
+
success = failures.zero? && errors.zero?
|
24
|
+
failures_count = "#{failures} failures, #{errors} errors,"
|
25
|
+
puts [
|
26
|
+
'Ran %d tests, %d assertions,' % [count, assertions],
|
27
|
+
success ? green(failures_count) : red(failures_count),
|
28
|
+
yellow("#{skips} skips, #{requeues} requeues"),
|
29
|
+
'in %.2fs' % total_time,
|
30
|
+
].join(' ')
|
31
|
+
end
|
32
|
+
|
33
|
+
def message_for(test)
|
34
|
+
e = test.failure
|
35
|
+
|
36
|
+
if test.requeued?
|
37
|
+
"Requeued:\n#{test.class}##{test.name} [#{location(e)}]:\n#{e.message}"
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def result_line
|
44
|
+
"#{super}, #{requeues} requeues"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'minitest/reporters'
|
2
|
+
require 'minitest/reporters/failure_formatter'
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Reporters
|
6
|
+
module RedisReporter
|
7
|
+
include ANSI::Code
|
8
|
+
|
9
|
+
COUNTERS = %w(
|
10
|
+
assertions
|
11
|
+
errors
|
12
|
+
failures
|
13
|
+
skips
|
14
|
+
requeues
|
15
|
+
total_time
|
16
|
+
).freeze
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :failure_formatter
|
20
|
+
end
|
21
|
+
self.failure_formatter = FailureFormatter
|
22
|
+
|
23
|
+
class Error
|
24
|
+
class << self
|
25
|
+
def load(payload)
|
26
|
+
Marshal.load(payload)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(data)
|
31
|
+
@data = data
|
32
|
+
end
|
33
|
+
|
34
|
+
def dump
|
35
|
+
Marshal.dump(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_name
|
39
|
+
@data[:test_name]
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_and_module_name
|
43
|
+
@data[:test_and_module_name]
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
output
|
48
|
+
end
|
49
|
+
|
50
|
+
def output
|
51
|
+
@data[:output]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Base < BaseReporter
|
56
|
+
def initialize(redis:, build_id:, **options)
|
57
|
+
@redis = redis
|
58
|
+
@key = "build:#{build_id}"
|
59
|
+
super(options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def total
|
63
|
+
redis.get(key('total')).to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
def processed
|
67
|
+
redis.get(key('processed')).to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
def completed?
|
71
|
+
total > 0 && total == processed
|
72
|
+
end
|
73
|
+
|
74
|
+
def error_reports
|
75
|
+
redis.hgetall(key('error-reports')).sort_by(&:first).map { |k, v| Error.load(v) }
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_reader :redis
|
81
|
+
|
82
|
+
def key(*args)
|
83
|
+
[@key, *args].join(':')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Summary < Base
|
88
|
+
include ANSI::Code
|
89
|
+
|
90
|
+
def report(io: STDOUT)
|
91
|
+
io.puts aggregates
|
92
|
+
errors = error_reports
|
93
|
+
io.puts errors
|
94
|
+
|
95
|
+
errors.empty?
|
96
|
+
end
|
97
|
+
|
98
|
+
def record(*)
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
|
102
|
+
def failures
|
103
|
+
fetch_summary['failures'].to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def errors
|
107
|
+
fetch_summary['errors'].to_i
|
108
|
+
end
|
109
|
+
|
110
|
+
def assertions
|
111
|
+
fetch_summary['assertions'].to_i
|
112
|
+
end
|
113
|
+
|
114
|
+
def skips
|
115
|
+
fetch_summary['skips'].to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
def requeues
|
119
|
+
fetch_summary['requeues'].to_i
|
120
|
+
end
|
121
|
+
|
122
|
+
def total_time
|
123
|
+
fetch_summary['total_time'].to_f
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def aggregates
|
129
|
+
success = failures.zero? && errors.zero?
|
130
|
+
failures_count = "#{failures} failures, #{errors} errors,"
|
131
|
+
|
132
|
+
[
|
133
|
+
'Ran %d tests, %d assertions,' % [processed, assertions],
|
134
|
+
success ? green(failures_count) : red(failures_count),
|
135
|
+
yellow("#{skips} skips, #{requeues} requeues"),
|
136
|
+
'in %.2fs (aggregated)' % total_time,
|
137
|
+
].join(' ')
|
138
|
+
end
|
139
|
+
|
140
|
+
def fetch_summary
|
141
|
+
@summary ||= begin
|
142
|
+
counts = redis.pipelined do
|
143
|
+
COUNTERS.each { |c| redis.hgetall(key(c)) }
|
144
|
+
end
|
145
|
+
COUNTERS.zip(counts.map { |h| h.values.map(&:to_f).inject(:+).to_f }).to_h
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class Worker < Base
|
151
|
+
attr_accessor :requeues
|
152
|
+
|
153
|
+
def initialize(worker_id:, **options)
|
154
|
+
super
|
155
|
+
@worker_id = worker_id
|
156
|
+
self.failures = 0
|
157
|
+
self.errors = 0
|
158
|
+
self.skips = 0
|
159
|
+
self.requeues = 0
|
160
|
+
end
|
161
|
+
|
162
|
+
def report
|
163
|
+
# noop
|
164
|
+
end
|
165
|
+
|
166
|
+
def record(test)
|
167
|
+
super
|
168
|
+
|
169
|
+
self.total_time = Minitest.clock_time - start_time
|
170
|
+
if test.requeued?
|
171
|
+
self.requeues += 1
|
172
|
+
elsif test.skipped?
|
173
|
+
self.skips += 1
|
174
|
+
elsif test.error?
|
175
|
+
self.errors += 1
|
176
|
+
elsif test.failure
|
177
|
+
self.failures += 1
|
178
|
+
end
|
179
|
+
|
180
|
+
redis.multi do
|
181
|
+
if (test.failure || test.error?) && !test.skipped?
|
182
|
+
redis.hset(key('error-reports'), "#{test.class}##{test.name}", dump(test))
|
183
|
+
else
|
184
|
+
redis.hdel(key('error-reports'), "#{test.class}##{test.name}")
|
185
|
+
end
|
186
|
+
COUNTERS.each do |counter|
|
187
|
+
redis.hset(key(counter), worker_id, send(counter))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def dump(test)
|
195
|
+
Error.new(RedisReporter.failure_formatter.new(test).to_h).dump
|
196
|
+
end
|
197
|
+
|
198
|
+
attr_reader :worker_id, :aggregates
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ci-queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.12'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest-reporters
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.1'
|
83
97
|
description: To parallelize your CI without having to balance your tests
|
84
98
|
email:
|
85
99
|
- jean.boussier@shopify.com
|
@@ -102,11 +116,15 @@ files:
|
|
102
116
|
- lib/ci/queue/file.rb
|
103
117
|
- lib/ci/queue/redis.rb
|
104
118
|
- lib/ci/queue/redis/base.rb
|
119
|
+
- lib/ci/queue/redis/retry.rb
|
105
120
|
- lib/ci/queue/redis/supervisor.rb
|
106
121
|
- lib/ci/queue/redis/worker.rb
|
107
122
|
- lib/ci/queue/static.rb
|
108
123
|
- lib/ci/queue/version.rb
|
109
124
|
- lib/minitest/queue.rb
|
125
|
+
- lib/minitest/reporters/failure_formatter.rb
|
126
|
+
- lib/minitest/reporters/queue_reporter.rb
|
127
|
+
- lib/minitest/reporters/redis_reporter.rb
|
110
128
|
homepage: https://github.com/Shopify/ci-queue
|
111
129
|
licenses:
|
112
130
|
- MIT
|
@@ -127,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
145
|
version: '0'
|
128
146
|
requirements: []
|
129
147
|
rubyforge_project:
|
130
|
-
rubygems_version: 2.
|
148
|
+
rubygems_version: 2.5.1
|
131
149
|
signing_key:
|
132
150
|
specification_version: 4
|
133
151
|
summary: Distribute tests over many workers using a queue
|