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 +4 -4
- data/README.md +48 -0
- data/exe/minitest-queue +4 -0
- data/lib/ci/queue.rb +28 -0
- data/lib/ci/queue/configuration.rb +46 -0
- data/lib/ci/queue/file.rb +8 -2
- data/lib/ci/queue/index.rb +20 -0
- data/lib/ci/queue/output_helpers.rb +64 -0
- data/lib/ci/queue/redis.rb +9 -2
- data/lib/ci/queue/redis/base.rb +20 -9
- data/lib/ci/queue/redis/retry.rb +7 -7
- data/lib/ci/queue/redis/supervisor.rb +19 -3
- data/lib/ci/queue/redis/worker.rb +40 -26
- data/lib/ci/queue/static.rb +43 -12
- data/lib/ci/queue/version.rb +1 -1
- data/lib/minitest/queue.rb +1 -1
- data/lib/minitest/queue/runner.rb +243 -0
- data/lib/minitest/reporters/failure_formatter.rb +0 -1
- data/lib/minitest/reporters/queue_reporter.rb +4 -2
- data/lib/minitest/reporters/redis_reporter.rb +10 -6
- metadata +11 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 97f69595ec09509df32703213fd2d882db14f017
         | 
| 4 | 
            +
              data.tar.gz: cae08e13e586bd5c7cc8927057225084b0ee0d74
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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.
         | 
    
        data/exe/minitest-queue
    ADDED
    
    
    
        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 | 
            -
                   | 
| 7 | 
            -
                     | 
| 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
         | 
    
        data/lib/ci/queue/redis.rb
    CHANGED
    
    | @@ -10,8 +10,15 @@ module CI | |
| 10 10 | 
             
                  Error = Class.new(StandardError)
         | 
| 11 11 | 
             
                  LostMaster = Class.new(Error)
         | 
| 12 12 |  | 
| 13 | 
            -
                   | 
| 14 | 
            -
             | 
| 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
         | 
    
        data/lib/ci/queue/redis/base.rb
    CHANGED
    
    | @@ -2,13 +2,14 @@ module CI | |
| 2 2 | 
             
              module Queue
         | 
| 3 3 | 
             
                module Redis
         | 
| 4 4 | 
             
                  class Base
         | 
| 5 | 
            -
                    def initialize( | 
| 6 | 
            -
                      @ | 
| 7 | 
            -
                      @ | 
| 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  | 
| 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 | 
            -
                         | 
| 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, : | 
| 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
         | 
    
        data/lib/ci/queue/redis/retry.rb
    CHANGED
    
    | @@ -2,27 +2,27 @@ module CI | |
| 2 2 | 
             
              module Queue
         | 
| 3 3 | 
             
                module Redis
         | 
| 4 4 | 
             
                  class Retry < Static
         | 
| 5 | 
            -
                    def initialize(tests, redis | 
| 5 | 
            +
                    def initialize(tests, config, redis:)
         | 
| 6 6 | 
             
                      @redis = redis
         | 
| 7 | 
            -
                       | 
| 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 | 
| 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 | 
            -
                       | 
| 13 | 
            -
                       | 
| 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( | 
| 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 | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 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? ||  | 
| 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 | 
| 52 | 
            -
                       | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 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 | 
            -
                       | 
| 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: [ | 
| 84 | 
            +
                        argv: [test_key],
         | 
| 78 85 | 
             
                      ) == 1
         | 
| 79 86 | 
             
                    end
         | 
| 80 87 |  | 
| 81 88 | 
             
                    def requeue(test, offset: Redis.requeue_offset)
         | 
| 82 | 
            -
                       | 
| 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,  | 
| 95 | 
            +
                        argv: [config.max_requeues, config.global_max_requeues(total), test_key, offset],
         | 
| 88 96 | 
             
                      ) == 1
         | 
| 89 97 |  | 
| 90 | 
            -
                      @reserved_test =  | 
| 98 | 
            +
                      @reserved_test = test_key unless requeued
         | 
| 91 99 | 
             
                      requeued
         | 
| 92 100 | 
             
                    end
         | 
| 93 101 |  | 
| 94 102 | 
             
                    private
         | 
| 95 103 |  | 
| 96 | 
            -
                    attr_reader : | 
| 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)
         | 
    
        data/lib/ci/queue/static.rb
    CHANGED
    
    | @@ -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,  | 
| 13 | 
            +
                  def initialize(tests, config)
         | 
| 7 14 | 
             
                    @queue = tests
         | 
| 15 | 
            +
                    @config = config
         | 
| 8 16 | 
             
                    @progress = 0
         | 
| 9 17 | 
             
                    @total = tests.size
         | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 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. | 
| 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  | 
| 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 | 
            -
                     | 
| 39 | 
            -
                     | 
| 40 | 
            -
                     | 
| 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 : | 
| 77 | 
            +
                  attr_reader :index, :config
         | 
| 47 78 |  | 
| 48 | 
            -
                  def should_requeue?( | 
| 49 | 
            -
                    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
         | 
    
        data/lib/ci/queue/version.rb
    CHANGED
    
    
    
        data/lib/minitest/queue.rb
    CHANGED
    
    
| @@ -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,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  | 
| 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 | 
            -
                     | 
| 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  | 
| 92 | 
            +
                    include ::CI::Queue::OutputHelpers
         | 
| 93 93 |  | 
| 94 | 
            -
                    def report | 
| 95 | 
            -
                       | 
| 94 | 
            +
                    def report
         | 
| 95 | 
            +
                      puts aggregates
         | 
| 96 96 | 
             
                      errors = error_reports
         | 
| 97 | 
            -
                       | 
| 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. | 
| 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- | 
| 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. | 
| 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
         |