ci-queue 0.6.0 → 0.7.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
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