ci-queue 0.11.1 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a44623e2359645e895ede304d4499f5efb7c689b
4
- data.tar.gz: 764685c0bb4dc8b568455aa3edb0ae7a384f5893
2
+ SHA256:
3
+ metadata.gz: 78b51045e571e242b8d4b64e49f37abd462be948f007de6a63380ca0f448ce4c
4
+ data.tar.gz: bdec2201394129e091516b82e5709a016209aec6a8f41488132ac8400c88150d
5
5
  SHA512:
6
- metadata.gz: 34f7fd1d6196c1b68e850b06472d8961acd449fd68cee006a0cfc9e6b2d79b338a22aad78a6e22efeba79d424b8eeaa595c857d81d63470e157ebcf7e7a1404e
7
- data.tar.gz: 641f5fb0cc38c5adbfe6308d637d8a0f93b66c2a1e547ff87d7077147f2d3edc0b818c6d15298e076f66a2b050878aa4f9778880c671d6a96094b9c11ab17ce5
6
+ metadata.gz: e51188516bdd8b04c29981a4cd7f89ac2f6a8a7979c72210065362d34b86565522e3e26305a2c088656d543c064c802c4886f6ad4801f675b7c73cbec82da38c
7
+ data.tar.gz: 14853c01858461192992d2dd330a96dcb66ca6a9c17100c4d73b4d6b364cb7b1c7e8661b14d8136a454f2d0bf0a72a8af375a07dd6eba91a2541784f6964f1fc
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
+ test/fixtures/log/junit.xml
2
+ test/fixtures/log/test_order.log
1
3
  /.byebug_history
2
4
  /.bundle/
3
5
  /.yardoc
data/dev.yml CHANGED
@@ -9,12 +9,3 @@ up:
9
9
 
10
10
  commands:
11
11
  test: REDIS_HOST=ci-queue.railgun bundle exec rake test
12
-
13
- railgun:
14
- image: dev:railgun-common-services-0.2.x
15
- services:
16
- redis: 6379
17
- ip_address: 192.168.64.245
18
- memory: 1G
19
- cores: 1
20
- disk: 512M
@@ -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
@@ -8,6 +8,10 @@ module CI
8
8
  @config = config
9
9
  end
10
10
 
11
+ def flaky?(test)
12
+ @config.flaky?(test)
13
+ end
14
+
11
15
  def exhausted?
12
16
  queue_initialized? && size == 0
13
17
  end
@@ -17,6 +17,10 @@ module CI
17
17
  @total = tests.size
18
18
  end
19
19
 
20
+ def flaky?(test)
21
+ @config.flaky?(test)
22
+ end
23
+
20
24
  def build
21
25
  @build ||= BuildRecord.new(self)
22
26
  end
@@ -1,6 +1,6 @@
1
1
  module CI
2
2
  module Queue
3
- VERSION = '0.11.1'
3
+ VERSION = '0.12.0'
4
4
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
5
5
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
6
6
  end
@@ -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.send(:alias_method, :klass, :class)
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
@@ -1,6 +1,6 @@
1
1
  require 'minitest/reporters'
2
2
 
3
- class Minitest::Reporters::OrderReporter < Minitest::Reporters::BaseReporter
3
+ class Minitest::Queue::OrderReporter < Minitest::Reporters::BaseReporter
4
4
  def initialize(options = {})
5
5
  @path = options.delete(:path)
6
6
  super
@@ -44,8 +44,10 @@ module Minitest
44
44
  set_load_path
45
45
  Minitest.queue = queue
46
46
  Minitest.queue_reporters = [
47
- Minitest::Queue::LocalRequeueReporter.new,
48
- Minitest::Queue::BuildStatusRecorder.new(build: queue.build)
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! }
@@ -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.11.1
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-02-13 00:00:00.000000000 Z
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.13
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