ci-queue 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/dev.yml +0 -9
- data/lib/ci/queue/configuration.rb +15 -1
- data/lib/ci/queue/redis/base.rb +4 -0
- data/lib/ci/queue/static.rb +4 -0
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue.rb +69 -2
- data/lib/minitest/queue/junit_reporter.rb +113 -0
- data/lib/minitest/{reporters → queue}/order_reporter.rb +1 -1
- data/lib/minitest/queue/runner.rb +4 -2
- data/railgun.yml +13 -0
- metadata +6 -5
- data/lib/minitest/reporters/redis_reporter.rb +0 -91
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 78b51045e571e242b8d4b64e49f37abd462be948f007de6a63380ca0f448ce4c
|
4
|
+
data.tar.gz: bdec2201394129e091516b82e5709a016209aec6a8f41488132ac8400c88150d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e51188516bdd8b04c29981a4cd7f89ac2f6a8a7979c72210065362d34b86565522e3e26305a2c088656d543c064c802c4886f6ad4801f675b7c73cbec82da38c
|
7
|
+
data.tar.gz: 14853c01858461192992d2dd330a96dcb66ca6a9c17100c4d73b4d6b364cb7b1c7e8661b14d8136a454f2d0bf0a72a8af375a07dd6eba91a2541784f6964f1fc
|
data/.gitignore
CHANGED
data/dev.yml
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
|
1
2
|
module CI
|
2
3
|
module Queue
|
3
4
|
class Configuration
|
@@ -9,13 +10,21 @@ module CI
|
|
9
10
|
build_id: env['CIRCLE_BUILD_URL'] || env['BUILDKITE_BUILD_ID'] || env['TRAVIS_BUILD_ID'],
|
10
11
|
worker_id: env['CIRCLE_NODE_INDEX'] || env['BUILDKITE_PARALLEL_JOB'],
|
11
12
|
seed: env['CIRCLE_SHA1'] || env['BUILDKITE_COMMIT'] || env['TRAVIS_COMMIT'],
|
13
|
+
flaky_tests: load_flaky_tests(env['CI_QUEUE_FLAKY_TESTS']),
|
12
14
|
)
|
13
15
|
end
|
16
|
+
|
17
|
+
def load_flaky_tests(path)
|
18
|
+
return [] unless path
|
19
|
+
::File.readlines(path).map(&:chomp).to_set
|
20
|
+
rescue SystemCallError
|
21
|
+
[]
|
22
|
+
end
|
14
23
|
end
|
15
24
|
|
16
25
|
def initialize(
|
17
26
|
timeout: 30, build_id: nil, worker_id: nil, max_requeues: 0, requeue_tolerance: 0,
|
18
|
-
namespace: nil, seed: nil
|
27
|
+
namespace: nil, seed: nil, flaky_tests: []
|
19
28
|
)
|
20
29
|
@namespace = namespace
|
21
30
|
@timeout = timeout
|
@@ -24,6 +33,11 @@ module CI
|
|
24
33
|
@max_requeues = max_requeues
|
25
34
|
@requeue_tolerance = requeue_tolerance
|
26
35
|
@seed = seed
|
36
|
+
@flaky_tests = flaky_tests
|
37
|
+
end
|
38
|
+
|
39
|
+
def flaky?(test)
|
40
|
+
@flaky_tests.include?(test.id)
|
27
41
|
end
|
28
42
|
|
29
43
|
def seed
|
data/lib/ci/queue/redis/base.rb
CHANGED
data/lib/ci/queue/static.rb
CHANGED
data/lib/ci/queue/version.rb
CHANGED
data/lib/minitest/queue.rb
CHANGED
@@ -7,7 +7,8 @@ require 'minitest/queue/error_report'
|
|
7
7
|
require 'minitest/queue/local_requeue_reporter'
|
8
8
|
require 'minitest/queue/build_status_recorder'
|
9
9
|
require 'minitest/queue/build_status_reporter'
|
10
|
-
|
10
|
+
require 'minitest/queue/order_reporter'
|
11
|
+
require 'minitest/queue/junit_reporter'
|
11
12
|
|
12
13
|
module Minitest
|
13
14
|
class Requeue < Skip
|
@@ -35,6 +36,31 @@ module Minitest
|
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
39
|
+
class Flaked < Skip
|
40
|
+
attr_reader :failure
|
41
|
+
|
42
|
+
def initialize(failure)
|
43
|
+
super()
|
44
|
+
@failure = failure
|
45
|
+
end
|
46
|
+
|
47
|
+
def result_label
|
48
|
+
"Skipped"
|
49
|
+
end
|
50
|
+
|
51
|
+
def backtrace
|
52
|
+
failure.backtrace
|
53
|
+
end
|
54
|
+
|
55
|
+
def error
|
56
|
+
failure.error
|
57
|
+
end
|
58
|
+
|
59
|
+
def message
|
60
|
+
failure.message
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
38
64
|
module Requeueing
|
39
65
|
# Make requeues acts as skips for reporters not aware of the difference.
|
40
66
|
def skipped?
|
@@ -50,6 +76,25 @@ module Minitest
|
|
50
76
|
end
|
51
77
|
end
|
52
78
|
|
79
|
+
module Flakiness
|
80
|
+
# Make failed flaky tests acts as skips for reporters not aware of the difference.
|
81
|
+
def skipped?
|
82
|
+
super || flaked?
|
83
|
+
end
|
84
|
+
|
85
|
+
def flaked?
|
86
|
+
!!((Flaked === failure) || @flaky)
|
87
|
+
end
|
88
|
+
|
89
|
+
def mark_as_flaked!
|
90
|
+
if passed?
|
91
|
+
@flaky = true
|
92
|
+
else
|
93
|
+
self.failures.unshift(Flaked.new(self.failures.shift))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
53
98
|
module Queue
|
54
99
|
class SingleExample
|
55
100
|
def initialize(runnable, method_name)
|
@@ -68,6 +113,10 @@ module Minitest
|
|
68
113
|
def run
|
69
114
|
Minitest.run_one_method(@runnable, @method_name)
|
70
115
|
end
|
116
|
+
|
117
|
+
def flaky?
|
118
|
+
Minitest.queue.flaky?(self)
|
119
|
+
end
|
71
120
|
end
|
72
121
|
|
73
122
|
attr_reader :queue
|
@@ -103,6 +152,12 @@ module Minitest
|
|
103
152
|
queue.poll do |example|
|
104
153
|
result = example.run
|
105
154
|
failed = !(result.passed? || result.skipped?)
|
155
|
+
|
156
|
+
if example.flaky?
|
157
|
+
result.mark_as_flaked!
|
158
|
+
failed = false
|
159
|
+
end
|
160
|
+
|
106
161
|
if failed && queue.requeue(example)
|
107
162
|
result.requeue!
|
108
163
|
reporter.record(result)
|
@@ -119,7 +174,19 @@ end
|
|
119
174
|
MiniTest.singleton_class.prepend(MiniTest::Queue)
|
120
175
|
if defined? MiniTest::Result
|
121
176
|
MiniTest::Result.prepend(MiniTest::Requeueing)
|
177
|
+
MiniTest::Result.prepend(MiniTest::Flakiness)
|
122
178
|
else
|
123
179
|
MiniTest::Test.prepend(MiniTest::Requeueing)
|
124
|
-
MiniTest::Test.
|
180
|
+
MiniTest::Test.prepend(MiniTest::Flakiness)
|
181
|
+
|
182
|
+
module MinitestBackwardCompatibility
|
183
|
+
def source_location
|
184
|
+
method(name).source_location
|
185
|
+
end
|
186
|
+
|
187
|
+
def klass
|
188
|
+
self.class.name
|
189
|
+
end
|
190
|
+
end
|
191
|
+
MiniTest::Test.prepend(MinitestBackwardCompatibility)
|
125
192
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'minitest/reporters'
|
2
|
+
require 'builder'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Minitest
|
6
|
+
module Queue
|
7
|
+
class JUnitReporter < Minitest::Reporters::BaseReporter
|
8
|
+
class XmlMarkup < ::Builder::XmlMarkup
|
9
|
+
def trunc!(txt)
|
10
|
+
txt.sub(/\n.*/m, '...')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(report_path = 'log/junit.xml', options = {})
|
15
|
+
super({})
|
16
|
+
@report_path = File.absolute_path(report_path)
|
17
|
+
@base_path = options[:base_path] || Dir.pwd
|
18
|
+
end
|
19
|
+
|
20
|
+
def report
|
21
|
+
super
|
22
|
+
|
23
|
+
suites = tests.group_by { |test| test.klass }
|
24
|
+
|
25
|
+
xml = Builder::XmlMarkup.new(indent: 2)
|
26
|
+
xml.instruct!
|
27
|
+
xml.test_suites do
|
28
|
+
suites.each do |suite, tests|
|
29
|
+
add_tests_to(xml, suite, tests)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
FileUtils.mkdir_p(File.dirname(@report_path))
|
33
|
+
File.open(@report_path, 'w+') { |file| file << xml.target! }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def add_tests_to(xml, suite, tests)
|
39
|
+
suite_result = analyze_suite(tests)
|
40
|
+
file_path = Pathname.new(tests.first.source_location.first)
|
41
|
+
base_path = Pathname.new(@base_path)
|
42
|
+
relative_path = file_path.relative_path_from(base_path)
|
43
|
+
|
44
|
+
xml.testsuite(name: suite, filepath: relative_path,
|
45
|
+
skipped: suite_result[:skip_count], failures: suite_result[:fail_count],
|
46
|
+
errors: suite_result[:error_count], tests: suite_result[:test_count],
|
47
|
+
assertions: suite_result[:assertion_count], time: suite_result[:time]) do
|
48
|
+
tests.each do |test|
|
49
|
+
lineno = test.source_location.last
|
50
|
+
xml.testcase(name: test.name, lineno: lineno, classname: suite, assertions: test.assertions,
|
51
|
+
time: test.time, flaky_test: test.flaked?) do
|
52
|
+
xml << xml_message_for(test) unless test.passed?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def xml_message_for(test)
|
59
|
+
xml = XmlMarkup.new(indent: 2, margin: 2)
|
60
|
+
error = test.failure
|
61
|
+
|
62
|
+
if test.skipped? && !test.flaked?
|
63
|
+
xml.skipped(type: test.name)
|
64
|
+
elsif test.error?
|
65
|
+
xml.error(type: test.name, message: xml.trunc!(error.message)) do
|
66
|
+
xml.text!(message_for(test))
|
67
|
+
end
|
68
|
+
elsif test.failure
|
69
|
+
xml.failure(type: test.name, message: xml.trunc!(error.message)) do
|
70
|
+
xml.text!(message_for(test))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def message_for(test)
|
76
|
+
suite = test.klass
|
77
|
+
name = test.name
|
78
|
+
error = test.failure
|
79
|
+
|
80
|
+
if test.passed?
|
81
|
+
nil
|
82
|
+
elsif test.skipped?
|
83
|
+
"Skipped:\n#{name}(#{suite}) [#{location(error)}]:\n#{error.message}\n"
|
84
|
+
elsif test.failure
|
85
|
+
"Failure:\n#{name}(#{suite}) [#{location(error)}]:\n#{error.message}\n"
|
86
|
+
elsif test.error?
|
87
|
+
"Error:\n#{name}(#{suite}):\n#{error.message}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def location(exception)
|
92
|
+
last_before_assertion = ''
|
93
|
+
exception.backtrace.reverse_each do |s|
|
94
|
+
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
95
|
+
last_before_assertion = s
|
96
|
+
end
|
97
|
+
last_before_assertion.sub(/:in .*$/, '')
|
98
|
+
end
|
99
|
+
|
100
|
+
def analyze_suite(tests)
|
101
|
+
result = Hash.new(0)
|
102
|
+
result[:time] = 0
|
103
|
+
tests.each do |test|
|
104
|
+
result[:"#{result(test)}_count"] += 1
|
105
|
+
result[:assertion_count] += test.assertions
|
106
|
+
result[:test_count] += 1
|
107
|
+
result[:time] += test.time
|
108
|
+
end
|
109
|
+
result
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -44,8 +44,10 @@ module Minitest
|
|
44
44
|
set_load_path
|
45
45
|
Minitest.queue = queue
|
46
46
|
Minitest.queue_reporters = [
|
47
|
-
|
48
|
-
|
47
|
+
LocalRequeueReporter.new,
|
48
|
+
BuildStatusRecorder.new(build: queue.build),
|
49
|
+
JUnitReporter.new,
|
50
|
+
OrderReporter.new(path: 'log/test_order.log'),
|
49
51
|
]
|
50
52
|
|
51
53
|
trap('TERM') { Minitest.queue.shutdown! }
|
data/railgun.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# This file is for Shopify employees development environment.
|
2
|
+
# If you are an external contributor you don't have to bother with it.
|
3
|
+
name: ci-queue
|
4
|
+
|
5
|
+
vm:
|
6
|
+
image: /opt/dev/misc/railgun-images/default
|
7
|
+
ip_address: 192.168.64.245
|
8
|
+
memory: 1G
|
9
|
+
cores: 2
|
10
|
+
volumes:
|
11
|
+
root: 512M
|
12
|
+
services:
|
13
|
+
- redis
|
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.12.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-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ansi
|
@@ -194,13 +194,14 @@ files:
|
|
194
194
|
- lib/minitest/queue/build_status_reporter.rb
|
195
195
|
- lib/minitest/queue/error_report.rb
|
196
196
|
- lib/minitest/queue/failure_formatter.rb
|
197
|
+
- lib/minitest/queue/junit_reporter.rb
|
197
198
|
- lib/minitest/queue/local_requeue_reporter.rb
|
199
|
+
- lib/minitest/queue/order_reporter.rb
|
198
200
|
- lib/minitest/queue/runner.rb
|
199
201
|
- lib/minitest/reporters/bisect_reporter.rb
|
200
|
-
- lib/minitest/reporters/order_reporter.rb
|
201
|
-
- lib/minitest/reporters/redis_reporter.rb
|
202
202
|
- lib/rspec/queue.rb
|
203
203
|
- lib/rspec/queue/build_status_recorder.rb
|
204
|
+
- railgun.yml
|
204
205
|
homepage: https://github.com/Shopify/ci-queue
|
205
206
|
licenses:
|
206
207
|
- MIT
|
@@ -221,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
222
|
version: '0'
|
222
223
|
requirements: []
|
223
224
|
rubyforge_project:
|
224
|
-
rubygems_version: 2.6
|
225
|
+
rubygems_version: 2.7.6
|
225
226
|
signing_key:
|
226
227
|
specification_version: 4
|
227
228
|
summary: Distribute tests over many workers using a queue
|
@@ -1,91 +0,0 @@
|
|
1
|
-
require 'minitest/reporters'
|
2
|
-
|
3
|
-
module Minitest
|
4
|
-
module Reporters
|
5
|
-
module RedisReporter
|
6
|
-
include ANSI::Code
|
7
|
-
|
8
|
-
COUNTERS = %w(
|
9
|
-
assertions
|
10
|
-
errors
|
11
|
-
failures
|
12
|
-
skips
|
13
|
-
requeues
|
14
|
-
total_time
|
15
|
-
).freeze
|
16
|
-
|
17
|
-
class << self
|
18
|
-
attr_accessor :failure_formatter
|
19
|
-
end
|
20
|
-
self.failure_formatter = FailureFormatter
|
21
|
-
|
22
|
-
class Base < BaseReporter
|
23
|
-
def initialize(build:, **options)
|
24
|
-
@build = build
|
25
|
-
super(options)
|
26
|
-
end
|
27
|
-
|
28
|
-
def completed?
|
29
|
-
build.queue_exhausted?
|
30
|
-
end
|
31
|
-
|
32
|
-
def error_reports
|
33
|
-
build.error_reports.sort_by(&:first).map { |k, v| ErrorReport.load(v) }
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
class Summary < Base
|
41
|
-
end
|
42
|
-
|
43
|
-
class Worker < Base
|
44
|
-
attr_accessor :requeues
|
45
|
-
|
46
|
-
def initialize(*)
|
47
|
-
super
|
48
|
-
self.failures = 0
|
49
|
-
self.errors = 0
|
50
|
-
self.skips = 0
|
51
|
-
self.requeues = 0
|
52
|
-
end
|
53
|
-
|
54
|
-
def report
|
55
|
-
# noop
|
56
|
-
end
|
57
|
-
|
58
|
-
def record(test)
|
59
|
-
super
|
60
|
-
|
61
|
-
self.total_time = Minitest.clock_time - start_time
|
62
|
-
if test.requeued?
|
63
|
-
self.requeues += 1
|
64
|
-
elsif test.skipped?
|
65
|
-
self.skips += 1
|
66
|
-
elsif test.error?
|
67
|
-
self.errors += 1
|
68
|
-
elsif test.failure
|
69
|
-
self.failures += 1
|
70
|
-
end
|
71
|
-
|
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)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
private
|
82
|
-
|
83
|
-
def dump(test)
|
84
|
-
ErrorReport.new(RedisReporter.failure_formatter.new(test).to_h).dump
|
85
|
-
end
|
86
|
-
|
87
|
-
attr_reader :aggregates
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|