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 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