ci-queue 0.10.1 → 0.11.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/README.md +6 -7
- data/lib/ci/queue.rb +1 -0
- data/lib/ci/queue/build_record.rb +44 -0
- data/lib/ci/queue/redis.rb +1 -0
- data/lib/ci/queue/redis/build_record.rb +63 -0
- data/lib/ci/queue/redis/retry.rb +2 -11
- data/lib/ci/queue/redis/supervisor.rb +7 -8
- data/lib/ci/queue/redis/worker.rb +5 -13
- data/lib/ci/queue/static.rb +2 -5
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue.rb +7 -6
- data/lib/minitest/queue/build_status_recorder.rb +68 -0
- data/lib/minitest/queue/build_status_reporter.rb +84 -0
- data/lib/minitest/queue/error_report.rb +69 -0
- data/lib/minitest/{reporters → queue}/failure_formatter.rb +2 -1
- data/lib/minitest/{reporters/queue_reporter.rb → queue/local_requeue_reporter.rb} +2 -2
- data/lib/minitest/queue/runner.rb +8 -6
- data/lib/minitest/reporters/redis_reporter.rb +13 -162
- data/lib/rspec/queue.rb +184 -30
- data/lib/rspec/queue/build_status_recorder.rb +38 -0
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14313109ee765675cfb5babf40a14366888cedcd
|
4
|
+
data.tar.gz: 79d6f2168cb41a110d0e029dccc5306b61fbce02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e29790ab693de057e62a2db788d2fade577eb6cb3e7f6bbe5c0081d74327e47a8db66cf7ae7dc64fcbce8fa974641e3eabea58b97a024adafd98479e7bcae3cc
|
7
|
+
data.tar.gz: 51c1f832932265c19ee882c6a1d0861b07b09052ca0719771f8f7f545dc39c0a67db1e8e3f8648e6077561d688d5224d25df14d4f779bd8d853788870f9d1671
|
data/README.md
CHANGED
@@ -51,18 +51,17 @@ minitest-queue --queue path/to/test_order.log --failing-test 'SomeTest#test_some
|
|
51
51
|
|
52
52
|
### RSpec
|
53
53
|
|
54
|
-
|
54
|
+
Assuming you use one of the supported CI providers, the command can be as simple as:
|
55
55
|
|
56
56
|
```bash
|
57
|
-
rspec-queue --queue redis://example.com
|
57
|
+
rspec-queue --queue redis://example.com
|
58
58
|
```
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
To be implemented:
|
60
|
+
If you'd like to centralize the error reporting you can do so with:
|
63
61
|
|
64
|
-
|
65
|
-
|
62
|
+
```bash
|
63
|
+
rspec-queue --queue redis://example.com --timeout 600 --report
|
64
|
+
```
|
66
65
|
|
67
66
|
#### Limitations
|
68
67
|
|
data/lib/ci/queue.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
module CI
|
2
|
+
module Queue
|
3
|
+
class BuildRecord
|
4
|
+
attr_reader :error_reports
|
5
|
+
|
6
|
+
def initialize(queue)
|
7
|
+
@queue = queue
|
8
|
+
@error_reports = {}
|
9
|
+
@stats = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def progress
|
13
|
+
@queue.progress
|
14
|
+
end
|
15
|
+
|
16
|
+
def queue_exhausted?
|
17
|
+
@queue.exhausted?
|
18
|
+
end
|
19
|
+
|
20
|
+
def record_error(id, payload, stats: nil)
|
21
|
+
error_reports[id] = payload
|
22
|
+
record_stats(stats)
|
23
|
+
end
|
24
|
+
|
25
|
+
def record_success(id, stats: nil)
|
26
|
+
error_reports.delete(id)
|
27
|
+
record_stats(stats)
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch_stats(stat_names)
|
31
|
+
stat_names.zip(stats.values_at(*stat_names).map(&:to_f))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :stats
|
37
|
+
|
38
|
+
def record_stats(builds_stats)
|
39
|
+
return unless builds_stats
|
40
|
+
stats.merge!(builds_stats)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/ci/queue/redis.rb
CHANGED
@@ -0,0 +1,63 @@
|
|
1
|
+
module CI
|
2
|
+
module Queue
|
3
|
+
module Redis
|
4
|
+
class BuildRecord
|
5
|
+
def initialize(queue, redis, config)
|
6
|
+
@queue = queue
|
7
|
+
@redis = redis
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def progress
|
12
|
+
@queue.progress
|
13
|
+
end
|
14
|
+
|
15
|
+
def queue_exhausted?
|
16
|
+
@queue.exhausted?
|
17
|
+
end
|
18
|
+
|
19
|
+
def record_error(id, payload, stats: nil)
|
20
|
+
redis.pipelined do
|
21
|
+
redis.hset(key('error-reports'), id, payload)
|
22
|
+
record_stats(stats)
|
23
|
+
end
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def record_success(id, stats: nil)
|
28
|
+
redis.pipelined do
|
29
|
+
redis.hdel(key('error-reports'), id)
|
30
|
+
record_stats(stats)
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def error_reports
|
36
|
+
redis.hgetall(key('error-reports'))
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch_stats(stat_names)
|
40
|
+
counts = redis.pipelined do
|
41
|
+
stat_names.each { |c| redis.hvals(key(c)) }
|
42
|
+
end
|
43
|
+
stat_names.zip(counts.map { |values| values.map(&:to_f).inject(:+).to_f }).to_h
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :config, :redis
|
49
|
+
|
50
|
+
def record_stats(stats)
|
51
|
+
return unless stats
|
52
|
+
stats.each do |stat_name, stat_value|
|
53
|
+
redis.hset(key(stat_name), config.worker_id, stat_value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def key(*args)
|
58
|
+
['build', config.build_id, *args].join(':')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/ci/queue/redis/retry.rb
CHANGED
@@ -7,17 +7,8 @@ module CI
|
|
7
7
|
super(tests, config)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
require 'minitest/reporters/redis_reporter'
|
13
|
-
@minitest_reporters ||= [
|
14
|
-
Minitest::Reporters::QueueReporter.new,
|
15
|
-
Minitest::Reporters::RedisReporter::Worker.new(
|
16
|
-
redis: redis,
|
17
|
-
build_id: config.build_id,
|
18
|
-
worker_id: config.worker_id,
|
19
|
-
)
|
20
|
-
]
|
10
|
+
def build
|
11
|
+
@build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config)
|
21
12
|
end
|
22
13
|
|
23
14
|
private
|
@@ -6,14 +6,13 @@ module CI
|
|
6
6
|
false
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
]
|
9
|
+
def total
|
10
|
+
wait_for_master(timeout: config.timeout)
|
11
|
+
redis.get(key('total')).to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
def build
|
15
|
+
@build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config)
|
17
16
|
end
|
18
17
|
|
19
18
|
def wait_for_workers
|
@@ -63,17 +63,8 @@ module CI
|
|
63
63
|
Supervisor.new(redis_url, config)
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
67
|
-
|
68
|
-
require 'minitest/reporters/redis_reporter'
|
69
|
-
@minitest_reporters ||= [
|
70
|
-
Minitest::Reporters::QueueReporter.new,
|
71
|
-
Minitest::Reporters::RedisReporter::Worker.new(
|
72
|
-
redis: redis,
|
73
|
-
build_id: build_id,
|
74
|
-
worker_id: worker_id,
|
75
|
-
)
|
76
|
-
]
|
66
|
+
def build
|
67
|
+
@build ||= CI::Queue::Redis::BuildRecord.new(self, redis, config)
|
77
68
|
end
|
78
69
|
|
79
70
|
def acknowledge(test)
|
@@ -89,11 +80,12 @@ module CI
|
|
89
80
|
def requeue(test, offset: Redis.requeue_offset)
|
90
81
|
test_key = test.id
|
91
82
|
raise_on_mismatching_test(test_key)
|
83
|
+
global_max_requeues = config.global_max_requeues(total)
|
92
84
|
|
93
|
-
requeued = eval_script(
|
85
|
+
requeued = config.max_requeues > 0 && global_max_requeues > 0 && eval_script(
|
94
86
|
:requeue,
|
95
87
|
keys: [key('processed'), key('requeues-count'), key('queue'), key('running')],
|
96
|
-
argv: [config.max_requeues,
|
88
|
+
argv: [config.max_requeues, global_max_requeues, test_key, offset],
|
97
89
|
) == 1
|
98
90
|
|
99
91
|
@reserved_test = test_key unless requeued
|
data/lib/ci/queue/static.rb
CHANGED
@@ -17,11 +17,8 @@ module CI
|
|
17
17
|
@total = tests.size
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
|
22
|
-
@minitest_reporters ||= [
|
23
|
-
Minitest::Reporters::QueueReporter.new,
|
24
|
-
]
|
20
|
+
def build
|
21
|
+
@build ||= BuildRecord.new(self)
|
25
22
|
end
|
26
23
|
|
27
24
|
def supervisor
|
data/lib/ci/queue/version.rb
CHANGED
data/lib/minitest/queue.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
require 'minitest'
|
2
|
-
|
3
2
|
gem 'minitest-reporters', '~> 1.1'
|
4
3
|
require 'minitest/reporters'
|
5
4
|
|
5
|
+
require 'minitest/queue/failure_formatter'
|
6
|
+
require 'minitest/queue/error_report'
|
7
|
+
require 'minitest/queue/local_requeue_reporter'
|
8
|
+
require 'minitest/queue/build_status_recorder'
|
9
|
+
require 'minitest/queue/build_status_reporter'
|
10
|
+
|
11
|
+
|
6
12
|
module Minitest
|
7
13
|
class Requeue < Skip
|
8
14
|
attr_reader :failure
|
@@ -68,11 +74,6 @@ module Minitest
|
|
68
74
|
|
69
75
|
def queue=(queue)
|
70
76
|
@queue = queue
|
71
|
-
if queue.respond_to?(:minitest_reporters)
|
72
|
-
self.queue_reporters = queue.minitest_reporters
|
73
|
-
else
|
74
|
-
self.queue_reporters = []
|
75
|
-
end
|
76
77
|
end
|
77
78
|
|
78
79
|
def queue_reporters=(reporters)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'minitest/reporters'
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
module Queue
|
5
|
+
class BuildStatusRecorder < Minitest::Reporters::BaseReporter
|
6
|
+
COUNTERS = %w(
|
7
|
+
assertions
|
8
|
+
errors
|
9
|
+
failures
|
10
|
+
skips
|
11
|
+
requeues
|
12
|
+
total_time
|
13
|
+
).freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :failure_formatter
|
17
|
+
end
|
18
|
+
self.failure_formatter = FailureFormatter
|
19
|
+
|
20
|
+
attr_accessor :requeues
|
21
|
+
|
22
|
+
def initialize(build:, **options)
|
23
|
+
super(options)
|
24
|
+
|
25
|
+
@build = build
|
26
|
+
self.failures = 0
|
27
|
+
self.errors = 0
|
28
|
+
self.skips = 0
|
29
|
+
self.requeues = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def report
|
33
|
+
# noop
|
34
|
+
end
|
35
|
+
|
36
|
+
def record(test)
|
37
|
+
super
|
38
|
+
|
39
|
+
self.total_time = Minitest.clock_time - start_time
|
40
|
+
if test.requeued?
|
41
|
+
self.requeues += 1
|
42
|
+
elsif test.skipped?
|
43
|
+
self.skips += 1
|
44
|
+
elsif test.error?
|
45
|
+
self.errors += 1
|
46
|
+
elsif test.failure
|
47
|
+
self.failures += 1
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
stats = COUNTERS.zip(COUNTERS.map { |c| send(c) })
|
52
|
+
if (test.failure || test.error?) && !test.skipped?
|
53
|
+
build.record_error("#{test.klass}##{test.name}", dump(test), stats: stats)
|
54
|
+
else
|
55
|
+
build.record_success("#{test.klass}##{test.name}", stats: stats)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def dump(test)
|
62
|
+
ErrorReport.new(self.class.failure_formatter.new(test).to_h).dump
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :build
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Minitest
|
2
|
+
module Queue
|
3
|
+
class BuildStatusReporter < Minitest::Reporters::BaseReporter
|
4
|
+
include ::CI::Queue::OutputHelpers
|
5
|
+
|
6
|
+
def initialize(build:, **options)
|
7
|
+
@build = build
|
8
|
+
super(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def completed?
|
12
|
+
build.queue_exhausted?
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_reports
|
16
|
+
build.error_reports.sort_by(&:first).map { |k, v| ErrorReport.load(v) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def report
|
20
|
+
puts aggregates
|
21
|
+
errors = error_reports
|
22
|
+
puts errors
|
23
|
+
|
24
|
+
errors.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def success?
|
28
|
+
errors == 0 && failures == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def record(*)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def failures
|
36
|
+
fetch_summary['failures'].to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def errors
|
40
|
+
fetch_summary['errors'].to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def assertions
|
44
|
+
fetch_summary['assertions'].to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def skips
|
48
|
+
fetch_summary['skips'].to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def requeues
|
52
|
+
fetch_summary['requeues'].to_i
|
53
|
+
end
|
54
|
+
|
55
|
+
def total_time
|
56
|
+
fetch_summary['total_time'].to_f
|
57
|
+
end
|
58
|
+
|
59
|
+
def progress
|
60
|
+
build.progress
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :build
|
66
|
+
|
67
|
+
def aggregates
|
68
|
+
success = failures.zero? && errors.zero?
|
69
|
+
failures_count = "#{failures} failures, #{errors} errors,"
|
70
|
+
|
71
|
+
step([
|
72
|
+
'Ran %d tests, %d assertions,' % [progress, assertions],
|
73
|
+
success ? green(failures_count) : red(failures_count),
|
74
|
+
yellow("#{skips} skips, #{requeues} requeues"),
|
75
|
+
'in %.2fs (aggregated)' % total_time,
|
76
|
+
].join(' '), collapsed: success)
|
77
|
+
end
|
78
|
+
|
79
|
+
def fetch_summary
|
80
|
+
@summary ||= build.fetch_stats(BuildStatusRecorder::COUNTERS)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Minitest
|
2
|
+
module Queue
|
3
|
+
class ErrorReport
|
4
|
+
class << self
|
5
|
+
attr_accessor :coder
|
6
|
+
|
7
|
+
def load(payload)
|
8
|
+
new(coder.load(payload))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
self.coder = Marshal
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'snappy'
|
16
|
+
require 'msgpack'
|
17
|
+
require 'stringio'
|
18
|
+
|
19
|
+
module SnappyPack
|
20
|
+
extend self
|
21
|
+
|
22
|
+
MSGPACK = MessagePack::Factory.new
|
23
|
+
MSGPACK.register_type(0x00, Symbol)
|
24
|
+
|
25
|
+
def load(payload)
|
26
|
+
io = StringIO.new(Snappy.inflate(payload))
|
27
|
+
MSGPACK.unpacker(io).unpack
|
28
|
+
end
|
29
|
+
|
30
|
+
def dump(object)
|
31
|
+
io = StringIO.new
|
32
|
+
packer = MSGPACK.packer(io)
|
33
|
+
packer.pack(object)
|
34
|
+
packer.flush
|
35
|
+
io.rewind
|
36
|
+
Snappy.deflate(io.string).force_encoding(Encoding::UTF_8)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
self.coder = SnappyPack
|
41
|
+
rescue LoadError
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(data)
|
45
|
+
@data = data
|
46
|
+
end
|
47
|
+
|
48
|
+
def dump
|
49
|
+
self.class.coder.dump(@data)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_name
|
53
|
+
@data[:test_name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_and_module_name
|
57
|
+
@data[:test_and_module_name]
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
output
|
62
|
+
end
|
63
|
+
|
64
|
+
def output
|
65
|
+
@data[:output]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -2,8 +2,8 @@ require 'ci/queue/output_helpers'
|
|
2
2
|
require 'minitest/reporters'
|
3
3
|
|
4
4
|
module Minitest
|
5
|
-
module
|
6
|
-
class
|
5
|
+
module Queue
|
6
|
+
class LocalRequeueReporter < Minitest::Reporters::BaseReporter
|
7
7
|
include ::CI::Queue::OutputHelpers
|
8
8
|
attr_accessor :requeues
|
9
9
|
|
@@ -43,6 +43,11 @@ module Minitest
|
|
43
43
|
def run_command
|
44
44
|
set_load_path
|
45
45
|
Minitest.queue = queue
|
46
|
+
Minitest.queue_reporters = [
|
47
|
+
Minitest::Queue::LocalRequeueReporter.new,
|
48
|
+
Minitest::Queue::BuildStatusRecorder.new(build: queue.build)
|
49
|
+
]
|
50
|
+
|
46
51
|
trap('TERM') { Minitest.queue.shutdown! }
|
47
52
|
trap('INT') { Minitest.queue.shutdown! }
|
48
53
|
load_tests
|
@@ -115,12 +120,9 @@ module Minitest
|
|
115
120
|
end
|
116
121
|
end
|
117
122
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
exit! success ? 0 : 1
|
123
|
+
reporter = BuildStatusReporter.new(build: supervisor.build)
|
124
|
+
reporter.report
|
125
|
+
exit! reporter.success? ? 0 : 1
|
124
126
|
end
|
125
127
|
|
126
128
|
private
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'minitest/reporters'
|
2
|
-
require 'minitest/reporters/failure_formatter'
|
3
2
|
|
4
3
|
module Minitest
|
5
4
|
module Reporters
|
@@ -20,177 +19,32 @@ module Minitest
|
|
20
19
|
end
|
21
20
|
self.failure_formatter = FailureFormatter
|
22
21
|
|
23
|
-
class Error
|
24
|
-
class << self
|
25
|
-
attr_accessor :coder
|
26
|
-
|
27
|
-
def load(payload)
|
28
|
-
new(coder.load(payload))
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
self.coder = Marshal
|
33
|
-
|
34
|
-
begin
|
35
|
-
require 'snappy'
|
36
|
-
require 'msgpack'
|
37
|
-
require 'stringio'
|
38
|
-
|
39
|
-
module SnappyPack
|
40
|
-
extend self
|
41
|
-
|
42
|
-
MSGPACK = MessagePack::Factory.new
|
43
|
-
MSGPACK.register_type(0x00, Symbol)
|
44
|
-
|
45
|
-
def load(payload)
|
46
|
-
io = StringIO.new(Snappy.inflate(payload))
|
47
|
-
MSGPACK.unpacker(io).unpack
|
48
|
-
end
|
49
|
-
|
50
|
-
def dump(object)
|
51
|
-
io = StringIO.new
|
52
|
-
packer = MSGPACK.packer(io)
|
53
|
-
packer.pack(object)
|
54
|
-
packer.flush
|
55
|
-
io.rewind
|
56
|
-
Snappy.deflate(io.string).force_encoding(Encoding::UTF_8)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
self.coder = SnappyPack
|
61
|
-
rescue LoadError
|
62
|
-
end
|
63
|
-
|
64
|
-
def initialize(data)
|
65
|
-
@data = data
|
66
|
-
end
|
67
|
-
|
68
|
-
def dump
|
69
|
-
self.class.coder.dump(@data)
|
70
|
-
end
|
71
|
-
|
72
|
-
def test_name
|
73
|
-
@data[:test_name]
|
74
|
-
end
|
75
|
-
|
76
|
-
def test_and_module_name
|
77
|
-
@data[:test_and_module_name]
|
78
|
-
end
|
79
|
-
|
80
|
-
def to_s
|
81
|
-
output
|
82
|
-
end
|
83
|
-
|
84
|
-
def output
|
85
|
-
@data[:output]
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
22
|
class Base < BaseReporter
|
90
|
-
def initialize(
|
91
|
-
@
|
92
|
-
@key = "build:#{build_id}"
|
23
|
+
def initialize(build:, **options)
|
24
|
+
@build = build
|
93
25
|
super(options)
|
94
26
|
end
|
95
27
|
|
96
|
-
def total
|
97
|
-
redis.get(key('total')).to_i
|
98
|
-
end
|
99
|
-
|
100
|
-
def processed
|
101
|
-
redis.scard(key('processed')).to_i
|
102
|
-
end
|
103
|
-
|
104
28
|
def completed?
|
105
|
-
|
29
|
+
build.queue_exhausted?
|
106
30
|
end
|
107
31
|
|
108
32
|
def error_reports
|
109
|
-
|
33
|
+
build.error_reports.sort_by(&:first).map { |k, v| ErrorReport.load(v) }
|
110
34
|
end
|
111
35
|
|
112
36
|
private
|
113
37
|
|
114
|
-
attr_reader :redis
|
115
|
-
|
116
|
-
def key(*args)
|
117
|
-
[@key, *args].join(':')
|
118
|
-
end
|
119
38
|
end
|
120
39
|
|
121
40
|
class Summary < Base
|
122
|
-
include ::CI::Queue::OutputHelpers
|
123
|
-
|
124
|
-
def report
|
125
|
-
puts aggregates
|
126
|
-
errors = error_reports
|
127
|
-
puts errors
|
128
|
-
|
129
|
-
errors.empty?
|
130
|
-
end
|
131
|
-
|
132
|
-
def success?
|
133
|
-
errors == 0 && failures == 0
|
134
|
-
end
|
135
|
-
|
136
|
-
def record(*)
|
137
|
-
raise NotImplementedError
|
138
|
-
end
|
139
|
-
|
140
|
-
def failures
|
141
|
-
fetch_summary['failures'].to_i
|
142
|
-
end
|
143
|
-
|
144
|
-
def errors
|
145
|
-
fetch_summary['errors'].to_i
|
146
|
-
end
|
147
|
-
|
148
|
-
def assertions
|
149
|
-
fetch_summary['assertions'].to_i
|
150
|
-
end
|
151
|
-
|
152
|
-
def skips
|
153
|
-
fetch_summary['skips'].to_i
|
154
|
-
end
|
155
|
-
|
156
|
-
def requeues
|
157
|
-
fetch_summary['requeues'].to_i
|
158
|
-
end
|
159
|
-
|
160
|
-
def total_time
|
161
|
-
fetch_summary['total_time'].to_f
|
162
|
-
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
def aggregates
|
167
|
-
success = failures.zero? && errors.zero?
|
168
|
-
failures_count = "#{failures} failures, #{errors} errors,"
|
169
|
-
|
170
|
-
step([
|
171
|
-
'Ran %d tests, %d assertions,' % [processed, assertions],
|
172
|
-
success ? green(failures_count) : red(failures_count),
|
173
|
-
yellow("#{skips} skips, #{requeues} requeues"),
|
174
|
-
'in %.2fs (aggregated)' % total_time,
|
175
|
-
].join(' '), collapsed: success)
|
176
|
-
end
|
177
|
-
|
178
|
-
def fetch_summary
|
179
|
-
@summary ||= begin
|
180
|
-
counts = redis.pipelined do
|
181
|
-
COUNTERS.each { |c| redis.hgetall(key(c)) }
|
182
|
-
end
|
183
|
-
COUNTERS.zip(counts.map { |h| h.values.map(&:to_f).inject(:+).to_f }).to_h
|
184
|
-
end
|
185
|
-
end
|
186
41
|
end
|
187
42
|
|
188
43
|
class Worker < Base
|
189
44
|
attr_accessor :requeues
|
190
45
|
|
191
|
-
def initialize(
|
46
|
+
def initialize(*)
|
192
47
|
super
|
193
|
-
@worker_id = worker_id
|
194
48
|
self.failures = 0
|
195
49
|
self.errors = 0
|
196
50
|
self.skips = 0
|
@@ -215,25 +69,22 @@ module Minitest
|
|
215
69
|
self.failures += 1
|
216
70
|
end
|
217
71
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
COUNTERS.each do |counter|
|
225
|
-
redis.hset(key(counter), worker_id, send(counter))
|
226
|
-
end
|
72
|
+
|
73
|
+
stats = COUNTERS.zip(COUNTERS.map { |c| send(c) })
|
74
|
+
if (test.failure || test.error?) && !test.skipped?
|
75
|
+
build.record_error("#{test.klass}##{test.name}", dump(test), stats: stats)
|
76
|
+
else
|
77
|
+
build.record_success("#{test.klass}##{test.name}", stats: stats)
|
227
78
|
end
|
228
79
|
end
|
229
80
|
|
230
81
|
private
|
231
82
|
|
232
83
|
def dump(test)
|
233
|
-
|
84
|
+
ErrorReport.new(RedisReporter.failure_formatter.new(test).to_h).dump
|
234
85
|
end
|
235
86
|
|
236
|
-
attr_reader :
|
87
|
+
attr_reader :aggregates
|
237
88
|
end
|
238
89
|
end
|
239
90
|
end
|
data/lib/rspec/queue.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
require 'delegate'
|
1
2
|
require 'rspec/core'
|
2
3
|
require 'ci/queue'
|
4
|
+
require 'rspec/queue/build_status_recorder'
|
3
5
|
|
4
6
|
module RSpec
|
5
7
|
module Queue
|
@@ -9,6 +11,34 @@ module RSpec
|
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
14
|
+
module RunnerHelpers
|
15
|
+
private
|
16
|
+
|
17
|
+
def queue_url
|
18
|
+
configuration.queue_url || ENV['CI_QUEUE_URL']
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalid_usage!(message)
|
22
|
+
reopen_previous_step
|
23
|
+
puts red(message)
|
24
|
+
puts
|
25
|
+
puts 'Please use --help for a listing of valid options'
|
26
|
+
exit! 1 # exit! is required to avoid at_exit callback
|
27
|
+
end
|
28
|
+
|
29
|
+
def exit!(*)
|
30
|
+
STDOUT.flush
|
31
|
+
STDERR.flush
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def abort!(message)
|
36
|
+
reopen_previous_step
|
37
|
+
puts red(message)
|
38
|
+
exit! 1 # exit! is required to avoid at_exit callback
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
12
42
|
module ConfigurationExtension
|
13
43
|
private
|
14
44
|
|
@@ -43,6 +73,11 @@ module RSpec
|
|
43
73
|
options[:queue_url] = url
|
44
74
|
end
|
45
75
|
|
76
|
+
parser.on('--report', *help) do |url|
|
77
|
+
options[:report] = true
|
78
|
+
options[:runner] = RSpec::Queue::ReportRunner.new
|
79
|
+
end
|
80
|
+
|
46
81
|
help = split_heredoc(<<-EOS)
|
47
82
|
Unique identifier for the workload. All workers working on the same suite of tests must have the same build identifier.
|
48
83
|
If the build is tried again, or another revision is built, this value must be different.
|
@@ -114,6 +149,69 @@ module RSpec
|
|
114
149
|
|
115
150
|
RSpec::Core::Parser.prepend(ParserExtension)
|
116
151
|
|
152
|
+
module ExampleExtension
|
153
|
+
protected
|
154
|
+
|
155
|
+
def mark_as_requeued!(reporter)
|
156
|
+
@metadata = @metadata.dup # Avoid mutating the @metadata hash of the original Example instance
|
157
|
+
@metadata[:execution_result] = execution_result.dup
|
158
|
+
|
159
|
+
|
160
|
+
failure_notification = RSpec::Core::Notifications::FailedExampleNotification.new(self)
|
161
|
+
execution_result.exception = @exception
|
162
|
+
execution_result.status = :failed
|
163
|
+
|
164
|
+
presenter = RSpec::Core::Formatters::ExceptionPresenter::Factory.new(self).build
|
165
|
+
error_message = presenter.fully_formatted_lines(nil, ::RSpec::Core::Formatters::ConsoleCodes)
|
166
|
+
error_message.delete_at(1) # remove the example description
|
167
|
+
|
168
|
+
@exception = nil
|
169
|
+
execution_result.exception = nil
|
170
|
+
execution_result.status = :pending
|
171
|
+
execution_result.pending_message = [
|
172
|
+
"The example failed, but another attempt will be done to rule out flakiness",
|
173
|
+
*error_message.map { |l| l.empty? ? l : " " + l },
|
174
|
+
].join("\n")
|
175
|
+
|
176
|
+
# Ensure the example is recorded as ran, so it's visible to formatters
|
177
|
+
reporter.example_started(self)
|
178
|
+
finish(reporter, acknowledge: false)
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def start(*)
|
184
|
+
reset! # In case that example was already ran but got requeued
|
185
|
+
super
|
186
|
+
end
|
187
|
+
|
188
|
+
def finish(reporter, acknowledge: true)
|
189
|
+
if acknowledge && reporter.respond_to?(:requeue)
|
190
|
+
if @exception && reporter.requeue
|
191
|
+
reporter.cancel_run!
|
192
|
+
dup.mark_as_requeued!(reporter)
|
193
|
+
return
|
194
|
+
elsif reporter.acknowledge || !@exception
|
195
|
+
# If the test was already acknowledged by another worker (we timed out)
|
196
|
+
# Then we only record it if it is successful.
|
197
|
+
super(reporter)
|
198
|
+
else
|
199
|
+
reporter.cancel_run!
|
200
|
+
return
|
201
|
+
end
|
202
|
+
else
|
203
|
+
super(reporter)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def reset!
|
208
|
+
@exception = nil
|
209
|
+
@metadata[:execution_result] = RSpec::Core::Example::ExecutionResult.new
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
RSpec::Core::Example.prepend(ExampleExtension)
|
214
|
+
|
117
215
|
class SingleExample
|
118
216
|
attr_reader :example_group, :example
|
119
217
|
|
@@ -134,16 +232,94 @@ module RSpec
|
|
134
232
|
return if RSpec.world.wants_to_quit
|
135
233
|
instance = example_group.new(example.inspect_output)
|
136
234
|
example_group.set_ivars(instance, example_group.before_context_ivars)
|
137
|
-
|
138
|
-
|
139
|
-
|
235
|
+
example.run(instance, reporter)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class ReportRunner
|
240
|
+
include RunnerHelpers
|
241
|
+
include CI::Queue::OutputHelpers
|
242
|
+
|
243
|
+
def call(options, stdout, stderr)
|
244
|
+
setup(options, stdout, stderr)
|
245
|
+
|
246
|
+
queue = CI::Queue.from_uri(queue_url, RSpec::Queue.config)
|
247
|
+
|
248
|
+
supervisor = begin
|
249
|
+
queue.supervisor
|
250
|
+
rescue NotImplementedError => error
|
251
|
+
abort! error.message
|
252
|
+
end
|
253
|
+
|
254
|
+
step("Waiting for workers to complete")
|
255
|
+
|
256
|
+
unless supervisor.wait_for_workers
|
257
|
+
unless supervisor.queue_initialized?
|
258
|
+
abort! "No master was elected. Did all workers crash?"
|
259
|
+
end
|
260
|
+
|
261
|
+
unless supervisor.exhausted?
|
262
|
+
abort! "#{supervisor.size} tests weren't run."
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# TODO: better reporting
|
267
|
+
errors = supervisor.build.error_reports.sort_by(&:first).map(&:last)
|
268
|
+
if errors.empty?
|
269
|
+
step(green('No errors found'))
|
270
|
+
0
|
271
|
+
else
|
272
|
+
message = errors.size == 1 ? "1 error found" : "#{errors.size} errors found"
|
273
|
+
step(red(message), collapsed: false)
|
274
|
+
puts errors
|
275
|
+
1
|
140
276
|
end
|
141
|
-
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
attr_reader :configuration
|
282
|
+
|
283
|
+
def setup(options, out, err)
|
284
|
+
@options = options
|
285
|
+
@configuration = RSpec.configuration
|
286
|
+
@world = RSpec.world
|
287
|
+
@configuration.error_stream = err
|
288
|
+
@configuration.output_stream = out if @configuration.output_stream == $stdout
|
289
|
+
@options.options.delete(:requires) # Prevent loading of spec_helper so the app doesn't need to boot
|
290
|
+
@options.configure(@configuration)
|
291
|
+
|
292
|
+
invalid_usage!('Missing --queue parameter') unless queue_url
|
293
|
+
invalid_usage!('Missing --build parameter') unless RSpec::Queue.config.build_id
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class QueueReporter < SimpleDelegator
|
298
|
+
def initialize(reporter, queue, example)
|
299
|
+
@queue = queue
|
300
|
+
@example = example
|
301
|
+
super(reporter)
|
302
|
+
end
|
303
|
+
|
304
|
+
def requeue
|
305
|
+
@queue.requeue(@example)
|
306
|
+
end
|
307
|
+
|
308
|
+
def cancel_run!
|
309
|
+
# Remove the requeued example from the list of examples ran
|
310
|
+
# Otherwise some formatters might break because the example state is reset
|
311
|
+
examples.pop
|
312
|
+
nil
|
313
|
+
end
|
314
|
+
|
315
|
+
def acknowledge
|
316
|
+
@queue.acknowledge(@example)
|
142
317
|
end
|
143
318
|
end
|
144
319
|
|
145
320
|
class Runner < ::RSpec::Core::Runner
|
146
321
|
include CI::Queue::OutputHelpers
|
322
|
+
include RunnerHelpers
|
147
323
|
|
148
324
|
def setup(err, out)
|
149
325
|
super
|
@@ -160,14 +336,16 @@ module RSpec
|
|
160
336
|
end
|
161
337
|
|
162
338
|
queue = CI::Queue.from_uri(queue_url, RSpec::Queue.config)
|
339
|
+
BuildStatusRecorder.build = queue.build
|
163
340
|
queue.populate(examples, random: ordering_seed, &:id)
|
164
341
|
examples_count = examples.size # TODO: figure out which stub value would be best
|
165
342
|
success = true
|
166
343
|
@configuration.reporter.report(examples_count) do |reporter|
|
344
|
+
@configuration.add_formatter(BuildStatusRecorder)
|
345
|
+
|
167
346
|
@configuration.with_suite_hooks do
|
168
347
|
queue.poll do |example|
|
169
|
-
success &= example.run(reporter)
|
170
|
-
queue.acknowledge(example)
|
348
|
+
success &= example.run(QueueReporter.new(reporter, queue, example))
|
171
349
|
end
|
172
350
|
end
|
173
351
|
end
|
@@ -185,30 +363,6 @@ module RSpec
|
|
185
363
|
Random.new
|
186
364
|
end
|
187
365
|
end
|
188
|
-
|
189
|
-
def queue_url
|
190
|
-
configuration.queue_url || ENV['CI_QUEUE_URL']
|
191
|
-
end
|
192
|
-
|
193
|
-
def invalid_usage!(message)
|
194
|
-
reopen_previous_step
|
195
|
-
puts red(message)
|
196
|
-
puts
|
197
|
-
puts 'Please use --help for a listing of valid options'
|
198
|
-
exit! 1 # exit! is required to avoid minitest at_exit callback
|
199
|
-
end
|
200
|
-
|
201
|
-
def exit!(*)
|
202
|
-
STDOUT.flush
|
203
|
-
STDERR.flush
|
204
|
-
super
|
205
|
-
end
|
206
|
-
|
207
|
-
def abort!(message)
|
208
|
-
reopen_previous_step
|
209
|
-
puts red(message)
|
210
|
-
exit! 1 # exit! is required to avoid minitest at_exit callback
|
211
|
-
end
|
212
366
|
end
|
213
367
|
end
|
214
368
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Queue
|
3
|
+
class BuildStatusRecorder
|
4
|
+
::RSpec::Core::Formatters.register self, :example_passed, :example_failed
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_accessor :build
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(*)
|
11
|
+
end
|
12
|
+
|
13
|
+
def example_passed(notification)
|
14
|
+
example = notification.example
|
15
|
+
build.record_success(example.id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def example_failed(notification)
|
19
|
+
example = notification.example
|
20
|
+
build.record_error(example.id, [
|
21
|
+
notification.fully_formatted(nil),
|
22
|
+
colorized_rerun_command(example),
|
23
|
+
].join("\n"))
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def colorized_rerun_command(example, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
|
29
|
+
colorizer.wrap("rspec #{example.location_rerun_argument}", RSpec.configuration.failure_color) + " " +
|
30
|
+
colorizer.wrap("# #{example.full_description}", RSpec.configuration.detail_color)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build
|
34
|
+
self.class.build
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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.11.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: 2018-
|
11
|
+
date: 2018-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ansi
|
@@ -173,12 +173,14 @@ files:
|
|
173
173
|
- exe/rspec-queue
|
174
174
|
- lib/ci/queue.rb
|
175
175
|
- lib/ci/queue/bisect.rb
|
176
|
+
- lib/ci/queue/build_record.rb
|
176
177
|
- lib/ci/queue/configuration.rb
|
177
178
|
- lib/ci/queue/file.rb
|
178
179
|
- lib/ci/queue/output_helpers.rb
|
179
180
|
- lib/ci/queue/redis.rb
|
180
181
|
- lib/ci/queue/redis/acknowledge.lua
|
181
182
|
- lib/ci/queue/redis/base.rb
|
183
|
+
- lib/ci/queue/redis/build_record.rb
|
182
184
|
- lib/ci/queue/redis/requeue.lua
|
183
185
|
- lib/ci/queue/redis/reserve.lua
|
184
186
|
- lib/ci/queue/redis/reserve_lost.lua
|
@@ -188,13 +190,17 @@ files:
|
|
188
190
|
- lib/ci/queue/static.rb
|
189
191
|
- lib/ci/queue/version.rb
|
190
192
|
- lib/minitest/queue.rb
|
193
|
+
- lib/minitest/queue/build_status_recorder.rb
|
194
|
+
- lib/minitest/queue/build_status_reporter.rb
|
195
|
+
- lib/minitest/queue/error_report.rb
|
196
|
+
- lib/minitest/queue/failure_formatter.rb
|
197
|
+
- lib/minitest/queue/local_requeue_reporter.rb
|
191
198
|
- lib/minitest/queue/runner.rb
|
192
199
|
- lib/minitest/reporters/bisect_reporter.rb
|
193
|
-
- lib/minitest/reporters/failure_formatter.rb
|
194
200
|
- lib/minitest/reporters/order_reporter.rb
|
195
|
-
- lib/minitest/reporters/queue_reporter.rb
|
196
201
|
- lib/minitest/reporters/redis_reporter.rb
|
197
202
|
- lib/rspec/queue.rb
|
203
|
+
- lib/rspec/queue/build_status_recorder.rb
|
198
204
|
homepage: https://github.com/Shopify/ci-queue
|
199
205
|
licenses:
|
200
206
|
- MIT
|