ci-queue 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/ci-queue.svg)](https://rubygems.org/gems/ci-queue)
|
4
|
+
[![Build Status](https://travis-ci.org/Shopify/ci-queue.svg?branch=master)](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
|