ci-queue 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +48 -0
- data/exe/minitest-queue +4 -0
- data/lib/ci/queue.rb +28 -0
- data/lib/ci/queue/configuration.rb +46 -0
- data/lib/ci/queue/file.rb +8 -2
- data/lib/ci/queue/index.rb +20 -0
- data/lib/ci/queue/output_helpers.rb +64 -0
- data/lib/ci/queue/redis.rb +9 -2
- data/lib/ci/queue/redis/base.rb +20 -9
- data/lib/ci/queue/redis/retry.rb +7 -7
- data/lib/ci/queue/redis/supervisor.rb +19 -3
- data/lib/ci/queue/redis/worker.rb +40 -26
- data/lib/ci/queue/static.rb +43 -12
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue.rb +1 -1
- data/lib/minitest/queue/runner.rb +243 -0
- data/lib/minitest/reporters/failure_formatter.rb +0 -1
- data/lib/minitest/reporters/queue_reporter.rb +4 -2
- data/lib/minitest/reporters/redis_reporter.rb +10 -6
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97f69595ec09509df32703213fd2d882db14f017
|
4
|
+
data.tar.gz: cae08e13e586bd5c7cc8927057225084b0ee0d74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 742b3d39caf903b480950a86eafb687776e8b4d1a9465d28e1ce29ff37d3242f3127a40d7115e51eddec03b6e22322cef0108ef2a64f6a5744b642b3e1b14ecf
|
7
|
+
data.tar.gz: 8f753085f6408c98e698c0a75c06b77a427e066cbad9aa7c1db102613cdcf4b6d25d27e93145e86928bd5ae956b54adeb44245f36ee2ceee3eb3e9646c45363b
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
## Installation
|
2
|
+
|
3
|
+
Add this line to your application's Gemfile:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
gem 'ci-queue'
|
7
|
+
```
|
8
|
+
|
9
|
+
And then execute:
|
10
|
+
|
11
|
+
$ bundle
|
12
|
+
|
13
|
+
Or install it yourself as:
|
14
|
+
|
15
|
+
$ gem install ci-queue
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Supported CI providers
|
20
|
+
|
21
|
+
`ci-queue` automatically infers most of its configuration if ran on one of the following CI providers:
|
22
|
+
|
23
|
+
- Buildkite
|
24
|
+
- CircleCI
|
25
|
+
- Travis
|
26
|
+
|
27
|
+
If you are using another CI system, please refer to the command usage message.
|
28
|
+
|
29
|
+
### Minitest
|
30
|
+
|
31
|
+
Assuming you use one of the supported CI providers, the command can be as simple as:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
minitest-queue --queue redis://example.com run -Itest test/**/*_test.rb
|
35
|
+
```
|
36
|
+
|
37
|
+
Additionally you can configure the requeue settings (see main README) with `--max-requeues` and `--requeue-tolerance`.
|
38
|
+
|
39
|
+
|
40
|
+
If you'd like to centralize the error reporting you can do so with:
|
41
|
+
|
42
|
+
```
|
43
|
+
minitest-queue --queue redis://example.com --timeout 600 report
|
44
|
+
```
|
45
|
+
|
46
|
+
### RSpec
|
47
|
+
|
48
|
+
The RSpec integration is not implemented yet.
|
data/exe/minitest-queue
ADDED
data/lib/ci/queue.rb
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
|
1
4
|
require 'ci/queue/version'
|
5
|
+
require 'ci/queue/output_helpers'
|
6
|
+
require 'ci/queue/index'
|
7
|
+
require 'ci/queue/configuration'
|
2
8
|
require 'ci/queue/static'
|
3
9
|
require 'ci/queue/file'
|
10
|
+
|
11
|
+
module CI
|
12
|
+
module Queue
|
13
|
+
extend self
|
14
|
+
|
15
|
+
def from_uri(url, config)
|
16
|
+
uri = URI(url)
|
17
|
+
implementation = case uri.scheme
|
18
|
+
when 'list'
|
19
|
+
Static
|
20
|
+
when 'file', nil
|
21
|
+
File
|
22
|
+
when 'redis'
|
23
|
+
require 'ci/queue/redis'
|
24
|
+
Redis
|
25
|
+
else
|
26
|
+
raise ArgumentError, "Don't know how to handle #{uri.scheme} URLs"
|
27
|
+
end
|
28
|
+
implementation.from_uri(uri, config)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module CI
|
2
|
+
module Queue
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :timeout, :build_id, :worker_id, :max_requeues, :requeue_tolerance, :namespace, :seed
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def from_env(env)
|
8
|
+
new(
|
9
|
+
build_id: env['CIRCLE_BUILD_URL'] || env['BUILDKITE_BUILD_ID'] || env['TRAVIS_BUILD_ID'],
|
10
|
+
worker_id: env['CIRCLE_NODE_INDEX'] || env['BUILDKITE_PARALLEL_JOB'],
|
11
|
+
seed: env['CIRCLE_SHA1'] || env['BUILDKITE_COMMIT'] || env['TRAVIS_COMMIT'],
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(
|
17
|
+
timeout: 30, build_id: nil, worker_id: nil, max_requeues: 0, requeue_tolerance: 0,
|
18
|
+
namespace: nil, seed: nil
|
19
|
+
)
|
20
|
+
@namespace = namespace
|
21
|
+
@timeout = timeout
|
22
|
+
@build_id = build_id
|
23
|
+
@worker_id = worker_id
|
24
|
+
@max_requeues = max_requeues
|
25
|
+
@requeue_tolerance = requeue_tolerance
|
26
|
+
@seed = seed
|
27
|
+
end
|
28
|
+
|
29
|
+
def seed
|
30
|
+
@seed || build_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_id
|
34
|
+
if namespace
|
35
|
+
"#{namespace}:#{@build_id}"
|
36
|
+
else
|
37
|
+
@build_id
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def global_max_requeues(tests_count)
|
42
|
+
(tests_count * Float(requeue_tolerance)).ceil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/ci/queue/file.rb
CHANGED
@@ -3,8 +3,14 @@ require 'ci/queue/static'
|
|
3
3
|
module CI
|
4
4
|
module Queue
|
5
5
|
class File < Static
|
6
|
-
|
7
|
-
|
6
|
+
class << self
|
7
|
+
def from_uri(uri, config)
|
8
|
+
new(uri.path, config)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(path, *args)
|
13
|
+
super(::File.readlines(path).map(&:strip).reject(&:empty?), *args)
|
8
14
|
end
|
9
15
|
end
|
10
16
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CI
|
2
|
+
module Queue
|
3
|
+
class Index
|
4
|
+
def initialize(objects, &indexer)
|
5
|
+
@index = objects.map { |o| [indexer.call(o), o] }.to_h
|
6
|
+
@indexer = indexer
|
7
|
+
end
|
8
|
+
|
9
|
+
def fetch(key)
|
10
|
+
@index.fetch(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def key(value)
|
14
|
+
key = @indexer.call(value)
|
15
|
+
raise KeyError, "value not found: #{value.inspect}" unless @index.key?(key)
|
16
|
+
key
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'ansi'
|
2
|
+
|
3
|
+
module CI
|
4
|
+
module Queue
|
5
|
+
module OutputHelpers
|
6
|
+
include ANSI::Code
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def step(*args)
|
11
|
+
ci_provider.step(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def reopen_previous_step
|
15
|
+
ci_provider.reopen_previous_step
|
16
|
+
end
|
17
|
+
|
18
|
+
def close_previous_step
|
19
|
+
ci_provider.close_previous_step
|
20
|
+
end
|
21
|
+
|
22
|
+
def ci_provider
|
23
|
+
@ci_provider ||= if ENV['BUILDKITE']
|
24
|
+
BuildkiteOutput
|
25
|
+
else
|
26
|
+
DefaultOutput
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module DefaultOutput
|
31
|
+
extend self
|
32
|
+
|
33
|
+
def step(title, collapsed: true)
|
34
|
+
puts title
|
35
|
+
end
|
36
|
+
|
37
|
+
def reopen_previous_step
|
38
|
+
# noop
|
39
|
+
end
|
40
|
+
|
41
|
+
def close_previous_step
|
42
|
+
# noop
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module BuildkiteOutput
|
47
|
+
extend self
|
48
|
+
|
49
|
+
def step(title, collapsed: true)
|
50
|
+
prefix = collapsed ? '---' : '+++'
|
51
|
+
puts "#{prefix} #{title}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def reopen_previous_step
|
55
|
+
puts '^^^ +++'
|
56
|
+
end
|
57
|
+
|
58
|
+
def close_previous_step
|
59
|
+
puts '^^^ ---'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/ci/queue/redis.rb
CHANGED
@@ -10,8 +10,15 @@ module CI
|
|
10
10
|
Error = Class.new(StandardError)
|
11
11
|
LostMaster = Class.new(Error)
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def new(*args)
|
16
|
+
Worker.new(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_uri(uri, config)
|
20
|
+
new(uri.to_s, config)
|
21
|
+
end
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
data/lib/ci/queue/redis/base.rb
CHANGED
@@ -2,13 +2,14 @@ module CI
|
|
2
2
|
module Queue
|
3
3
|
module Redis
|
4
4
|
class Base
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@
|
5
|
+
def initialize(redis_url, config)
|
6
|
+
@redis_url = redis_url
|
7
|
+
@redis = ::Redis.new(url: redis_url)
|
8
|
+
@config = config
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
size == 0
|
11
|
+
def exhausted?
|
12
|
+
queue_initialized? && size == 0
|
12
13
|
end
|
13
14
|
|
14
15
|
def size
|
@@ -22,7 +23,7 @@ module CI
|
|
22
23
|
redis.multi do
|
23
24
|
redis.lrange(key('queue'), 0, -1)
|
24
25
|
redis.zrange(key('running'), 0, -1)
|
25
|
-
end.flatten.reverse
|
26
|
+
end.flatten.reverse.map { |k| index.fetch(k) }
|
26
27
|
end
|
27
28
|
|
28
29
|
def progress
|
@@ -32,8 +33,7 @@ module CI
|
|
32
33
|
def wait_for_master(timeout: 10)
|
33
34
|
return true if master?
|
34
35
|
(timeout * 10 + 1).to_i.times do
|
35
|
-
|
36
|
-
when 'ready', 'finished'
|
36
|
+
if queue_initialized?
|
37
37
|
return true
|
38
38
|
else
|
39
39
|
sleep 0.1
|
@@ -46,14 +46,25 @@ module CI
|
|
46
46
|
redis.scard(key('workers'))
|
47
47
|
end
|
48
48
|
|
49
|
+
def queue_initialized?
|
50
|
+
@queue_initialized ||= begin
|
51
|
+
status = master_status
|
52
|
+
status == 'ready' || status == 'finished'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
49
56
|
private
|
50
57
|
|
51
|
-
attr_reader :redis, :
|
58
|
+
attr_reader :redis, :config, :redis_url
|
52
59
|
|
53
60
|
def key(*args)
|
54
61
|
['build', build_id, *args].join(':')
|
55
62
|
end
|
56
63
|
|
64
|
+
def build_id
|
65
|
+
config.build_id
|
66
|
+
end
|
67
|
+
|
57
68
|
def master_status
|
58
69
|
redis.get(key('master-status'))
|
59
70
|
end
|
data/lib/ci/queue/redis/retry.rb
CHANGED
@@ -2,27 +2,27 @@ module CI
|
|
2
2
|
module Queue
|
3
3
|
module Redis
|
4
4
|
class Retry < Static
|
5
|
-
def initialize(tests, redis
|
5
|
+
def initialize(tests, config, redis:)
|
6
6
|
@redis = redis
|
7
|
-
|
8
|
-
@worker_id = worker_id
|
9
|
-
super(tests, **args)
|
7
|
+
super(tests, config)
|
10
8
|
end
|
11
9
|
|
12
10
|
def minitest_reporters
|
11
|
+
require 'minitest/reporters/queue_reporter'
|
13
12
|
require 'minitest/reporters/redis_reporter'
|
14
13
|
@minitest_reporters ||= [
|
14
|
+
Minitest::Reporters::QueueReporter.new,
|
15
15
|
Minitest::Reporters::RedisReporter::Worker.new(
|
16
16
|
redis: redis,
|
17
|
-
build_id: build_id,
|
18
|
-
worker_id: worker_id,
|
17
|
+
build_id: config.build_id,
|
18
|
+
worker_id: config.worker_id,
|
19
19
|
)
|
20
20
|
]
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
-
attr_reader :redis
|
25
|
+
attr_reader :redis
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -6,11 +6,27 @@ module CI
|
|
6
6
|
false
|
7
7
|
end
|
8
8
|
|
9
|
+
def minitest_reporters
|
10
|
+
require 'minitest/reporters/redis_reporter'
|
11
|
+
@reporters ||= [
|
12
|
+
Minitest::Reporters::RedisReporter::Summary.new(
|
13
|
+
build_id: build_id,
|
14
|
+
redis: redis,
|
15
|
+
)
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
9
19
|
def wait_for_workers
|
10
|
-
return false unless wait_for_master
|
20
|
+
return false unless wait_for_master(timeout: config.timeout)
|
11
21
|
|
12
|
-
|
13
|
-
|
22
|
+
time_left = config.timeout
|
23
|
+
until exhausted? || time_left <= 0
|
24
|
+
sleep 0.1
|
25
|
+
time_left -= 0.1
|
26
|
+
end
|
27
|
+
exhausted?
|
28
|
+
rescue CI::Queue::Redis::LostMaster
|
29
|
+
false
|
14
30
|
end
|
15
31
|
end
|
16
32
|
end
|
@@ -13,15 +13,20 @@ module CI
|
|
13
13
|
class Worker < Base
|
14
14
|
attr_reader :total
|
15
15
|
|
16
|
-
def initialize(
|
16
|
+
def initialize(redis, config)
|
17
17
|
@reserved_test = nil
|
18
|
-
@max_requeues = max_requeues
|
19
|
-
@global_max_requeues = (tests.size * requeue_tolerance).ceil
|
20
18
|
@shutdown_required = false
|
21
|
-
super(redis
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
super(redis, config)
|
20
|
+
end
|
21
|
+
|
22
|
+
def populate(tests, &indexer)
|
23
|
+
@index = Index.new(tests, &indexer)
|
24
|
+
push(tests.map { |t| index.key(t) })
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def populated?
|
29
|
+
!!defined?(@index)
|
25
30
|
end
|
26
31
|
|
27
32
|
def shutdown!
|
@@ -38,9 +43,9 @@ module CI
|
|
38
43
|
|
39
44
|
def poll
|
40
45
|
wait_for_master
|
41
|
-
until shutdown_required? ||
|
46
|
+
until shutdown_required? || exhausted?
|
42
47
|
if test = reserve
|
43
|
-
yield test
|
48
|
+
yield index.fetch(test)
|
44
49
|
else
|
45
50
|
sleep 0.05
|
46
51
|
end
|
@@ -48,19 +53,20 @@ module CI
|
|
48
53
|
rescue ::Redis::BaseConnectionError
|
49
54
|
end
|
50
55
|
|
51
|
-
def retry_queue
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
)
|
56
|
+
def retry_queue
|
57
|
+
log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1).reverse.uniq
|
58
|
+
Retry.new(log, config, redis: redis)
|
59
|
+
end
|
60
|
+
|
61
|
+
def supervisor
|
62
|
+
Supervisor.new(redis_url, config)
|
59
63
|
end
|
60
64
|
|
61
65
|
def minitest_reporters
|
66
|
+
require 'minitest/reporters/queue_reporter'
|
62
67
|
require 'minitest/reporters/redis_reporter'
|
63
68
|
@minitest_reporters ||= [
|
69
|
+
Minitest::Reporters::QueueReporter.new,
|
64
70
|
Minitest::Reporters::RedisReporter::Worker.new(
|
65
71
|
redis: redis,
|
66
72
|
build_id: build_id,
|
@@ -70,30 +76,40 @@ module CI
|
|
70
76
|
end
|
71
77
|
|
72
78
|
def acknowledge(test)
|
73
|
-
|
79
|
+
test_key = index.key(test)
|
80
|
+
raise_on_mismatching_test(test_key)
|
74
81
|
eval_script(
|
75
82
|
:acknowledge,
|
76
83
|
keys: [key('running'), key('processed')],
|
77
|
-
argv: [
|
84
|
+
argv: [test_key],
|
78
85
|
) == 1
|
79
86
|
end
|
80
87
|
|
81
88
|
def requeue(test, offset: Redis.requeue_offset)
|
82
|
-
|
89
|
+
test_key = index.key(test)
|
90
|
+
raise_on_mismatching_test(test_key)
|
83
91
|
|
84
92
|
requeued = eval_script(
|
85
93
|
:requeue,
|
86
94
|
keys: [key('processed'), key('requeues-count'), key('queue'), key('running')],
|
87
|
-
argv: [max_requeues, global_max_requeues,
|
95
|
+
argv: [config.max_requeues, config.global_max_requeues(total), test_key, offset],
|
88
96
|
) == 1
|
89
97
|
|
90
|
-
@reserved_test =
|
98
|
+
@reserved_test = test_key unless requeued
|
91
99
|
requeued
|
92
100
|
end
|
93
101
|
|
94
102
|
private
|
95
103
|
|
96
|
-
attr_reader :
|
104
|
+
attr_reader :index
|
105
|
+
|
106
|
+
def worker_id
|
107
|
+
config.worker_id
|
108
|
+
end
|
109
|
+
|
110
|
+
def timeout
|
111
|
+
config.timeout
|
112
|
+
end
|
97
113
|
|
98
114
|
def raise_on_mismatching_test(test)
|
99
115
|
if @reserved_test == test
|
@@ -112,9 +128,6 @@ module CI
|
|
112
128
|
@reserved_test = (try_to_reserve_lost_test || try_to_reserve_test)
|
113
129
|
end
|
114
130
|
|
115
|
-
RESERVE_TEST = %{
|
116
|
-
}
|
117
|
-
|
118
131
|
def try_to_reserve_test
|
119
132
|
eval_script(
|
120
133
|
:reserve,
|
@@ -133,6 +146,7 @@ module CI
|
|
133
146
|
|
134
147
|
def push(tests)
|
135
148
|
@total = tests.size
|
149
|
+
|
136
150
|
if @master = redis.setnx(key('master-status'), 'setup')
|
137
151
|
redis.multi do
|
138
152
|
redis.lpush(key('queue'), tests)
|
data/lib/ci/queue/static.rb
CHANGED
@@ -1,18 +1,48 @@
|
|
1
1
|
module CI
|
2
2
|
module Queue
|
3
3
|
class Static
|
4
|
+
class << self
|
5
|
+
def from_uri(uri, config)
|
6
|
+
tests = uri.opaque.split(':').map { |t| CGI.unescape(t) }
|
7
|
+
new(tests, config)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
4
11
|
attr_reader :progress, :total
|
5
12
|
|
6
|
-
def initialize(tests,
|
13
|
+
def initialize(tests, config)
|
7
14
|
@queue = tests
|
15
|
+
@config = config
|
8
16
|
@progress = 0
|
9
17
|
@total = tests.size
|
10
|
-
|
11
|
-
|
18
|
+
end
|
19
|
+
|
20
|
+
def minitest_reporters
|
21
|
+
require 'minitest/reporters/queue_reporter'
|
22
|
+
@minitest_reporters ||= [
|
23
|
+
Minitest::Reporters::QueueReporter.new,
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def supervisor
|
28
|
+
raise NotImplementedError, "This type of queue can't be supervised"
|
29
|
+
end
|
30
|
+
|
31
|
+
def retry_queue
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def populate(tests, &indexer)
|
36
|
+
@index = Index.new(tests, &indexer)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def populated?
|
41
|
+
!!defined?(@index)
|
12
42
|
end
|
13
43
|
|
14
44
|
def to_a
|
15
|
-
@queue.
|
45
|
+
@queue.map { |i| index.fetch(i) }
|
16
46
|
end
|
17
47
|
|
18
48
|
def size
|
@@ -21,12 +51,12 @@ module CI
|
|
21
51
|
|
22
52
|
def poll
|
23
53
|
while test = @queue.shift
|
24
|
-
yield test
|
54
|
+
yield index.fetch(test)
|
25
55
|
@progress += 1
|
26
56
|
end
|
27
57
|
end
|
28
58
|
|
29
|
-
def
|
59
|
+
def exhausted?
|
30
60
|
@queue.empty?
|
31
61
|
end
|
32
62
|
|
@@ -35,18 +65,19 @@ module CI
|
|
35
65
|
end
|
36
66
|
|
37
67
|
def requeue(test)
|
38
|
-
|
39
|
-
|
40
|
-
|
68
|
+
key = index.key(test)
|
69
|
+
return false unless should_requeue?(key)
|
70
|
+
requeues[key] += 1
|
71
|
+
@queue.unshift(index.key(test))
|
41
72
|
true
|
42
73
|
end
|
43
74
|
|
44
75
|
private
|
45
76
|
|
46
|
-
attr_reader :
|
77
|
+
attr_reader :index, :config
|
47
78
|
|
48
|
-
def should_requeue?(
|
49
|
-
requeues[
|
79
|
+
def should_requeue?(key)
|
80
|
+
requeues[key] < config.max_requeues && requeues.values.inject(0, :+) < config.global_max_requeues(total)
|
50
81
|
end
|
51
82
|
|
52
83
|
def requeues
|
data/lib/ci/queue/version.rb
CHANGED
data/lib/minitest/queue.rb
CHANGED
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'minitest/queue'
|
3
|
+
require 'ci/queue'
|
4
|
+
require 'digest/md5'
|
5
|
+
|
6
|
+
module Minitest
|
7
|
+
module Queue
|
8
|
+
class Runner
|
9
|
+
include ::CI::Queue::OutputHelpers
|
10
|
+
|
11
|
+
Error = Class.new(StandardError)
|
12
|
+
MissingParameter = Class.new(Error)
|
13
|
+
|
14
|
+
def self.invoke(argv)
|
15
|
+
new(argv).run!
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(argv)
|
19
|
+
@queue_config = CI::Queue::Configuration.from_env(ENV)
|
20
|
+
@command, @argv = parse(argv)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run!
|
24
|
+
invalid_usage!("No command given") if command.nil?
|
25
|
+
invalid_usage!('Missing queue URL') unless queue_url
|
26
|
+
|
27
|
+
@queue = CI::Queue.from_uri(queue_url, queue_config)
|
28
|
+
|
29
|
+
method = "#{command}_command"
|
30
|
+
if respond_to?(method)
|
31
|
+
public_send(method)
|
32
|
+
else
|
33
|
+
invalid_usage!("Unknown command: #{command}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def retry_command
|
38
|
+
self.queue = queue.retry_queue
|
39
|
+
run_command
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_command
|
43
|
+
set_load_path
|
44
|
+
Minitest.queue = queue
|
45
|
+
trap('TERM') { Minitest.queue.shutdown! }
|
46
|
+
trap('INT') { Minitest.queue.shutdown! }
|
47
|
+
load_tests
|
48
|
+
populate_queue
|
49
|
+
# Let minitest's at_exit hook trigger
|
50
|
+
end
|
51
|
+
|
52
|
+
def report_command
|
53
|
+
supervisor = begin
|
54
|
+
queue.supervisor
|
55
|
+
rescue NotImplementedError => error
|
56
|
+
abort! error.message
|
57
|
+
end
|
58
|
+
|
59
|
+
step("Waiting for workers to complete")
|
60
|
+
|
61
|
+
unless supervisor.wait_for_workers
|
62
|
+
unless supervisor.queue_initialized?
|
63
|
+
abort! "No master was elected. Did all workers crash?"
|
64
|
+
end
|
65
|
+
|
66
|
+
unless supervisor.exhausted?
|
67
|
+
abort! "#{supervisor.size} tests weren't run."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
success = supervisor.minitest_reporters.all?(&:success?)
|
72
|
+
supervisor.minitest_reporters.each do |reporter|
|
73
|
+
reporter.report
|
74
|
+
end
|
75
|
+
|
76
|
+
STDOUT.flush
|
77
|
+
exit! success ? 0 : 1
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
attr_reader :queue_config, :options, :command, :argv
|
83
|
+
attr_accessor :queue, :queue_url, :load_paths
|
84
|
+
|
85
|
+
def populate_queue
|
86
|
+
Minitest.queue.populate(shuffle(Minitest.loaded_tests), &:to_s) # TODO: stop serializing
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_load_path
|
90
|
+
if paths = load_paths
|
91
|
+
paths.split(':').reverse.each do |path|
|
92
|
+
$LOAD_PATH.unshift(File.expand_path(path))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_tests
|
98
|
+
argv.sort.each do |f|
|
99
|
+
require File.expand_path(f)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse(argv)
|
104
|
+
parser.parse!(argv)
|
105
|
+
command = argv.shift
|
106
|
+
return command, argv
|
107
|
+
end
|
108
|
+
|
109
|
+
def parser
|
110
|
+
@parser ||= OptionParser.new do |opts|
|
111
|
+
opts.banner = "Usage: minitest-queue [options] COMMAND [ARGS]"
|
112
|
+
|
113
|
+
opts.separator ""
|
114
|
+
opts.separator "Example: minitest-queue -Itest --queue redis://example.com run test/**/*_test.rb"
|
115
|
+
|
116
|
+
opts.separator ""
|
117
|
+
opts.separator "GLOBAL OPTIONS"
|
118
|
+
|
119
|
+
|
120
|
+
help = split_heredoc(<<-EOS)
|
121
|
+
URL of the queue, e.g. redis://example.com.
|
122
|
+
Defaults to $CI_QUEUE_URL if set.
|
123
|
+
EOS
|
124
|
+
opts.separator ""
|
125
|
+
opts.on('--queue URL', *help) do |url|
|
126
|
+
self.queue_url = url
|
127
|
+
end
|
128
|
+
|
129
|
+
help = split_heredoc(<<-EOS)
|
130
|
+
Unique identifier for the workload. All workers working on the same suite of tests must have the same build identifier.
|
131
|
+
If the build is tried again, or another revision is built, this value must be different.
|
132
|
+
It's automatically inferred on Buildkite, CircleCI and Travis.
|
133
|
+
EOS
|
134
|
+
opts.separator ""
|
135
|
+
opts.on('--build BUILD_ID', *help) do |build_id|
|
136
|
+
queue_config.build_id = build_id
|
137
|
+
end
|
138
|
+
|
139
|
+
help = split_heredoc(<<-EOS)
|
140
|
+
Optional. Sets a prefix for the build id in case a single CI build runs multiple independent test suites.
|
141
|
+
Example: --namespace integration
|
142
|
+
EOS
|
143
|
+
opts.separator ""
|
144
|
+
opts.on('--namespace NAMESPACE', *help) do |namespace|
|
145
|
+
queue_config.namespace = namespace
|
146
|
+
end
|
147
|
+
|
148
|
+
opts.separator ""
|
149
|
+
opts.separator "COMMANDS"
|
150
|
+
opts.separator ""
|
151
|
+
opts.separator " run [TEST_FILES...]: Participate in leader election, and then work off the test queue."
|
152
|
+
|
153
|
+
help = split_heredoc(<<-EOS)
|
154
|
+
Specify a timeout after which if a test haven't completed, it will be picked up by another worker.
|
155
|
+
It is very important to set this vlaue higher than the slowest test in the suite, otherwise performance will be impacted.
|
156
|
+
Defaults to 30 seconds.
|
157
|
+
EOS
|
158
|
+
opts.separator ""
|
159
|
+
opts.on('--timeout TIMEOUT', *help) do |timeout|
|
160
|
+
queue_config.timeout = Float(timeout)
|
161
|
+
end
|
162
|
+
|
163
|
+
help = split_heredoc(<<-EOS)
|
164
|
+
Specify $LOAD_PATH directory, similar to Ruby's -I
|
165
|
+
EOS
|
166
|
+
opts.separator ""
|
167
|
+
opts.on('-IPATHS', *help) do |paths|
|
168
|
+
self.load_paths = paths
|
169
|
+
end
|
170
|
+
|
171
|
+
help = split_heredoc(<<-EOS)
|
172
|
+
Sepcify a seed used to shuffle the test suite.
|
173
|
+
On Buildkite, CircleCI and Travis, the commit revision will be used by default.
|
174
|
+
EOS
|
175
|
+
opts.separator ""
|
176
|
+
opts.on('--seed SEED', *help) do |seed|
|
177
|
+
queue_config.seed = seed
|
178
|
+
end
|
179
|
+
|
180
|
+
help = split_heredoc(<<-EOS)
|
181
|
+
A unique identifier for this worker, It must be consistent to allow retries.
|
182
|
+
If not specified, retries won't be available.
|
183
|
+
It's automatically inferred on Buildkite and CircleCI.
|
184
|
+
EOS
|
185
|
+
opts.separator ""
|
186
|
+
opts.on('--worker WORKER_ID', *help) do |worker_id|
|
187
|
+
queue_config.worker_id = worker_id
|
188
|
+
end
|
189
|
+
|
190
|
+
help = split_heredoc(<<-EOS)
|
191
|
+
Defines how many time a single test can be requeued.
|
192
|
+
Defaults to 0.
|
193
|
+
EOS
|
194
|
+
opts.separator ""
|
195
|
+
opts.on('--max-requeues MAX') do |max|
|
196
|
+
queue_config.max_requeues = Integer(max)
|
197
|
+
end
|
198
|
+
|
199
|
+
help = split_heredoc(<<-EOS)
|
200
|
+
Defines how many requeues can happen overall, based on the test suite size. e.g 0.05 for 5%.
|
201
|
+
Defaults to 0.
|
202
|
+
EOS
|
203
|
+
opts.separator ""
|
204
|
+
opts.on('--requeue-tolerance RATIO', *help) do |ratio|
|
205
|
+
queue_config.requeue_tolerance = Float(ratio)
|
206
|
+
end
|
207
|
+
|
208
|
+
opts.separator ""
|
209
|
+
opts.separator " retry: Replays a previous run in the same order."
|
210
|
+
|
211
|
+
opts.separator ""
|
212
|
+
opts.separator " report: Wait for all workers to complete and summarize the test failures."
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def split_heredoc(string)
|
217
|
+
string.lines.map(&:strip)
|
218
|
+
end
|
219
|
+
|
220
|
+
def shuffle(tests)
|
221
|
+
random = Random.new(Digest::MD5.hexdigest(queue_config.seed).to_i(16))
|
222
|
+
tests.shuffle(random: random)
|
223
|
+
end
|
224
|
+
|
225
|
+
def queue_url
|
226
|
+
@queue_url || ENV['CI_QUEUE_URL']
|
227
|
+
end
|
228
|
+
|
229
|
+
def invalid_usage!(message)
|
230
|
+
reopen_previous_step
|
231
|
+
puts red(message)
|
232
|
+
puts parser
|
233
|
+
exit! 1 # exit! is required to avoid minitest at_exit callback
|
234
|
+
end
|
235
|
+
|
236
|
+
def abort!(message)
|
237
|
+
reopen_previous_step
|
238
|
+
puts red(message)
|
239
|
+
exit! 1 # exit! is required to avoid minitest at_exit callback
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
require 'ci/queue/output_helpers'
|
1
2
|
require 'minitest/reporters'
|
2
3
|
|
3
4
|
module Minitest
|
4
5
|
module Reporters
|
5
6
|
class QueueReporter < BaseReporter
|
6
|
-
include
|
7
|
+
include ::CI::Queue::OutputHelpers
|
7
8
|
attr_accessor :requeues
|
8
9
|
|
9
10
|
def initialize(*)
|
@@ -20,9 +21,10 @@ module Minitest
|
|
20
21
|
private
|
21
22
|
|
22
23
|
def print_report
|
24
|
+
reopen_previous_step if failures > 0 || errors > 0
|
23
25
|
success = failures.zero? && errors.zero?
|
24
26
|
failures_count = "#{failures} failures, #{errors} errors,"
|
25
|
-
|
27
|
+
step [
|
26
28
|
'Ran %d tests, %d assertions,' % [count, assertions],
|
27
29
|
success ? green(failures_count) : red(failures_count),
|
28
30
|
yellow("#{skips} skips, #{requeues} requeues"),
|
@@ -89,16 +89,20 @@ module Minitest
|
|
89
89
|
end
|
90
90
|
|
91
91
|
class Summary < Base
|
92
|
-
include
|
92
|
+
include ::CI::Queue::OutputHelpers
|
93
93
|
|
94
|
-
def report
|
95
|
-
|
94
|
+
def report
|
95
|
+
puts aggregates
|
96
96
|
errors = error_reports
|
97
|
-
|
97
|
+
puts errors
|
98
98
|
|
99
99
|
errors.empty?
|
100
100
|
end
|
101
101
|
|
102
|
+
def success?
|
103
|
+
errors == 0 && failures == 0
|
104
|
+
end
|
105
|
+
|
102
106
|
def record(*)
|
103
107
|
raise NotImplementedError
|
104
108
|
end
|
@@ -133,12 +137,12 @@ module Minitest
|
|
133
137
|
success = failures.zero? && errors.zero?
|
134
138
|
failures_count = "#{failures} failures, #{errors} errors,"
|
135
139
|
|
136
|
-
[
|
140
|
+
step([
|
137
141
|
'Ran %d tests, %d assertions,' % [processed, assertions],
|
138
142
|
success ? green(failures_count) : red(failures_count),
|
139
143
|
yellow("#{skips} skips, #{requeues} requeues"),
|
140
144
|
'in %.2fs (aggregated)' % total_time,
|
141
|
-
].join(' ')
|
145
|
+
].join(' '), collapsed: success)
|
142
146
|
end
|
143
147
|
|
144
148
|
def fetch_summary
|
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.7.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: 2017-
|
11
|
+
date: 2017-11-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -97,12 +97,14 @@ dependencies:
|
|
97
97
|
description: To parallelize your CI without having to balance your tests
|
98
98
|
email:
|
99
99
|
- jean.boussier@shopify.com
|
100
|
-
executables:
|
100
|
+
executables:
|
101
|
+
- minitest-queue
|
101
102
|
extensions: []
|
102
103
|
extra_rdoc_files: []
|
103
104
|
files:
|
104
105
|
- ".gitignore"
|
105
106
|
- Gemfile
|
107
|
+
- README.md
|
106
108
|
- Rakefile
|
107
109
|
- bin/bundler
|
108
110
|
- bin/console
|
@@ -110,8 +112,12 @@ files:
|
|
110
112
|
- bin/setup
|
111
113
|
- ci-queue.gemspec
|
112
114
|
- dev.yml
|
115
|
+
- exe/minitest-queue
|
113
116
|
- lib/ci/queue.rb
|
117
|
+
- lib/ci/queue/configuration.rb
|
114
118
|
- lib/ci/queue/file.rb
|
119
|
+
- lib/ci/queue/index.rb
|
120
|
+
- lib/ci/queue/output_helpers.rb
|
115
121
|
- lib/ci/queue/redis.rb
|
116
122
|
- lib/ci/queue/redis/acknowledge.lua
|
117
123
|
- lib/ci/queue/redis/base.rb
|
@@ -124,6 +130,7 @@ files:
|
|
124
130
|
- lib/ci/queue/static.rb
|
125
131
|
- lib/ci/queue/version.rb
|
126
132
|
- lib/minitest/queue.rb
|
133
|
+
- lib/minitest/queue/runner.rb
|
127
134
|
- lib/minitest/reporters/failure_formatter.rb
|
128
135
|
- lib/minitest/reporters/order_reporter.rb
|
129
136
|
- lib/minitest/reporters/queue_reporter.rb
|
@@ -148,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
155
|
version: '0'
|
149
156
|
requirements: []
|
150
157
|
rubyforge_project:
|
151
|
-
rubygems_version: 2.6.
|
158
|
+
rubygems_version: 2.6.10
|
152
159
|
signing_key:
|
153
160
|
specification_version: 4
|
154
161
|
summary: Distribute tests over many workers using a queue
|