ci-queue 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ccbb5a14a7d65abe7ff69cac03dd08789351090
4
- data.tar.gz: 1bcbaf620cc579174d1b4bcb5c53b9e8c49ba5f2
3
+ metadata.gz: 14313109ee765675cfb5babf40a14366888cedcd
4
+ data.tar.gz: 79d6f2168cb41a110d0e029dccc5306b61fbce02
5
5
  SHA512:
6
- metadata.gz: 48b6fbbb41d76f883a84c9e989e01fae5810ea4a06b4685d79fe006362b2db391d4d7b3ebc0d8506cbe7eb56cb0642da37dc31cb1c25d1dbf61252c74a05724c
7
- data.tar.gz: 63d66696121227cdc1767ac13f8afbe1d437e2f13c7991026f0a3a7189589503de9203ba1f40122b84b20836f21aad336a721d27e1c05fc166c11449be68ea2b
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
- The RSpec integration is still missing some features, but is already usable:
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 --build XXX --worker XXX
57
+ rspec-queue --queue redis://example.com
58
58
  ```
59
59
 
60
- #### Missing features
61
-
62
- To be implemented:
60
+ If you'd like to centralize the error reporting you can do so with:
63
61
 
64
- - Requeueing
65
- - Centralized reporting
62
+ ```bash
63
+ rspec-queue --queue redis://example.com --timeout 600 --report
64
+ ```
66
65
 
67
66
  #### Limitations
68
67
 
@@ -4,6 +4,7 @@ require 'cgi'
4
4
  require 'ci/queue/version'
5
5
  require 'ci/queue/output_helpers'
6
6
  require 'ci/queue/configuration'
7
+ require 'ci/queue/build_record'
7
8
  require 'ci/queue/static'
8
9
  require 'ci/queue/file'
9
10
  require 'ci/queue/bisect'
@@ -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
@@ -1,4 +1,5 @@
1
1
  require 'redis'
2
+ require 'ci/queue/redis/build_record'
2
3
  require 'ci/queue/redis/base'
3
4
  require 'ci/queue/redis/worker'
4
5
  require 'ci/queue/redis/retry'
@@ -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
@@ -7,17 +7,8 @@ module CI
7
7
  super(tests, config)
8
8
  end
9
9
 
10
- def minitest_reporters
11
- require 'minitest/reporters/queue_reporter'
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 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
- ]
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 minitest_reporters
67
- require 'minitest/reporters/queue_reporter'
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, config.global_max_requeues(total), test_key, offset],
88
+ argv: [config.max_requeues, global_max_requeues, test_key, offset],
97
89
  ) == 1
98
90
 
99
91
  @reserved_test = test_key unless requeued
@@ -17,11 +17,8 @@ module CI
17
17
  @total = tests.size
18
18
  end
19
19
 
20
- def minitest_reporters
21
- require 'minitest/reporters/queue_reporter'
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
@@ -1,6 +1,6 @@
1
1
  module CI
2
2
  module Queue
3
- VERSION = '0.10.1'
3
+ VERSION = '0.11.0'
4
4
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
5
5
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
6
6
  end
@@ -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
@@ -1,7 +1,8 @@
1
1
  require 'delegate'
2
+ require 'ansi'
2
3
 
3
4
  module Minitest
4
- module Reporters
5
+ module Queue
5
6
  class FailureFormatter < SimpleDelegator
6
7
  include ANSI::Code
7
8
 
@@ -2,8 +2,8 @@ require 'ci/queue/output_helpers'
2
2
  require 'minitest/reporters'
3
3
 
4
4
  module Minitest
5
- module Reporters
6
- class QueueReporter < BaseReporter
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
- success = supervisor.minitest_reporters.all?(&:success?)
119
- supervisor.minitest_reporters.each do |reporter|
120
- reporter.report
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(redis:, build_id:, **options)
91
- @redis = redis
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
- total > 0 && total == processed
29
+ build.queue_exhausted?
106
30
  end
107
31
 
108
32
  def error_reports
109
- redis.hgetall(key('error-reports')).sort_by(&:first).map { |k, v| Error.load(v) }
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(worker_id:, **options)
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
- redis.multi do
219
- if (test.failure || test.error?) && !test.skipped?
220
- redis.hset(key('error-reports'), "#{test.klass}##{test.name}", dump(test))
221
- else
222
- redis.hdel(key('error-reports'), "#{test.klass}##{test.name}")
223
- end
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
- Error.new(RedisReporter.failure_formatter.new(test).to_h).dump
84
+ ErrorReport.new(RedisReporter.failure_formatter.new(test).to_h).dump
234
85
  end
235
86
 
236
- attr_reader :worker_id, :aggregates
87
+ attr_reader :aggregates
237
88
  end
238
89
  end
239
90
  end
@@ -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
- succeeded = example.run(instance, reporter)
138
- if !succeeded && reporter.fail_fast_limit_met?
139
- RSpec.world.wants_to_quit = true
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
- succeeded
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.10.1
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-01-10 00:00:00.000000000 Z
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