ci-queue 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/Rakefile +1 -0
- data/bin/console +1 -0
- data/ci-queue.gemspec +2 -0
- data/exe/minitest-queue +1 -0
- data/exe/rspec-queue +1 -0
- data/lib/ci/queue.rb +3 -0
- data/lib/ci/queue/bisect.rb +1 -0
- data/lib/ci/queue/build_record.rb +1 -0
- data/lib/ci/queue/circuit_breaker.rb +39 -0
- data/lib/ci/queue/common.rb +3 -2
- data/lib/ci/queue/configuration.rb +25 -11
- data/lib/ci/queue/file.rb +1 -0
- data/lib/ci/queue/grind.rb +23 -0
- data/lib/ci/queue/output_helpers.rb +1 -0
- data/lib/ci/queue/redis.rb +6 -0
- data/lib/ci/queue/redis/base.rb +1 -0
- data/lib/ci/queue/redis/build_record.rb +4 -3
- data/lib/ci/queue/redis/grind.rb +17 -0
- data/lib/ci/queue/redis/grind_record.rb +66 -0
- data/lib/ci/queue/redis/grind_supervisor.rb +13 -0
- data/lib/ci/queue/redis/retry.rb +1 -0
- data/lib/ci/queue/redis/supervisor.rb +2 -1
- data/lib/ci/queue/redis/test_time_record.rb +66 -0
- data/lib/ci/queue/redis/worker.rb +2 -1
- data/lib/ci/queue/static.rb +3 -1
- data/lib/ci/queue/version.rb +3 -1
- data/lib/minitest/queue.rb +10 -4
- data/lib/minitest/queue/build_status_recorder.rb +1 -0
- data/lib/minitest/queue/build_status_reporter.rb +1 -0
- data/lib/minitest/queue/error_report.rb +13 -0
- data/lib/minitest/queue/failure_formatter.rb +4 -0
- data/lib/minitest/queue/grind_recorder.rb +68 -0
- data/lib/minitest/queue/grind_reporter.rb +74 -0
- data/lib/minitest/queue/junit_reporter.rb +6 -5
- data/lib/minitest/queue/local_requeue_reporter.rb +1 -0
- data/lib/minitest/queue/order_reporter.rb +1 -0
- data/lib/minitest/queue/runner.rb +156 -32
- data/lib/minitest/queue/statsd.rb +1 -0
- data/lib/minitest/queue/test_data.rb +138 -0
- data/lib/minitest/queue/test_data_reporter.rb +32 -0
- data/lib/minitest/queue/test_time_recorder.rb +17 -0
- data/lib/minitest/queue/test_time_reporter.rb +70 -0
- data/lib/minitest/reporters/bisect_reporter.rb +1 -0
- data/lib/minitest/reporters/statsd_reporter.rb +1 -0
- data/lib/rspec/queue.rb +15 -14
- data/lib/rspec/queue/build_status_recorder.rb +1 -0
- data/lib/rspec/queue/order_recorder.rb +20 -0
- metadata +29 -4
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'minitest/reporters'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Minitest
|
6
|
+
module Queue
|
7
|
+
class TestData
|
8
|
+
attr_reader :namespace, :test_index
|
9
|
+
|
10
|
+
def initialize(test:, index:, namespace:, base_path:)
|
11
|
+
@test = test
|
12
|
+
@base_path = base_path
|
13
|
+
@namespace = namespace
|
14
|
+
@test_index = index
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_id
|
18
|
+
"#{test_suite}##{test_name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_name
|
22
|
+
@test.name
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_suite
|
26
|
+
@test.klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_retried
|
30
|
+
@test.requeued?
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_result
|
34
|
+
if @test.passed?
|
35
|
+
'success'
|
36
|
+
elsif !@test.requeued? && @test.skipped?
|
37
|
+
'skipped'
|
38
|
+
elsif @test.error?
|
39
|
+
'error'
|
40
|
+
elsif @test.failure
|
41
|
+
'failure'
|
42
|
+
else
|
43
|
+
'undefined'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_assertions
|
48
|
+
@test.assertions
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_duration
|
52
|
+
@test.time
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_file_path
|
56
|
+
path = @test.source_location.first
|
57
|
+
relative_path_for(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_file_line_number
|
61
|
+
@test.source_location.last
|
62
|
+
end
|
63
|
+
|
64
|
+
# Error class only considers failures wheras the other error fields also consider skips
|
65
|
+
def error_class
|
66
|
+
return nil unless @test.failure
|
67
|
+
|
68
|
+
@test.failure.error.class.name
|
69
|
+
end
|
70
|
+
|
71
|
+
def error_message
|
72
|
+
return nil unless @test.failure
|
73
|
+
|
74
|
+
@test.failure.message
|
75
|
+
end
|
76
|
+
|
77
|
+
def error_file_path
|
78
|
+
return nil unless @test.failure
|
79
|
+
|
80
|
+
path = error_location(@test.failure).first
|
81
|
+
relative_path_for(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
def error_file_number
|
85
|
+
return nil unless @test.failure
|
86
|
+
|
87
|
+
error_location(@test.failure).last
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_h
|
91
|
+
{
|
92
|
+
namespace: namespace,
|
93
|
+
test_id: test_id,
|
94
|
+
test_name: test_name,
|
95
|
+
test_suite: test_suite,
|
96
|
+
test_result: test_result,
|
97
|
+
test_index: test_index,
|
98
|
+
test_result_ignored: @test.flaked?,
|
99
|
+
test_retried: test_retried,
|
100
|
+
test_assertions: test_assertions,
|
101
|
+
test_duration: test_duration,
|
102
|
+
test_file_path: test_file_path,
|
103
|
+
test_file_line_number: test_file_line_number,
|
104
|
+
error_class: error_class,
|
105
|
+
error_message: error_message,
|
106
|
+
error_file_path: error_file_path,
|
107
|
+
error_file_number: error_file_number,
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def relative_path_for(path)
|
114
|
+
file_path = Pathname.new(path)
|
115
|
+
base_path = Pathname.new(@base_path)
|
116
|
+
file_path.relative_path_from(base_path)
|
117
|
+
end
|
118
|
+
|
119
|
+
def error_location(exception)
|
120
|
+
@error_location ||= begin
|
121
|
+
last_before_assertion = ''
|
122
|
+
exception.backtrace.reverse_each do |s|
|
123
|
+
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
124
|
+
|
125
|
+
last_before_assertion = s
|
126
|
+
end
|
127
|
+
path = last_before_assertion.sub(/:in .*$/, '')
|
128
|
+
# the path includes the linenumber at the end,
|
129
|
+
# which is seperated by a :
|
130
|
+
# rpartition splits the string at the last occurence of :
|
131
|
+
result = path.rpartition(':')
|
132
|
+
# We return [path, linenumber] here
|
133
|
+
[result.first, result.last.to_i]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'minitest/reporters'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require 'minitest/queue/test_data'
|
7
|
+
|
8
|
+
module Minitest
|
9
|
+
module Queue
|
10
|
+
class TestDataReporter < Minitest::Reporters::BaseReporter
|
11
|
+
def initialize(report_path: 'log/test_data.json', base_path: nil, namespace: '')
|
12
|
+
super({})
|
13
|
+
@report_path = File.absolute_path(report_path)
|
14
|
+
@base_path = base_path || Dir.pwd
|
15
|
+
@namespace = namespace || ''
|
16
|
+
end
|
17
|
+
|
18
|
+
def report
|
19
|
+
super
|
20
|
+
|
21
|
+
result = tests.map.with_index do |test, index|
|
22
|
+
Queue::TestData.new(test: test, index: index,
|
23
|
+
base_path: @base_path, namespace: @namespace).to_h
|
24
|
+
end.to_json
|
25
|
+
|
26
|
+
dirname = File.dirname(@report_path)
|
27
|
+
FileUtils.mkdir_p(dirname)
|
28
|
+
File.open(@report_path, 'w+') { |file| file << result }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Minitest
|
3
|
+
module Queue
|
4
|
+
class TestTimeRecorder < Minitest::Reporters::BaseReporter
|
5
|
+
def initialize(build:, **options)
|
6
|
+
super(options)
|
7
|
+
@build = build
|
8
|
+
end
|
9
|
+
|
10
|
+
def record(test)
|
11
|
+
return unless test.passed?
|
12
|
+
test_duration_in_milliseconds = test.time * 1000
|
13
|
+
@build.record(test.name, test_duration_in_milliseconds)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'minitest/reporters'
|
3
|
+
|
4
|
+
module Minitest
|
5
|
+
module Queue
|
6
|
+
class TestTimeReporter < Minitest::Reporters::BaseReporter
|
7
|
+
include ::CI::Queue::OutputHelpers
|
8
|
+
|
9
|
+
def initialize(build:, limit: nil, percentile: nil, **options)
|
10
|
+
super(options)
|
11
|
+
@test_time_hash = build.fetch
|
12
|
+
@limit = limit
|
13
|
+
@percentile = percentile
|
14
|
+
@success = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def report
|
18
|
+
return if limit.nil? || test_time_hash.empty?
|
19
|
+
|
20
|
+
puts '+++ Test Time Report'
|
21
|
+
|
22
|
+
if offending_tests.empty?
|
23
|
+
msg = "The #{humanized_percentile} of test execution time is within #{limit} milliseconds."
|
24
|
+
puts green(msg)
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
@success = false
|
29
|
+
puts <<~EOS
|
30
|
+
#{red("Detected #{offending_tests.size} test(s) over the desired time limit.")}
|
31
|
+
Please make them faster than #{limit}ms in the #{humanized_percentile} percentile.
|
32
|
+
EOS
|
33
|
+
offending_tests.each do |test_name, duration|
|
34
|
+
puts "#{red(test_name)}: #{duration}ms"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def success?
|
39
|
+
@success
|
40
|
+
end
|
41
|
+
|
42
|
+
def record(*)
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :test_time_hash, :limit, :percentile
|
49
|
+
|
50
|
+
def humanized_percentile
|
51
|
+
percentile_in_percentage = percentile * 100
|
52
|
+
"#{percentile_in_percentage.to_i}th"
|
53
|
+
end
|
54
|
+
|
55
|
+
def offending_tests
|
56
|
+
@offending_tests ||= begin
|
57
|
+
test_time_hash.each_with_object({}) do |(test_name, durations), offenders|
|
58
|
+
duration = calculate_percentile(durations)
|
59
|
+
next if duration <= limit
|
60
|
+
offenders[test_name] = duration
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def calculate_percentile(array)
|
66
|
+
array.sort[(percentile * array.length).ceil - 1]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/rspec/queue.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'fileutils'
|
1
3
|
require 'delegate'
|
2
4
|
require 'rspec/core'
|
3
5
|
require 'ci/queue'
|
4
6
|
require 'rspec/queue/build_status_recorder'
|
7
|
+
require 'rspec/queue/order_recorder'
|
5
8
|
|
6
9
|
module RSpec
|
7
10
|
module Queue
|
@@ -64,7 +67,7 @@ module RSpec
|
|
64
67
|
|
65
68
|
parser.separator("\n **** Queue options ****\n\n")
|
66
69
|
|
67
|
-
help =
|
70
|
+
help = <<~EOS
|
68
71
|
URL of the queue, e.g. redis://example.com.
|
69
72
|
Defaults to $CI_QUEUE_URL if set.
|
70
73
|
EOS
|
@@ -73,7 +76,7 @@ module RSpec
|
|
73
76
|
options[:queue_url] = url
|
74
77
|
end
|
75
78
|
|
76
|
-
help =
|
79
|
+
help = <<~EOS
|
77
80
|
Wait for all workers to complete and summarize the test failures.
|
78
81
|
EOS
|
79
82
|
parser.on('--report', *help) do |url|
|
@@ -81,14 +84,14 @@ module RSpec
|
|
81
84
|
options[:runner] = RSpec::Queue::ReportRunner.new
|
82
85
|
end
|
83
86
|
|
84
|
-
help =
|
87
|
+
help = <<~EOS
|
85
88
|
Replays a previous run in the same order.
|
86
89
|
EOS
|
87
90
|
parser.on('--retry', *help) do |url|
|
88
91
|
STDERR.puts "Warning: The --retry flag is deprecated"
|
89
92
|
end
|
90
93
|
|
91
|
-
help =
|
94
|
+
help = <<~EOS
|
92
95
|
Unique identifier for the workload. All workers working on the same suite of tests must have the same build identifier.
|
93
96
|
If the build is tried again, or another revision is built, this value must be different.
|
94
97
|
It's automatically inferred on Buildkite, CircleCI and Travis.
|
@@ -98,7 +101,7 @@ module RSpec
|
|
98
101
|
queue_config.build_id = build_id
|
99
102
|
end
|
100
103
|
|
101
|
-
help =
|
104
|
+
help = <<~EOS
|
102
105
|
Optional. Sets a prefix for the build id in case a single CI build runs multiple independent test suites.
|
103
106
|
Example: --namespace integration
|
104
107
|
EOS
|
@@ -107,7 +110,7 @@ module RSpec
|
|
107
110
|
queue_config.namespace = namespace
|
108
111
|
end
|
109
112
|
|
110
|
-
help =
|
113
|
+
help = <<~EOS
|
111
114
|
Specify a timeout after which if a test haven't completed, it will be picked up by another worker.
|
112
115
|
It is very important to set this vlaue higher than the slowest test in the suite, otherwise performance will be impacted.
|
113
116
|
Defaults to 30 seconds.
|
@@ -117,7 +120,7 @@ module RSpec
|
|
117
120
|
queue_config.timeout = Float(timeout)
|
118
121
|
end
|
119
122
|
|
120
|
-
help =
|
123
|
+
help = <<~EOS
|
121
124
|
A unique identifier for this worker, It must be consistent to allow retries.
|
122
125
|
If not specified, retries won't be available.
|
123
126
|
It's automatically inferred on Buildkite and CircleCI.
|
@@ -127,7 +130,7 @@ module RSpec
|
|
127
130
|
queue_config.worker_id = worker_id
|
128
131
|
end
|
129
132
|
|
130
|
-
help =
|
133
|
+
help = <<~EOS
|
131
134
|
Defines how many time a single test can be requeued.
|
132
135
|
Defaults to 0.
|
133
136
|
EOS
|
@@ -136,7 +139,7 @@ module RSpec
|
|
136
139
|
queue_config.max_requeues = Integer(max)
|
137
140
|
end
|
138
141
|
|
139
|
-
help =
|
142
|
+
help = <<~EOS
|
140
143
|
Defines how many requeues can happen overall, based on the test suite size. e.g 0.05 for 5%.
|
141
144
|
Defaults to 0.
|
142
145
|
EOS
|
@@ -145,7 +148,7 @@ module RSpec
|
|
145
148
|
queue_config.requeue_tolerance = Float(ratio)
|
146
149
|
end
|
147
150
|
|
148
|
-
help =
|
151
|
+
help = <<~EOS
|
149
152
|
Defines after how many consecutive failures the worker will be considered unhealthy and terminate itself.
|
150
153
|
Defaults to disabled.
|
151
154
|
EOS
|
@@ -157,10 +160,6 @@ module RSpec
|
|
157
160
|
parser
|
158
161
|
end
|
159
162
|
|
160
|
-
def split_heredoc(string)
|
161
|
-
string.lines.map(&:strip)
|
162
|
-
end
|
163
|
-
|
164
163
|
def queue_config
|
165
164
|
::RSpec::Queue.config
|
166
165
|
end
|
@@ -387,6 +386,8 @@ module RSpec
|
|
387
386
|
success = true
|
388
387
|
@configuration.reporter.report(examples_count) do |reporter|
|
389
388
|
@configuration.add_formatter(BuildStatusRecorder)
|
389
|
+
FileUtils.mkdir_p('log')
|
390
|
+
@configuration.add_formatter(OrderRecorder, open('log/test_order.log', 'w+'))
|
390
391
|
|
391
392
|
@configuration.with_suite_hooks do
|
392
393
|
break if @world.wants_to_quit
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rspec/core/formatters/base_formatter'
|
3
|
+
|
4
|
+
module RSpec
|
5
|
+
module Queue
|
6
|
+
class OrderRecorder < ::RSpec::Core::Formatters::BaseFormatter
|
7
|
+
::RSpec::Core::Formatters.register self, :example_started
|
8
|
+
|
9
|
+
def initialize(*)
|
10
|
+
super
|
11
|
+
output.sync = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def example_started(notification)
|
15
|
+
return if notification.is_a?(RSpec::Core::Notifications::SkippedExampleNotification)
|
16
|
+
output.write("#{notification.example.id}\n")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
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.17.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:
|
11
|
+
date: 2020-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ansi
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
153
167
|
description: To parallelize your CI without having to balance your tests
|
154
168
|
email:
|
155
169
|
- jean.boussier@shopify.com
|
@@ -178,16 +192,21 @@ files:
|
|
178
192
|
- lib/ci/queue/common.rb
|
179
193
|
- lib/ci/queue/configuration.rb
|
180
194
|
- lib/ci/queue/file.rb
|
195
|
+
- lib/ci/queue/grind.rb
|
181
196
|
- lib/ci/queue/output_helpers.rb
|
182
197
|
- lib/ci/queue/redis.rb
|
183
198
|
- lib/ci/queue/redis/acknowledge.lua
|
184
199
|
- lib/ci/queue/redis/base.rb
|
185
200
|
- lib/ci/queue/redis/build_record.rb
|
201
|
+
- lib/ci/queue/redis/grind.rb
|
202
|
+
- lib/ci/queue/redis/grind_record.rb
|
203
|
+
- lib/ci/queue/redis/grind_supervisor.rb
|
186
204
|
- lib/ci/queue/redis/requeue.lua
|
187
205
|
- lib/ci/queue/redis/reserve.lua
|
188
206
|
- lib/ci/queue/redis/reserve_lost.lua
|
189
207
|
- lib/ci/queue/redis/retry.rb
|
190
208
|
- lib/ci/queue/redis/supervisor.rb
|
209
|
+
- lib/ci/queue/redis/test_time_record.rb
|
191
210
|
- lib/ci/queue/redis/worker.rb
|
192
211
|
- lib/ci/queue/static.rb
|
193
212
|
- lib/ci/queue/version.rb
|
@@ -196,15 +215,22 @@ files:
|
|
196
215
|
- lib/minitest/queue/build_status_reporter.rb
|
197
216
|
- lib/minitest/queue/error_report.rb
|
198
217
|
- lib/minitest/queue/failure_formatter.rb
|
218
|
+
- lib/minitest/queue/grind_recorder.rb
|
219
|
+
- lib/minitest/queue/grind_reporter.rb
|
199
220
|
- lib/minitest/queue/junit_reporter.rb
|
200
221
|
- lib/minitest/queue/local_requeue_reporter.rb
|
201
222
|
- lib/minitest/queue/order_reporter.rb
|
202
223
|
- lib/minitest/queue/runner.rb
|
203
224
|
- lib/minitest/queue/statsd.rb
|
225
|
+
- lib/minitest/queue/test_data.rb
|
226
|
+
- lib/minitest/queue/test_data_reporter.rb
|
227
|
+
- lib/minitest/queue/test_time_recorder.rb
|
228
|
+
- lib/minitest/queue/test_time_reporter.rb
|
204
229
|
- lib/minitest/reporters/bisect_reporter.rb
|
205
230
|
- lib/minitest/reporters/statsd_reporter.rb
|
206
231
|
- lib/rspec/queue.rb
|
207
232
|
- lib/rspec/queue/build_status_recorder.rb
|
233
|
+
- lib/rspec/queue/order_recorder.rb
|
208
234
|
- railgun.yml
|
209
235
|
homepage: https://github.com/Shopify/ci-queue
|
210
236
|
licenses:
|
@@ -225,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
225
251
|
- !ruby/object:Gem::Version
|
226
252
|
version: '0'
|
227
253
|
requirements: []
|
228
|
-
|
229
|
-
rubygems_version: 2.7.6
|
254
|
+
rubygems_version: 3.0.3
|
230
255
|
signing_key:
|
231
256
|
specification_version: 4
|
232
257
|
summary: Distribute tests over many workers using a queue
|