ci-queue 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/Rakefile +1 -0
  4. data/bin/console +1 -0
  5. data/ci-queue.gemspec +2 -0
  6. data/exe/minitest-queue +1 -0
  7. data/exe/rspec-queue +1 -0
  8. data/lib/ci/queue.rb +3 -0
  9. data/lib/ci/queue/bisect.rb +1 -0
  10. data/lib/ci/queue/build_record.rb +1 -0
  11. data/lib/ci/queue/circuit_breaker.rb +39 -0
  12. data/lib/ci/queue/common.rb +3 -2
  13. data/lib/ci/queue/configuration.rb +25 -11
  14. data/lib/ci/queue/file.rb +1 -0
  15. data/lib/ci/queue/grind.rb +23 -0
  16. data/lib/ci/queue/output_helpers.rb +1 -0
  17. data/lib/ci/queue/redis.rb +6 -0
  18. data/lib/ci/queue/redis/base.rb +1 -0
  19. data/lib/ci/queue/redis/build_record.rb +4 -3
  20. data/lib/ci/queue/redis/grind.rb +17 -0
  21. data/lib/ci/queue/redis/grind_record.rb +66 -0
  22. data/lib/ci/queue/redis/grind_supervisor.rb +13 -0
  23. data/lib/ci/queue/redis/retry.rb +1 -0
  24. data/lib/ci/queue/redis/supervisor.rb +2 -1
  25. data/lib/ci/queue/redis/test_time_record.rb +66 -0
  26. data/lib/ci/queue/redis/worker.rb +2 -1
  27. data/lib/ci/queue/static.rb +3 -1
  28. data/lib/ci/queue/version.rb +3 -1
  29. data/lib/minitest/queue.rb +10 -4
  30. data/lib/minitest/queue/build_status_recorder.rb +1 -0
  31. data/lib/minitest/queue/build_status_reporter.rb +1 -0
  32. data/lib/minitest/queue/error_report.rb +13 -0
  33. data/lib/minitest/queue/failure_formatter.rb +4 -0
  34. data/lib/minitest/queue/grind_recorder.rb +68 -0
  35. data/lib/minitest/queue/grind_reporter.rb +74 -0
  36. data/lib/minitest/queue/junit_reporter.rb +6 -5
  37. data/lib/minitest/queue/local_requeue_reporter.rb +1 -0
  38. data/lib/minitest/queue/order_reporter.rb +1 -0
  39. data/lib/minitest/queue/runner.rb +156 -32
  40. data/lib/minitest/queue/statsd.rb +1 -0
  41. data/lib/minitest/queue/test_data.rb +138 -0
  42. data/lib/minitest/queue/test_data_reporter.rb +32 -0
  43. data/lib/minitest/queue/test_time_recorder.rb +17 -0
  44. data/lib/minitest/queue/test_time_reporter.rb +70 -0
  45. data/lib/minitest/reporters/bisect_reporter.rb +1 -0
  46. data/lib/minitest/reporters/statsd_reporter.rb +1 -0
  47. data/lib/rspec/queue.rb +15 -14
  48. data/lib/rspec/queue/build_status_recorder.rb +1 -0
  49. data/lib/rspec/queue/order_recorder.rb +20 -0
  50. metadata +29 -4
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Implements a small and limited StatsD implementation to reduce importing unnecessary dependencies because
2
3
  # we don't want to require on the bundle which would slow down a CI Queue run
3
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'minitest/reporters'
2
3
 
3
4
  module Minitest
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'minitest/queue'
2
3
  require 'minitest/queue/statsd'
3
4
  require 'minitest/reporters'
@@ -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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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 = split_heredoc(<<-EOS)
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module RSpec
2
3
  module Queue
3
4
  class BuildStatusRecorder
@@ -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.16.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: 2019-03-19 00:00:00.000000000 Z
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
- rubyforge_project:
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