ci-queue 0.6.0 → 0.7.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
2
  SHA1:
3
- metadata.gz: 4d636ab99ab50bd22a5fc67415187e75bac147b8
4
- data.tar.gz: 6825eca869948ed64897fb6e2b3ce46fc5e1a918
3
+ metadata.gz: 97f69595ec09509df32703213fd2d882db14f017
4
+ data.tar.gz: cae08e13e586bd5c7cc8927057225084b0ee0d74
5
5
  SHA512:
6
- metadata.gz: 129ec21f70ad9ee2ad108537bfc7ec0032b4bbf9738973da28376a558f37dbc5de884bd4d5e9049ecf94f2ee5c02760f9a678fae67d40a92b7d2c31919f2c123
7
- data.tar.gz: 5759824ed9f3b81060b926de0093d523c229f13bd98e5cefb6d4770f97da43acedd480cd97c2af92b4af5c51ecf4474a51a3461007760aa16a5de90529ec8d6c
6
+ metadata.gz: 742b3d39caf903b480950a86eafb687776e8b4d1a9465d28e1ce29ff37d3242f3127a40d7115e51eddec03b6e22322cef0108ef2a64f6a5744b642b3e1b14ecf
7
+ data.tar.gz: 8f753085f6408c98e698c0a75c06b77a427e066cbad9aa7c1db102613cdcf4b6d25d27e93145e86928bd5ae956b54adeb44245f36ee2ceee3eb3e9646c45363b
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ ## Installation
2
+
3
+ Add this line to your application's Gemfile:
4
+
5
+ ```ruby
6
+ gem 'ci-queue'
7
+ ```
8
+
9
+ And then execute:
10
+
11
+ $ bundle
12
+
13
+ Or install it yourself as:
14
+
15
+ $ gem install ci-queue
16
+
17
+ ## Usage
18
+
19
+ ### Supported CI providers
20
+
21
+ `ci-queue` automatically infers most of its configuration if ran on one of the following CI providers:
22
+
23
+ - Buildkite
24
+ - CircleCI
25
+ - Travis
26
+
27
+ If you are using another CI system, please refer to the command usage message.
28
+
29
+ ### Minitest
30
+
31
+ Assuming you use one of the supported CI providers, the command can be as simple as:
32
+
33
+ ```bash
34
+ minitest-queue --queue redis://example.com run -Itest test/**/*_test.rb
35
+ ```
36
+
37
+ Additionally you can configure the requeue settings (see main README) with `--max-requeues` and `--requeue-tolerance`.
38
+
39
+
40
+ If you'd like to centralize the error reporting you can do so with:
41
+
42
+ ```
43
+ minitest-queue --queue redis://example.com --timeout 600 report
44
+ ```
45
+
46
+ ### RSpec
47
+
48
+ The RSpec integration is not implemented yet.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/queue/runner'
4
+ Minitest::Queue::Runner.invoke(ARGV)
data/lib/ci/queue.rb CHANGED
@@ -1,3 +1,31 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+
1
4
  require 'ci/queue/version'
5
+ require 'ci/queue/output_helpers'
6
+ require 'ci/queue/index'
7
+ require 'ci/queue/configuration'
2
8
  require 'ci/queue/static'
3
9
  require 'ci/queue/file'
10
+
11
+ module CI
12
+ module Queue
13
+ extend self
14
+
15
+ def from_uri(url, config)
16
+ uri = URI(url)
17
+ implementation = case uri.scheme
18
+ when 'list'
19
+ Static
20
+ when 'file', nil
21
+ File
22
+ when 'redis'
23
+ require 'ci/queue/redis'
24
+ Redis
25
+ else
26
+ raise ArgumentError, "Don't know how to handle #{uri.scheme} URLs"
27
+ end
28
+ implementation.from_uri(uri, config)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ module CI
2
+ module Queue
3
+ class Configuration
4
+ attr_accessor :timeout, :build_id, :worker_id, :max_requeues, :requeue_tolerance, :namespace, :seed
5
+
6
+ class << self
7
+ def from_env(env)
8
+ new(
9
+ build_id: env['CIRCLE_BUILD_URL'] || env['BUILDKITE_BUILD_ID'] || env['TRAVIS_BUILD_ID'],
10
+ worker_id: env['CIRCLE_NODE_INDEX'] || env['BUILDKITE_PARALLEL_JOB'],
11
+ seed: env['CIRCLE_SHA1'] || env['BUILDKITE_COMMIT'] || env['TRAVIS_COMMIT'],
12
+ )
13
+ end
14
+ end
15
+
16
+ def initialize(
17
+ timeout: 30, build_id: nil, worker_id: nil, max_requeues: 0, requeue_tolerance: 0,
18
+ namespace: nil, seed: nil
19
+ )
20
+ @namespace = namespace
21
+ @timeout = timeout
22
+ @build_id = build_id
23
+ @worker_id = worker_id
24
+ @max_requeues = max_requeues
25
+ @requeue_tolerance = requeue_tolerance
26
+ @seed = seed
27
+ end
28
+
29
+ def seed
30
+ @seed || build_id
31
+ end
32
+
33
+ def build_id
34
+ if namespace
35
+ "#{namespace}:#{@build_id}"
36
+ else
37
+ @build_id
38
+ end
39
+ end
40
+
41
+ def global_max_requeues(tests_count)
42
+ (tests_count * Float(requeue_tolerance)).ceil
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/ci/queue/file.rb CHANGED
@@ -3,8 +3,14 @@ require 'ci/queue/static'
3
3
  module CI
4
4
  module Queue
5
5
  class File < Static
6
- def initialize(path, **args)
7
- super(::File.readlines(path).map(&:strip).reject(&:empty?), **args)
6
+ class << self
7
+ def from_uri(uri, config)
8
+ new(uri.path, config)
9
+ end
10
+ end
11
+
12
+ def initialize(path, *args)
13
+ super(::File.readlines(path).map(&:strip).reject(&:empty?), *args)
8
14
  end
9
15
  end
10
16
  end
@@ -0,0 +1,20 @@
1
+ module CI
2
+ module Queue
3
+ class Index
4
+ def initialize(objects, &indexer)
5
+ @index = objects.map { |o| [indexer.call(o), o] }.to_h
6
+ @indexer = indexer
7
+ end
8
+
9
+ def fetch(key)
10
+ @index.fetch(key)
11
+ end
12
+
13
+ def key(value)
14
+ key = @indexer.call(value)
15
+ raise KeyError, "value not found: #{value.inspect}" unless @index.key?(key)
16
+ key
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ require 'ansi'
2
+
3
+ module CI
4
+ module Queue
5
+ module OutputHelpers
6
+ include ANSI::Code
7
+
8
+ private
9
+
10
+ def step(*args)
11
+ ci_provider.step(*args)
12
+ end
13
+
14
+ def reopen_previous_step
15
+ ci_provider.reopen_previous_step
16
+ end
17
+
18
+ def close_previous_step
19
+ ci_provider.close_previous_step
20
+ end
21
+
22
+ def ci_provider
23
+ @ci_provider ||= if ENV['BUILDKITE']
24
+ BuildkiteOutput
25
+ else
26
+ DefaultOutput
27
+ end
28
+ end
29
+
30
+ module DefaultOutput
31
+ extend self
32
+
33
+ def step(title, collapsed: true)
34
+ puts title
35
+ end
36
+
37
+ def reopen_previous_step
38
+ # noop
39
+ end
40
+
41
+ def close_previous_step
42
+ # noop
43
+ end
44
+ end
45
+
46
+ module BuildkiteOutput
47
+ extend self
48
+
49
+ def step(title, collapsed: true)
50
+ prefix = collapsed ? '---' : '+++'
51
+ puts "#{prefix} #{title}"
52
+ end
53
+
54
+ def reopen_previous_step
55
+ puts '^^^ +++'
56
+ end
57
+
58
+ def close_previous_step
59
+ puts '^^^ ---'
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -10,8 +10,15 @@ module CI
10
10
  Error = Class.new(StandardError)
11
11
  LostMaster = Class.new(Error)
12
12
 
13
- def self.new(*args)
14
- Worker.new(*args)
13
+ class << self
14
+
15
+ def new(*args)
16
+ Worker.new(*args)
17
+ end
18
+
19
+ def from_uri(uri, config)
20
+ new(uri.to_s, config)
21
+ end
15
22
  end
16
23
  end
17
24
  end
@@ -2,13 +2,14 @@ module CI
2
2
  module Queue
3
3
  module Redis
4
4
  class Base
5
- def initialize(redis:, build_id:)
6
- @redis = redis
7
- @build_id = build_id
5
+ def initialize(redis_url, config)
6
+ @redis_url = redis_url
7
+ @redis = ::Redis.new(url: redis_url)
8
+ @config = config
8
9
  end
9
10
 
10
- def empty?
11
- size == 0
11
+ def exhausted?
12
+ queue_initialized? && size == 0
12
13
  end
13
14
 
14
15
  def size
@@ -22,7 +23,7 @@ module CI
22
23
  redis.multi do
23
24
  redis.lrange(key('queue'), 0, -1)
24
25
  redis.zrange(key('running'), 0, -1)
25
- end.flatten.reverse
26
+ end.flatten.reverse.map { |k| index.fetch(k) }
26
27
  end
27
28
 
28
29
  def progress
@@ -32,8 +33,7 @@ module CI
32
33
  def wait_for_master(timeout: 10)
33
34
  return true if master?
34
35
  (timeout * 10 + 1).to_i.times do
35
- case master_status
36
- when 'ready', 'finished'
36
+ if queue_initialized?
37
37
  return true
38
38
  else
39
39
  sleep 0.1
@@ -46,14 +46,25 @@ module CI
46
46
  redis.scard(key('workers'))
47
47
  end
48
48
 
49
+ def queue_initialized?
50
+ @queue_initialized ||= begin
51
+ status = master_status
52
+ status == 'ready' || status == 'finished'
53
+ end
54
+ end
55
+
49
56
  private
50
57
 
51
- attr_reader :redis, :build_id
58
+ attr_reader :redis, :config, :redis_url
52
59
 
53
60
  def key(*args)
54
61
  ['build', build_id, *args].join(':')
55
62
  end
56
63
 
64
+ def build_id
65
+ config.build_id
66
+ end
67
+
57
68
  def master_status
58
69
  redis.get(key('master-status'))
59
70
  end
@@ -2,27 +2,27 @@ module CI
2
2
  module Queue
3
3
  module Redis
4
4
  class Retry < Static
5
- def initialize(tests, redis:, build_id:, worker_id:, **args)
5
+ def initialize(tests, config, redis:)
6
6
  @redis = redis
7
- @build_id = build_id
8
- @worker_id = worker_id
9
- super(tests, **args)
7
+ super(tests, config)
10
8
  end
11
9
 
12
10
  def minitest_reporters
11
+ require 'minitest/reporters/queue_reporter'
13
12
  require 'minitest/reporters/redis_reporter'
14
13
  @minitest_reporters ||= [
14
+ Minitest::Reporters::QueueReporter.new,
15
15
  Minitest::Reporters::RedisReporter::Worker.new(
16
16
  redis: redis,
17
- build_id: build_id,
18
- worker_id: worker_id,
17
+ build_id: config.build_id,
18
+ worker_id: config.worker_id,
19
19
  )
20
20
  ]
21
21
  end
22
22
 
23
23
  private
24
24
 
25
- attr_reader :redis, :build_id, :worker_id
25
+ attr_reader :redis
26
26
  end
27
27
  end
28
28
  end
@@ -6,11 +6,27 @@ module CI
6
6
  false
7
7
  end
8
8
 
9
+ def minitest_reporters
10
+ require 'minitest/reporters/redis_reporter'
11
+ @reporters ||= [
12
+ Minitest::Reporters::RedisReporter::Summary.new(
13
+ build_id: build_id,
14
+ redis: redis,
15
+ )
16
+ ]
17
+ end
18
+
9
19
  def wait_for_workers
10
- return false unless wait_for_master
20
+ return false unless wait_for_master(timeout: config.timeout)
11
21
 
12
- sleep 0.1 until empty?
13
- true
22
+ time_left = config.timeout
23
+ until exhausted? || time_left <= 0
24
+ sleep 0.1
25
+ time_left -= 0.1
26
+ end
27
+ exhausted?
28
+ rescue CI::Queue::Redis::LostMaster
29
+ false
14
30
  end
15
31
  end
16
32
  end
@@ -13,15 +13,20 @@ module CI
13
13
  class Worker < Base
14
14
  attr_reader :total
15
15
 
16
- def initialize(tests, redis:, build_id:, worker_id:, timeout:, max_requeues: 0, requeue_tolerance: 0.0)
16
+ def initialize(redis, config)
17
17
  @reserved_test = nil
18
- @max_requeues = max_requeues
19
- @global_max_requeues = (tests.size * requeue_tolerance).ceil
20
18
  @shutdown_required = false
21
- super(redis: redis, build_id: build_id)
22
- @worker_id = worker_id.to_s
23
- @timeout = timeout
24
- push(tests)
19
+ super(redis, config)
20
+ end
21
+
22
+ def populate(tests, &indexer)
23
+ @index = Index.new(tests, &indexer)
24
+ push(tests.map { |t| index.key(t) })
25
+ self
26
+ end
27
+
28
+ def populated?
29
+ !!defined?(@index)
25
30
  end
26
31
 
27
32
  def shutdown!
@@ -38,9 +43,9 @@ module CI
38
43
 
39
44
  def poll
40
45
  wait_for_master
41
- until shutdown_required? || empty?
46
+ until shutdown_required? || exhausted?
42
47
  if test = reserve
43
- yield test
48
+ yield index.fetch(test)
44
49
  else
45
50
  sleep 0.05
46
51
  end
@@ -48,19 +53,20 @@ module CI
48
53
  rescue ::Redis::BaseConnectionError
49
54
  end
50
55
 
51
- def retry_queue(**args)
52
- Retry.new(
53
- redis.lrange(key('worker', worker_id, 'queue'), 0, -1).reverse.uniq,
54
- redis: redis,
55
- build_id: build_id,
56
- worker_id: worker_id,
57
- **args
58
- )
56
+ def retry_queue
57
+ log = redis.lrange(key('worker', worker_id, 'queue'), 0, -1).reverse.uniq
58
+ Retry.new(log, config, redis: redis)
59
+ end
60
+
61
+ def supervisor
62
+ Supervisor.new(redis_url, config)
59
63
  end
60
64
 
61
65
  def minitest_reporters
66
+ require 'minitest/reporters/queue_reporter'
62
67
  require 'minitest/reporters/redis_reporter'
63
68
  @minitest_reporters ||= [
69
+ Minitest::Reporters::QueueReporter.new,
64
70
  Minitest::Reporters::RedisReporter::Worker.new(
65
71
  redis: redis,
66
72
  build_id: build_id,
@@ -70,30 +76,40 @@ module CI
70
76
  end
71
77
 
72
78
  def acknowledge(test)
73
- raise_on_mismatching_test(test)
79
+ test_key = index.key(test)
80
+ raise_on_mismatching_test(test_key)
74
81
  eval_script(
75
82
  :acknowledge,
76
83
  keys: [key('running'), key('processed')],
77
- argv: [test],
84
+ argv: [test_key],
78
85
  ) == 1
79
86
  end
80
87
 
81
88
  def requeue(test, offset: Redis.requeue_offset)
82
- raise_on_mismatching_test(test)
89
+ test_key = index.key(test)
90
+ raise_on_mismatching_test(test_key)
83
91
 
84
92
  requeued = eval_script(
85
93
  :requeue,
86
94
  keys: [key('processed'), key('requeues-count'), key('queue'), key('running')],
87
- argv: [max_requeues, global_max_requeues, test, offset],
95
+ argv: [config.max_requeues, config.global_max_requeues(total), test_key, offset],
88
96
  ) == 1
89
97
 
90
- @reserved_test = test unless requeued
98
+ @reserved_test = test_key unless requeued
91
99
  requeued
92
100
  end
93
101
 
94
102
  private
95
103
 
96
- attr_reader :worker_id, :timeout, :max_requeues, :global_max_requeues
104
+ attr_reader :index
105
+
106
+ def worker_id
107
+ config.worker_id
108
+ end
109
+
110
+ def timeout
111
+ config.timeout
112
+ end
97
113
 
98
114
  def raise_on_mismatching_test(test)
99
115
  if @reserved_test == test
@@ -112,9 +128,6 @@ module CI
112
128
  @reserved_test = (try_to_reserve_lost_test || try_to_reserve_test)
113
129
  end
114
130
 
115
- RESERVE_TEST = %{
116
- }
117
-
118
131
  def try_to_reserve_test
119
132
  eval_script(
120
133
  :reserve,
@@ -133,6 +146,7 @@ module CI
133
146
 
134
147
  def push(tests)
135
148
  @total = tests.size
149
+
136
150
  if @master = redis.setnx(key('master-status'), 'setup')
137
151
  redis.multi do
138
152
  redis.lpush(key('queue'), tests)
@@ -1,18 +1,48 @@
1
1
  module CI
2
2
  module Queue
3
3
  class Static
4
+ class << self
5
+ def from_uri(uri, config)
6
+ tests = uri.opaque.split(':').map { |t| CGI.unescape(t) }
7
+ new(tests, config)
8
+ end
9
+ end
10
+
4
11
  attr_reader :progress, :total
5
12
 
6
- def initialize(tests, max_requeues: 0, requeue_tolerance: 0.0)
13
+ def initialize(tests, config)
7
14
  @queue = tests
15
+ @config = config
8
16
  @progress = 0
9
17
  @total = tests.size
10
- @max_requeues = max_requeues
11
- @global_max_requeues = (tests.size * requeue_tolerance).ceil
18
+ end
19
+
20
+ def minitest_reporters
21
+ require 'minitest/reporters/queue_reporter'
22
+ @minitest_reporters ||= [
23
+ Minitest::Reporters::QueueReporter.new,
24
+ ]
25
+ end
26
+
27
+ def supervisor
28
+ raise NotImplementedError, "This type of queue can't be supervised"
29
+ end
30
+
31
+ def retry_queue
32
+ self
33
+ end
34
+
35
+ def populate(tests, &indexer)
36
+ @index = Index.new(tests, &indexer)
37
+ self
38
+ end
39
+
40
+ def populated?
41
+ !!defined?(@index)
12
42
  end
13
43
 
14
44
  def to_a
15
- @queue.dup
45
+ @queue.map { |i| index.fetch(i) }
16
46
  end
17
47
 
18
48
  def size
@@ -21,12 +51,12 @@ module CI
21
51
 
22
52
  def poll
23
53
  while test = @queue.shift
24
- yield test
54
+ yield index.fetch(test)
25
55
  @progress += 1
26
56
  end
27
57
  end
28
58
 
29
- def empty?
59
+ def exhausted?
30
60
  @queue.empty?
31
61
  end
32
62
 
@@ -35,18 +65,19 @@ module CI
35
65
  end
36
66
 
37
67
  def requeue(test)
38
- return false unless should_requeue?(test)
39
- requeues[test] += 1
40
- @queue.unshift(test)
68
+ key = index.key(test)
69
+ return false unless should_requeue?(key)
70
+ requeues[key] += 1
71
+ @queue.unshift(index.key(test))
41
72
  true
42
73
  end
43
74
 
44
75
  private
45
76
 
46
- attr_reader :max_requeues, :global_max_requeues
77
+ attr_reader :index, :config
47
78
 
48
- def should_requeue?(test)
49
- requeues[test] < max_requeues && requeues.values.inject(0, :+) < global_max_requeues
79
+ def should_requeue?(key)
80
+ requeues[key] < config.max_requeues && requeues.values.inject(0, :+) < config.global_max_requeues(total)
50
81
  end
51
82
 
52
83
  def requeues
@@ -1,6 +1,6 @@
1
1
  module CI
2
2
  module Queue
3
- VERSION = '0.6.0'
3
+ VERSION = '0.7.0'
4
4
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
5
5
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
6
6
  end
@@ -65,7 +65,7 @@ module Minitest
65
65
  SuiteNotFound = Class.new(StandardError)
66
66
 
67
67
  def loaded_tests
68
- MiniTest::Test.runnables.flat_map do |suite|
68
+ Minitest::Test.runnables.flat_map do |suite|
69
69
  suite.runnable_methods.map do |method|
70
70
  "#{suite}##{method}"
71
71
  end
@@ -0,0 +1,243 @@
1
+ require 'optparse'
2
+ require 'minitest/queue'
3
+ require 'ci/queue'
4
+ require 'digest/md5'
5
+
6
+ module Minitest
7
+ module Queue
8
+ class Runner
9
+ include ::CI::Queue::OutputHelpers
10
+
11
+ Error = Class.new(StandardError)
12
+ MissingParameter = Class.new(Error)
13
+
14
+ def self.invoke(argv)
15
+ new(argv).run!
16
+ end
17
+
18
+ def initialize(argv)
19
+ @queue_config = CI::Queue::Configuration.from_env(ENV)
20
+ @command, @argv = parse(argv)
21
+ end
22
+
23
+ def run!
24
+ invalid_usage!("No command given") if command.nil?
25
+ invalid_usage!('Missing queue URL') unless queue_url
26
+
27
+ @queue = CI::Queue.from_uri(queue_url, queue_config)
28
+
29
+ method = "#{command}_command"
30
+ if respond_to?(method)
31
+ public_send(method)
32
+ else
33
+ invalid_usage!("Unknown command: #{command}")
34
+ end
35
+ end
36
+
37
+ def retry_command
38
+ self.queue = queue.retry_queue
39
+ run_command
40
+ end
41
+
42
+ def run_command
43
+ set_load_path
44
+ Minitest.queue = queue
45
+ trap('TERM') { Minitest.queue.shutdown! }
46
+ trap('INT') { Minitest.queue.shutdown! }
47
+ load_tests
48
+ populate_queue
49
+ # Let minitest's at_exit hook trigger
50
+ end
51
+
52
+ def report_command
53
+ supervisor = begin
54
+ queue.supervisor
55
+ rescue NotImplementedError => error
56
+ abort! error.message
57
+ end
58
+
59
+ step("Waiting for workers to complete")
60
+
61
+ unless supervisor.wait_for_workers
62
+ unless supervisor.queue_initialized?
63
+ abort! "No master was elected. Did all workers crash?"
64
+ end
65
+
66
+ unless supervisor.exhausted?
67
+ abort! "#{supervisor.size} tests weren't run."
68
+ end
69
+ end
70
+
71
+ success = supervisor.minitest_reporters.all?(&:success?)
72
+ supervisor.minitest_reporters.each do |reporter|
73
+ reporter.report
74
+ end
75
+
76
+ STDOUT.flush
77
+ exit! success ? 0 : 1
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :queue_config, :options, :command, :argv
83
+ attr_accessor :queue, :queue_url, :load_paths
84
+
85
+ def populate_queue
86
+ Minitest.queue.populate(shuffle(Minitest.loaded_tests), &:to_s) # TODO: stop serializing
87
+ end
88
+
89
+ def set_load_path
90
+ if paths = load_paths
91
+ paths.split(':').reverse.each do |path|
92
+ $LOAD_PATH.unshift(File.expand_path(path))
93
+ end
94
+ end
95
+ end
96
+
97
+ def load_tests
98
+ argv.sort.each do |f|
99
+ require File.expand_path(f)
100
+ end
101
+ end
102
+
103
+ def parse(argv)
104
+ parser.parse!(argv)
105
+ command = argv.shift
106
+ return command, argv
107
+ end
108
+
109
+ def parser
110
+ @parser ||= OptionParser.new do |opts|
111
+ opts.banner = "Usage: minitest-queue [options] COMMAND [ARGS]"
112
+
113
+ opts.separator ""
114
+ opts.separator "Example: minitest-queue -Itest --queue redis://example.com run test/**/*_test.rb"
115
+
116
+ opts.separator ""
117
+ opts.separator "GLOBAL OPTIONS"
118
+
119
+
120
+ help = split_heredoc(<<-EOS)
121
+ URL of the queue, e.g. redis://example.com.
122
+ Defaults to $CI_QUEUE_URL if set.
123
+ EOS
124
+ opts.separator ""
125
+ opts.on('--queue URL', *help) do |url|
126
+ self.queue_url = url
127
+ end
128
+
129
+ help = split_heredoc(<<-EOS)
130
+ Unique identifier for the workload. All workers working on the same suite of tests must have the same build identifier.
131
+ If the build is tried again, or another revision is built, this value must be different.
132
+ It's automatically inferred on Buildkite, CircleCI and Travis.
133
+ EOS
134
+ opts.separator ""
135
+ opts.on('--build BUILD_ID', *help) do |build_id|
136
+ queue_config.build_id = build_id
137
+ end
138
+
139
+ help = split_heredoc(<<-EOS)
140
+ Optional. Sets a prefix for the build id in case a single CI build runs multiple independent test suites.
141
+ Example: --namespace integration
142
+ EOS
143
+ opts.separator ""
144
+ opts.on('--namespace NAMESPACE', *help) do |namespace|
145
+ queue_config.namespace = namespace
146
+ end
147
+
148
+ opts.separator ""
149
+ opts.separator "COMMANDS"
150
+ opts.separator ""
151
+ opts.separator " run [TEST_FILES...]: Participate in leader election, and then work off the test queue."
152
+
153
+ help = split_heredoc(<<-EOS)
154
+ Specify a timeout after which if a test haven't completed, it will be picked up by another worker.
155
+ It is very important to set this vlaue higher than the slowest test in the suite, otherwise performance will be impacted.
156
+ Defaults to 30 seconds.
157
+ EOS
158
+ opts.separator ""
159
+ opts.on('--timeout TIMEOUT', *help) do |timeout|
160
+ queue_config.timeout = Float(timeout)
161
+ end
162
+
163
+ help = split_heredoc(<<-EOS)
164
+ Specify $LOAD_PATH directory, similar to Ruby's -I
165
+ EOS
166
+ opts.separator ""
167
+ opts.on('-IPATHS', *help) do |paths|
168
+ self.load_paths = paths
169
+ end
170
+
171
+ help = split_heredoc(<<-EOS)
172
+ Sepcify a seed used to shuffle the test suite.
173
+ On Buildkite, CircleCI and Travis, the commit revision will be used by default.
174
+ EOS
175
+ opts.separator ""
176
+ opts.on('--seed SEED', *help) do |seed|
177
+ queue_config.seed = seed
178
+ end
179
+
180
+ help = split_heredoc(<<-EOS)
181
+ A unique identifier for this worker, It must be consistent to allow retries.
182
+ If not specified, retries won't be available.
183
+ It's automatically inferred on Buildkite and CircleCI.
184
+ EOS
185
+ opts.separator ""
186
+ opts.on('--worker WORKER_ID', *help) do |worker_id|
187
+ queue_config.worker_id = worker_id
188
+ end
189
+
190
+ help = split_heredoc(<<-EOS)
191
+ Defines how many time a single test can be requeued.
192
+ Defaults to 0.
193
+ EOS
194
+ opts.separator ""
195
+ opts.on('--max-requeues MAX') do |max|
196
+ queue_config.max_requeues = Integer(max)
197
+ end
198
+
199
+ help = split_heredoc(<<-EOS)
200
+ Defines how many requeues can happen overall, based on the test suite size. e.g 0.05 for 5%.
201
+ Defaults to 0.
202
+ EOS
203
+ opts.separator ""
204
+ opts.on('--requeue-tolerance RATIO', *help) do |ratio|
205
+ queue_config.requeue_tolerance = Float(ratio)
206
+ end
207
+
208
+ opts.separator ""
209
+ opts.separator " retry: Replays a previous run in the same order."
210
+
211
+ opts.separator ""
212
+ opts.separator " report: Wait for all workers to complete and summarize the test failures."
213
+ end
214
+ end
215
+
216
+ def split_heredoc(string)
217
+ string.lines.map(&:strip)
218
+ end
219
+
220
+ def shuffle(tests)
221
+ random = Random.new(Digest::MD5.hexdigest(queue_config.seed).to_i(16))
222
+ tests.shuffle(random: random)
223
+ end
224
+
225
+ def queue_url
226
+ @queue_url || ENV['CI_QUEUE_URL']
227
+ end
228
+
229
+ def invalid_usage!(message)
230
+ reopen_previous_step
231
+ puts red(message)
232
+ puts parser
233
+ exit! 1 # exit! is required to avoid minitest at_exit callback
234
+ end
235
+
236
+ def abort!(message)
237
+ reopen_previous_step
238
+ puts red(message)
239
+ exit! 1 # exit! is required to avoid minitest at_exit callback
240
+ end
241
+ end
242
+ end
243
+ end
@@ -1,4 +1,3 @@
1
- require 'ansi'
2
1
  require 'delegate'
3
2
 
4
3
  module Minitest
@@ -1,9 +1,10 @@
1
+ require 'ci/queue/output_helpers'
1
2
  require 'minitest/reporters'
2
3
 
3
4
  module Minitest
4
5
  module Reporters
5
6
  class QueueReporter < BaseReporter
6
- include ANSI::Code
7
+ include ::CI::Queue::OutputHelpers
7
8
  attr_accessor :requeues
8
9
 
9
10
  def initialize(*)
@@ -20,9 +21,10 @@ module Minitest
20
21
  private
21
22
 
22
23
  def print_report
24
+ reopen_previous_step if failures > 0 || errors > 0
23
25
  success = failures.zero? && errors.zero?
24
26
  failures_count = "#{failures} failures, #{errors} errors,"
25
- puts [
27
+ step [
26
28
  'Ran %d tests, %d assertions,' % [count, assertions],
27
29
  success ? green(failures_count) : red(failures_count),
28
30
  yellow("#{skips} skips, #{requeues} requeues"),
@@ -89,16 +89,20 @@ module Minitest
89
89
  end
90
90
 
91
91
  class Summary < Base
92
- include ANSI::Code
92
+ include ::CI::Queue::OutputHelpers
93
93
 
94
- def report(io: STDOUT)
95
- io.puts aggregates
94
+ def report
95
+ puts aggregates
96
96
  errors = error_reports
97
- io.puts errors
97
+ puts errors
98
98
 
99
99
  errors.empty?
100
100
  end
101
101
 
102
+ def success?
103
+ errors == 0 && failures == 0
104
+ end
105
+
102
106
  def record(*)
103
107
  raise NotImplementedError
104
108
  end
@@ -133,12 +137,12 @@ module Minitest
133
137
  success = failures.zero? && errors.zero?
134
138
  failures_count = "#{failures} failures, #{errors} errors,"
135
139
 
136
- [
140
+ step([
137
141
  'Ran %d tests, %d assertions,' % [processed, assertions],
138
142
  success ? green(failures_count) : red(failures_count),
139
143
  yellow("#{skips} skips, #{requeues} requeues"),
140
144
  'in %.2fs (aggregated)' % total_time,
141
- ].join(' ')
145
+ ].join(' '), collapsed: success)
142
146
  end
143
147
 
144
148
  def fetch_summary
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.6.0
4
+ version: 0.7.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: 2017-08-30 00:00:00.000000000 Z
11
+ date: 2017-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -97,12 +97,14 @@ dependencies:
97
97
  description: To parallelize your CI without having to balance your tests
98
98
  email:
99
99
  - jean.boussier@shopify.com
100
- executables: []
100
+ executables:
101
+ - minitest-queue
101
102
  extensions: []
102
103
  extra_rdoc_files: []
103
104
  files:
104
105
  - ".gitignore"
105
106
  - Gemfile
107
+ - README.md
106
108
  - Rakefile
107
109
  - bin/bundler
108
110
  - bin/console
@@ -110,8 +112,12 @@ files:
110
112
  - bin/setup
111
113
  - ci-queue.gemspec
112
114
  - dev.yml
115
+ - exe/minitest-queue
113
116
  - lib/ci/queue.rb
117
+ - lib/ci/queue/configuration.rb
114
118
  - lib/ci/queue/file.rb
119
+ - lib/ci/queue/index.rb
120
+ - lib/ci/queue/output_helpers.rb
115
121
  - lib/ci/queue/redis.rb
116
122
  - lib/ci/queue/redis/acknowledge.lua
117
123
  - lib/ci/queue/redis/base.rb
@@ -124,6 +130,7 @@ files:
124
130
  - lib/ci/queue/static.rb
125
131
  - lib/ci/queue/version.rb
126
132
  - lib/minitest/queue.rb
133
+ - lib/minitest/queue/runner.rb
127
134
  - lib/minitest/reporters/failure_formatter.rb
128
135
  - lib/minitest/reporters/order_reporter.rb
129
136
  - lib/minitest/reporters/queue_reporter.rb
@@ -148,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
155
  version: '0'
149
156
  requirements: []
150
157
  rubyforge_project:
151
- rubygems_version: 2.6.13
158
+ rubygems_version: 2.6.10
152
159
  signing_key:
153
160
  specification_version: 4
154
161
  summary: Distribute tests over many workers using a queue