rspec-sharder 0.0.1
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 +7 -0
 - data/Gemfile +11 -0
 - data/Gemfile.lock +36 -0
 - data/LICENSE +21 -0
 - data/README.md +26 -0
 - data/bin/rspec-sharder +75 -0
 - data/lib/rspec-sharder/version.rb +7 -0
 - data/lib/rspec-sharder.rb +261 -0
 - data/rspec-sharder.gemspec +29 -0
 - data/scripts/release.sh +26 -0
 - metadata +70 -0
 
    
        checksums.yaml
    ADDED
    
    | 
         @@ -0,0 +1,7 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            ---
         
     | 
| 
      
 2 
     | 
    
         
            +
            SHA256:
         
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 6d525960fabd4fd6d3c83ce963601ca0552bbcb15e11c212cdf872de2cd71a36
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 5c7591851f241b1ab2b017ad81f3488bd5ac6dea21c1978df4e2cb963bd17bf2
         
     | 
| 
      
 5 
     | 
    
         
            +
            SHA512:
         
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 1c65df78f7e5d973308808db3c48309c14d3e0abd1d7b5ccc37d1faa1d2f82b1ff98acba5a38f39947f47927cffaedf4dfe3f4b744b4cceb11959df2762b5bf7
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: db806df9aaa7f00cf7f62a2f583908a1d98864d2dd34b93a61a477c35716fa55f3f42c2cf9f22b9be49474e0d164f01776f2d1d8fbe4ce7ecbc05c9ebb3ab9dd
         
     | 
    
        data/Gemfile
    ADDED
    
    
    
        data/Gemfile.lock
    ADDED
    
    | 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            PATH
         
     | 
| 
      
 2 
     | 
    
         
            +
              remote: .
         
     | 
| 
      
 3 
     | 
    
         
            +
              specs:
         
     | 
| 
      
 4 
     | 
    
         
            +
                rspec-sharder (0.0.1)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  rspec-core
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            GEM
         
     | 
| 
      
 8 
     | 
    
         
            +
              remote: https://rubygems.org/
         
     | 
| 
      
 9 
     | 
    
         
            +
              specs:
         
     | 
| 
      
 10 
     | 
    
         
            +
                diff-lcs (1.4.4)
         
     | 
| 
      
 11 
     | 
    
         
            +
                rspec (3.10.0)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  rspec-core (~> 3.10.0)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  rspec-expectations (~> 3.10.0)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  rspec-mocks (~> 3.10.0)
         
     | 
| 
      
 15 
     | 
    
         
            +
                rspec-core (3.10.1)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  rspec-support (~> 3.10.0)
         
     | 
| 
      
 17 
     | 
    
         
            +
                rspec-expectations (3.10.1)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  diff-lcs (>= 1.2.0, < 2.0)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  rspec-support (~> 3.10.0)
         
     | 
| 
      
 20 
     | 
    
         
            +
                rspec-mocks (3.10.2)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  diff-lcs (>= 1.2.0, < 2.0)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  rspec-support (~> 3.10.0)
         
     | 
| 
      
 23 
     | 
    
         
            +
                rspec-support (3.10.2)
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            PLATFORMS
         
     | 
| 
      
 26 
     | 
    
         
            +
              x86_64-darwin-20
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            DEPENDENCIES
         
     | 
| 
      
 29 
     | 
    
         
            +
              rspec
         
     | 
| 
      
 30 
     | 
    
         
            +
              rspec-sharder!
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            RUBY VERSION
         
     | 
| 
      
 33 
     | 
    
         
            +
               ruby 3.0.0p0
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            BUNDLED WITH
         
     | 
| 
      
 36 
     | 
    
         
            +
               2.2.21
         
     | 
    
        data/LICENSE
    ADDED
    
    | 
         @@ -0,0 +1,21 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            MIT License
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Copyright (c) 2021 Nick Dower
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         
     | 
| 
      
 6 
     | 
    
         
            +
            of this software and associated documentation files (the "Software"), to deal
         
     | 
| 
      
 7 
     | 
    
         
            +
            in the Software without restriction, including without limitation the rights
         
     | 
| 
      
 8 
     | 
    
         
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         
     | 
| 
      
 9 
     | 
    
         
            +
            copies of the Software, and to permit persons to whom the Software is
         
     | 
| 
      
 10 
     | 
    
         
            +
            furnished to do so, subject to the following conditions:
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            The above copyright notice and this permission notice shall be included in all
         
     | 
| 
      
 13 
     | 
    
         
            +
            copies or substantial portions of the Software.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         
     | 
| 
      
 16 
     | 
    
         
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         
     | 
| 
      
 17 
     | 
    
         
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         
     | 
| 
      
 18 
     | 
    
         
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         
     | 
| 
      
 19 
     | 
    
         
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         
     | 
| 
      
 20 
     | 
    
         
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         
     | 
| 
      
 21 
     | 
    
         
            +
            SOFTWARE.
         
     | 
    
        data/README.md
    ADDED
    
    | 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # RSpec Sharder
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            ```
         
     | 
| 
      
 4 
     | 
    
         
            +
            Groups specs into shards, ensuring that each shard has a similar size, and runs
         
     | 
| 
      
 5 
     | 
    
         
            +
            the specified shard.
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            Shard size is determined by summing the saved durations for each spec file in
         
     | 
| 
      
 8 
     | 
    
         
            +
            the shard. Durations are saved in .spec_durations. If a spec file is not found
         
     | 
| 
      
 9 
     | 
    
         
            +
            in .spec_durations, the duration is estimated based on the number of examples in
         
     | 
| 
      
 10 
     | 
    
         
            +
            the spec file.
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            .spec_durations is generate/updated after a successful run when --persist is
         
     | 
| 
      
 13 
     | 
    
         
            +
            specified, but only for the shard which was actually executed. To generate
         
     | 
| 
      
 14 
     | 
    
         
            +
            durations for all shards simultaneously, run with the default options of 1 total
         
     | 
| 
      
 15 
     | 
    
         
            +
            shards and --persist:
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            bundle exec rspec-sharder --persist -- [<rspec-args...>]
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            Usage: bundle exec rspec-sharder [--total-shards <num> [--shard <num>]] [--persist] -- [<rspec-args...>]
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            Options:
         
     | 
| 
      
 22 
     | 
    
         
            +
                -h, --help                       Print this message.
         
     | 
| 
      
 23 
     | 
    
         
            +
                -t, --total-shards <num>         The total number of shards. Defaults to 1.
         
     | 
| 
      
 24 
     | 
    
         
            +
                -s, --shard <num>                The shard to run. Defaults to 1.
         
     | 
| 
      
 25 
     | 
    
         
            +
                -p, --persist                    Save durations to .spec_durations.
         
     | 
| 
      
 26 
     | 
    
         
            +
            ```
         
     | 
    
        data/bin/rspec-sharder
    ADDED
    
    | 
         @@ -0,0 +1,75 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env ruby
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'optparse'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'rspec-sharder'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            def fail(message)
         
     | 
| 
      
 7 
     | 
    
         
            +
              warn message
         
     | 
| 
      
 8 
     | 
    
         
            +
              puts
         
     | 
| 
      
 9 
     | 
    
         
            +
              puts @parser.help
         
     | 
| 
      
 10 
     | 
    
         
            +
              exit 1
         
     | 
| 
      
 11 
     | 
    
         
            +
            end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            @total_shards = 1
         
     | 
| 
      
 14 
     | 
    
         
            +
            @shard_num = 1
         
     | 
| 
      
 15 
     | 
    
         
            +
            @persist = false
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            @parser = OptionParser.new do |opts|
         
     | 
| 
      
 18 
     | 
    
         
            +
              opts.banner = <<~EOF
         
     | 
| 
      
 19 
     | 
    
         
            +
                Groups specs into shards, ensuring that each shard has a similar size, and runs
         
     | 
| 
      
 20 
     | 
    
         
            +
                the specified shard.
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                Shard size is determined by summing the saved durations for each spec file in
         
     | 
| 
      
 23 
     | 
    
         
            +
                the shard. Durations are saved in .spec_durations. If a spec file is not found
         
     | 
| 
      
 24 
     | 
    
         
            +
                in .spec_durations, the duration is estimated based on the number of examples in
         
     | 
| 
      
 25 
     | 
    
         
            +
                the spec file.
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                .spec_durations is generate/updated after a successful run when --persist is
         
     | 
| 
      
 28 
     | 
    
         
            +
                specified, but only for the shard which was actually executed. To generate
         
     | 
| 
      
 29 
     | 
    
         
            +
                durations for all shards simultaneously, run with the default options of 1 total
         
     | 
| 
      
 30 
     | 
    
         
            +
                shards and --persist:
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                bundle exec rspec-sharder --persist -- [<rspec-args...>]
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                Usage: bundle exec rspec-sharder [--total-shards <num> [--shard <num>]] [--persist] -- [<rspec-args...>]
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                Options:
         
     | 
| 
      
 37 
     | 
    
         
            +
              EOF
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              opts.on('-h', '--help', "Print this message.") do
         
     | 
| 
      
 40 
     | 
    
         
            +
                puts opts
         
     | 
| 
      
 41 
     | 
    
         
            +
                exit
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              opts.on('-t', '--total-shards <num>', 'The total number of shards. Defaults to 1.') do |total_shards|
         
     | 
| 
      
 45 
     | 
    
         
            +
                begin
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @total_shards = Integer(total_shards)
         
     | 
| 
      
 47 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 48 
     | 
    
         
            +
                  fail('fatal: invalid value for --total-shards')
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              opts.on('-s', '--shard <num>', 'The shard to run. Defaults to 1.') do |shard|
         
     | 
| 
      
 53 
     | 
    
         
            +
                begin
         
     | 
| 
      
 54 
     | 
    
         
            +
                  @shard = Integer(shard)
         
     | 
| 
      
 55 
     | 
    
         
            +
                rescue ArgumentError
         
     | 
| 
      
 56 
     | 
    
         
            +
                  fail('fatal: invalid value for --shard')
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
              opts.on('-p', '--persist', 'Save durations to .spec_durations.') do
         
     | 
| 
      
 61 
     | 
    
         
            +
                @persist = true
         
     | 
| 
      
 62 
     | 
    
         
            +
              end
         
     | 
| 
      
 63 
     | 
    
         
            +
            end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            begin
         
     | 
| 
      
 66 
     | 
    
         
            +
              @parser.parse!
         
     | 
| 
      
 67 
     | 
    
         
            +
            rescue StandardError => e
         
     | 
| 
      
 68 
     | 
    
         
            +
              fail("fatal: #{e.message}")
         
     | 
| 
      
 69 
     | 
    
         
            +
            end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            fail('fatal: invalid value for --total-shards') unless @total_shards > 0
         
     | 
| 
      
 72 
     | 
    
         
            +
            fail('fatal: invalid value for --shard') unless @shard > 0
         
     | 
| 
      
 73 
     | 
    
         
            +
            fail('fatal: --shard may not be greater than --total-shards') unless @shard <= @total_shards
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
            RSpec::Sharder.run(total_shards: @total_shards, shard_num: @shard, persist: @persist, rspec_args: ARGV)
         
     | 
| 
         @@ -0,0 +1,261 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'rspec/core'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module RSpec
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Sharder
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                class ShardError < StandardError; end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def self.run(total_shards:, shard_num:, persist:, rspec_args:)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  raise "fatal: invalid total shards: #{total_shards}" unless total_shards.is_a?(Integer) && total_shards > 0
         
     | 
| 
      
 10 
     | 
    
         
            +
                  raise "fatal: invalid shard number: #{shard_num}" unless shard_num.is_a?(Integer) && shard_num > 0 && shard_num <= total_shards
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 13 
     | 
    
         
            +
                    ::RSpec::Core::ConfigurationOptions.new(rspec_args).configure(::RSpec.configuration)
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    return if ::RSpec.world.wants_to_quit
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    ::RSpec.configuration.load_spec_files
         
     | 
| 
      
 18 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 19 
     | 
    
         
            +
                    ::RSpec.world.announce_filters
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code) if ::RSpec.world.wants_to_quit
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  all_durations = load_recorded_durations
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 27 
     | 
    
         
            +
                    shards = build_shards(total_shards, shard_num, all_durations)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  rescue ShardError => e
         
     | 
| 
      
 29 
     | 
    
         
            +
                    ::RSpec.configuration.error_stream.puts e.message
         
     | 
| 
      
 30 
     | 
    
         
            +
                    exit ::RSpec.configuration.failure_exit_cod
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  print_shards(shards)
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  expected_total_duration = shards[shard_num - 1][:duration]
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  shard_file_paths = shards[shard_num - 1][:file_paths]
         
     | 
| 
      
 38 
     | 
    
         
            +
                  example_groups = ::RSpec.world.ordered_example_groups.select do |example_group|
         
     | 
| 
      
 39 
     | 
    
         
            +
                    shard_file_paths.include?(example_group.metadata[:file_path])
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
                  example_count = ::RSpec.world.example_count(example_groups)
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  new_durations = { }
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  actual_total_duration = 0
         
     | 
| 
      
 46 
     | 
    
         
            +
                  exit_code = ::RSpec.configuration.reporter.report(example_count) do |reporter|
         
     | 
| 
      
 47 
     | 
    
         
            +
                    ::RSpec.configuration.with_suite_hooks do
         
     | 
| 
      
 48 
     | 
    
         
            +
                      if example_count == 0 && ::RSpec.configuration.fail_if_no_examples
         
     | 
| 
      
 49 
     | 
    
         
            +
                        return ::RSpec.configuration.failure_exit_code
         
     | 
| 
      
 50 
     | 
    
         
            +
                      end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                      group_results = example_groups.map do |example_group|
         
     | 
| 
      
 53 
     | 
    
         
            +
                        start_time = current_time_millis
         
     | 
| 
      
 54 
     | 
    
         
            +
                        result = example_group.run(reporter)
         
     | 
| 
      
 55 
     | 
    
         
            +
                        end_time = current_time_millis
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                        file_path = example_group.metadata[:file_path]
         
     | 
| 
      
 58 
     | 
    
         
            +
                        duration = (end_time - start_time).to_i
         
     | 
| 
      
 59 
     | 
    
         
            +
                        actual_total_duration += duration
         
     | 
| 
      
 60 
     | 
    
         
            +
                        new_durations[file_path] ||= 0
         
     | 
| 
      
 61 
     | 
    
         
            +
                        new_durations[file_path] += duration
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                        result
         
     | 
| 
      
 64 
     | 
    
         
            +
                      end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                      success = group_results.all?
         
     | 
| 
      
 67 
     | 
    
         
            +
                      exit_code = success ? 0 : 1
         
     | 
| 
      
 68 
     | 
    
         
            +
                      if ::RSpec.world.non_example_failure
         
     | 
| 
      
 69 
     | 
    
         
            +
                        success = false
         
     | 
| 
      
 70 
     | 
    
         
            +
                        exit_code = ::RSpec.configuration.failure_exit_code
         
     | 
| 
      
 71 
     | 
    
         
            +
                      end
         
     | 
| 
      
 72 
     | 
    
         
            +
                      exit_code
         
     | 
| 
      
 73 
     | 
    
         
            +
                    end
         
     | 
| 
      
 74 
     | 
    
         
            +
                  end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                  # Write results to .examples file.
         
     | 
| 
      
 77 
     | 
    
         
            +
                  unless ::RSpec.configuration.dry_run
         
     | 
| 
      
 78 
     | 
    
         
            +
                    persist_example_statuses(shard_file_paths)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                  if ::RSpec.configuration.dry_run
         
     | 
| 
      
 82 
     | 
    
         
            +
                    if persist
         
     | 
| 
      
 83 
     | 
    
         
            +
                      ::RSpec.configuration.output_stream.puts <<~EOF
         
     | 
| 
      
 84 
     | 
    
         
            +
                        
         
     | 
| 
      
 85 
     | 
    
         
            +
                        Dry run. Not saving to .spec_durations.
         
     | 
| 
      
 86 
     | 
    
         
            +
                      EOF
         
     | 
| 
      
 87 
     | 
    
         
            +
                    end
         
     | 
| 
      
 88 
     | 
    
         
            +
                  else
         
     | 
| 
      
 89 
     | 
    
         
            +
                    if exit_code == 0
         
     | 
| 
      
 90 
     | 
    
         
            +
                      # Print recorded durations and summary.
         
     | 
| 
      
 91 
     | 
    
         
            +
                      ::RSpec.configuration.output_stream.puts <<~EOF
         
     | 
| 
      
 92 
     | 
    
         
            +
                        
         
     | 
| 
      
 93 
     | 
    
         
            +
                        Durations:
         
     | 
| 
      
 94 
     | 
    
         
            +
                      EOF
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                      new_durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
         
     | 
| 
      
 97 
     | 
    
         
            +
                        ::RSpec.configuration.output_stream.puts "#{file_path},#{duration}"
         
     | 
| 
      
 98 
     | 
    
         
            +
                      end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                      ::RSpec.configuration.output_stream.puts <<~EOF
         
     | 
| 
      
 101 
     | 
    
         
            +
                        
         
     | 
| 
      
 102 
     | 
    
         
            +
                        Expected total duration: #{pretty_duration(expected_total_duration)}
         
     | 
| 
      
 103 
     | 
    
         
            +
                        Actual total duration:   #{pretty_duration(actual_total_duration)}
         
     | 
| 
      
 104 
     | 
    
         
            +
                        Diff:                    #{pretty_duration((actual_total_duration - expected_total_duration).abs)}
         
     | 
| 
      
 105 
     | 
    
         
            +
                      EOF
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                      if persist
         
     | 
| 
      
 108 
     | 
    
         
            +
                        # Write all durations with updates to .spec_durations.
         
     | 
| 
      
 109 
     | 
    
         
            +
                        ::RSpec.configuration.output_stream.puts <<~EOF
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                          Saving to .spec_durations.
         
     | 
| 
      
 112 
     | 
    
         
            +
                        EOF
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                        new_durations.each do |file_path, duration|
         
     | 
| 
      
 115 
     | 
    
         
            +
                          all_durations[file_path] = duration
         
     | 
| 
      
 116 
     | 
    
         
            +
                        end
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                        persist_durations(all_durations)
         
     | 
| 
      
 119 
     | 
    
         
            +
                      end
         
     | 
| 
      
 120 
     | 
    
         
            +
                    elsif persist
         
     | 
| 
      
 121 
     | 
    
         
            +
                      ::RSpec.configuration.output_stream.puts <<~EOF
         
     | 
| 
      
 122 
     | 
    
         
            +
                        
         
     | 
| 
      
 123 
     | 
    
         
            +
                        RSpec failed. Not saving to .spec_durations.
         
     | 
| 
      
 124 
     | 
    
         
            +
                      EOF
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                  exit exit_code
         
     | 
| 
      
 129 
     | 
    
         
            +
                end
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                private
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                def self.load_recorded_durations
         
     | 
| 
      
 134 
     | 
    
         
            +
                  durations = { }
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                  if File.exist?('.spec_durations')
         
     | 
| 
      
 137 
     | 
    
         
            +
                    File.readlines('.spec_durations').each_with_index do |line, index|
         
     | 
| 
      
 138 
     | 
    
         
            +
                      line = line.strip
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                      if !line.start_with?('#') && !line.empty?
         
     | 
| 
      
 141 
     | 
    
         
            +
                        parts = line.split(',')
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
                        unless parts.length == 2
         
     | 
| 
      
 144 
     | 
    
         
            +
                          raise ShardError.new("fatal: invalid .spec_durations at line #{index + 1}")
         
     | 
| 
      
 145 
     | 
    
         
            +
                        end
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                        file_path = parts[0].strip
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                        if file_path.empty?
         
     | 
| 
      
 150 
     | 
    
         
            +
                          raise ShardError.new("fatal: invalid file path in .spec_durations at line #{index + 1}")
         
     | 
| 
      
 151 
     | 
    
         
            +
                        end
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                        unless File.exist?(file_path)
         
     | 
| 
      
 154 
     | 
    
         
            +
                          raise ShardError.new("fatal: file in .spec_durations not found at line #{index + 1}")
         
     | 
| 
      
 155 
     | 
    
         
            +
                        end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                        begin
         
     | 
| 
      
 158 
     | 
    
         
            +
                          duration = Integer(parts[1])
         
     | 
| 
      
 159 
     | 
    
         
            +
                        rescue ArgumentError => e
         
     | 
| 
      
 160 
     | 
    
         
            +
                          raise ShardError.new("fatal: invalid .spec_durations at line #{index + 1}")
         
     | 
| 
      
 161 
     | 
    
         
            +
                        end
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
                        durations[file_path] = duration
         
     | 
| 
      
 164 
     | 
    
         
            +
                      end
         
     | 
| 
      
 165 
     | 
    
         
            +
                    end.compact
         
     | 
| 
      
 166 
     | 
    
         
            +
                  end
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
                  durations
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                def self.build_shards(total_shards, shard_num, durations)
         
     | 
| 
      
 172 
     | 
    
         
            +
                  files = { }
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                  ::RSpec.world.ordered_example_groups.each do |example_group|
         
     | 
| 
      
 175 
     | 
    
         
            +
                    file_path = example_group.metadata[:file_path]
         
     | 
| 
      
 176 
     | 
    
         
            +
                    files[file_path] ||= 0
         
     | 
| 
      
 177 
     | 
    
         
            +
                    if durations[file_path]
         
     | 
| 
      
 178 
     | 
    
         
            +
                      files[file_path] = durations[file_path]
         
     | 
| 
      
 179 
     | 
    
         
            +
                    else
         
     | 
| 
      
 180 
     | 
    
         
            +
                      ::RSpec.configuration.error_stream.puts "warning: recorded duration not found for #{file_path}"
         
     | 
| 
      
 181 
     | 
    
         
            +
             
     | 
| 
      
 182 
     | 
    
         
            +
                      # Assume 1000 milliseconds per example.
         
     | 
| 
      
 183 
     | 
    
         
            +
                      files[file_path] += ::RSpec.world.example_count([example_group]) * 1000
         
     | 
| 
      
 184 
     | 
    
         
            +
                    end
         
     | 
| 
      
 185 
     | 
    
         
            +
                  end
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                  shards = (1..total_shards).map { { duration: 0, file_paths: [] } }
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
                  # First sort by duration to ensure large files are distributed evenly.
         
     | 
| 
      
 190 
     | 
    
         
            +
                  # Next, sort by path to ensure shards are generated deterministically.
         
     | 
| 
      
 191 
     | 
    
         
            +
                  # Note that files is a map, sorting it turns it into an array of arrays.
         
     | 
| 
      
 192 
     | 
    
         
            +
                  files = files.sort_by { |file_path, duration| [duration, file_path] }.reverse
         
     | 
| 
      
 193 
     | 
    
         
            +
                  files.each do |file_path, duration|
         
     | 
| 
      
 194 
     | 
    
         
            +
                    shards.sort_by! { |shard| shard[:duration] }
         
     | 
| 
      
 195 
     | 
    
         
            +
                    shards[0][:file_paths] << file_path
         
     | 
| 
      
 196 
     | 
    
         
            +
                    shards[0][:duration] += duration
         
     | 
| 
      
 197 
     | 
    
         
            +
                  end
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                  shards.each { |shard| shard[:file_paths].sort! }
         
     | 
| 
      
 200 
     | 
    
         
            +
             
     | 
| 
      
 201 
     | 
    
         
            +
                  shards
         
     | 
| 
      
 202 
     | 
    
         
            +
                end
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                def self.persist_example_statuses(file_paths)
         
     | 
| 
      
 205 
     | 
    
         
            +
                  return unless (path = ::RSpec.configuration.example_status_persistence_file_path)
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
      
 207 
     | 
    
         
            +
                  examples = ::RSpec.world.all_examples.select do |example|
         
     | 
| 
      
 208 
     | 
    
         
            +
                    file_paths.include?(example.metadata[:file_path])
         
     | 
| 
      
 209 
     | 
    
         
            +
                  end
         
     | 
| 
      
 210 
     | 
    
         
            +
                  ::RSpec::Core::ExampleStatusPersister.persist(examples, path)
         
     | 
| 
      
 211 
     | 
    
         
            +
                rescue SystemCallError => e
         
     | 
| 
      
 212 
     | 
    
         
            +
                  ::RSpec.configuration.error_stream.puts "warning: failed to write results to #{path}"
         
     | 
| 
      
 213 
     | 
    
         
            +
                end
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                def self.pretty_duration(duration_millis)
         
     | 
| 
      
 216 
     | 
    
         
            +
                  duration_seconds = (duration_millis / 1000.0).round
         
     | 
| 
      
 217 
     | 
    
         
            +
                  minutes = duration_seconds / 60
         
     | 
| 
      
 218 
     | 
    
         
            +
                  seconds = duration_seconds % 60
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
                  minutes_str = "#{minutes} minute#{minutes == 1 ? '' : 's'}"
         
     | 
| 
      
 221 
     | 
    
         
            +
                  seconds_str = "#{seconds} second#{seconds == 1 ? '' : 's'}"
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                  if minutes == 0
         
     | 
| 
      
 224 
     | 
    
         
            +
                    seconds_str
         
     | 
| 
      
 225 
     | 
    
         
            +
                  else
         
     | 
| 
      
 226 
     | 
    
         
            +
                    "#{minutes_str}, #{seconds_str}"
         
     | 
| 
      
 227 
     | 
    
         
            +
                  end
         
     | 
| 
      
 228 
     | 
    
         
            +
                end
         
     | 
| 
      
 229 
     | 
    
         
            +
             
     | 
| 
      
 230 
     | 
    
         
            +
                def self.print_shards(shards)
         
     | 
| 
      
 231 
     | 
    
         
            +
                  ::RSpec.configuration.output_stream.puts
         
     | 
| 
      
 232 
     | 
    
         
            +
                  shards.each_with_index do |shard, i|
         
     | 
| 
      
 233 
     | 
    
         
            +
                    ::RSpec.configuration.output_stream.puts(
         
     | 
| 
      
 234 
     | 
    
         
            +
                      "Shard #{i + 1} (Files: #{shard[:file_paths].size}, Duration: #{pretty_duration(shard[:duration])}):"
         
     | 
| 
      
 235 
     | 
    
         
            +
                    )
         
     | 
| 
      
 236 
     | 
    
         
            +
                    shard[:file_paths].each do |file_path|
         
     | 
| 
      
 237 
     | 
    
         
            +
                      ::RSpec.configuration.output_stream.puts file_path
         
     | 
| 
      
 238 
     | 
    
         
            +
                    end
         
     | 
| 
      
 239 
     | 
    
         
            +
                    ::RSpec.configuration.output_stream.puts
         
     | 
| 
      
 240 
     | 
    
         
            +
                  end
         
     | 
| 
      
 241 
     | 
    
         
            +
                end
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
                def self.persist_durations(durations)
         
     | 
| 
      
 244 
     | 
    
         
            +
                  File.open(".spec_durations", "w+") do |file|
         
     | 
| 
      
 245 
     | 
    
         
            +
                    file.puts <<~EOF
         
     | 
| 
      
 246 
     | 
    
         
            +
                      # This file was created by rspec-sharder on #{Time.now.to_s}.
         
     | 
| 
      
 247 
     | 
    
         
            +
                      # It is used to shard specs evenly. If test shards are uneven, run:
         
     | 
| 
      
 248 
     | 
    
         
            +
                      # 
         
     | 
| 
      
 249 
     | 
    
         
            +
                      #   bundle exec rspec-sharder --help
         
     | 
| 
      
 250 
     | 
    
         
            +
                    EOF
         
     | 
| 
      
 251 
     | 
    
         
            +
                    durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
         
     | 
| 
      
 252 
     | 
    
         
            +
                      file.puts "#{file_path},#{duration}"
         
     | 
| 
      
 253 
     | 
    
         
            +
                    end
         
     | 
| 
      
 254 
     | 
    
         
            +
                  end
         
     | 
| 
      
 255 
     | 
    
         
            +
                end
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
                def self.current_time_millis
         
     | 
| 
      
 258 
     | 
    
         
            +
                  (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
         
     | 
| 
      
 259 
     | 
    
         
            +
                end
         
     | 
| 
      
 260 
     | 
    
         
            +
              end
         
     | 
| 
      
 261 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,29 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "lib/rspec-sharder/version"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Gem::Specification.new do |spec|
         
     | 
| 
      
 6 
     | 
    
         
            +
              spec.name          = "rspec-sharder"
         
     | 
| 
      
 7 
     | 
    
         
            +
              spec.version       = RSpec::Sharder::VERSION
         
     | 
| 
      
 8 
     | 
    
         
            +
              spec.authors       = ["Nick Dower"]
         
     | 
| 
      
 9 
     | 
    
         
            +
              spec.email         = ["nicholasdower@gmail.com"]
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              spec.summary       = "A utility which shards specs."
         
     | 
| 
      
 12 
     | 
    
         
            +
              spec.description   = "A utility which shards specs."
         
     | 
| 
      
 13 
     | 
    
         
            +
              spec.homepage      = "https://github.com/nicholasdower/rspec-sharder"
         
     | 
| 
      
 14 
     | 
    
         
            +
              spec.license       = "MIT"
         
     | 
| 
      
 15 
     | 
    
         
            +
              spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              spec.metadata["homepage_uri"] = spec.homepage
         
     | 
| 
      
 18 
     | 
    
         
            +
              spec.metadata["source_code_uri"] = "https://github.com/nicholasdower/rspec-sharder"
         
     | 
| 
      
 19 
     | 
    
         
            +
              spec.metadata["changelog_uri"] = "https://github.com/nicholasdower/rspec-sharder/releases"
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         
     | 
| 
      
 22 
     | 
    
         
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
              spec.bindir        = 'bin'
         
     | 
| 
      
 25 
     | 
    
         
            +
              spec.executables   << 'rspec-sharder'
         
     | 
| 
      
 26 
     | 
    
         
            +
              spec.require_paths = ["lib"]
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              spec.add_dependency 'rspec-core'
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     | 
    
        data/scripts/release.sh
    ADDED
    
    | 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/bin/bash
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            set -e
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            if [ $# -ne 1 ]; then
         
     | 
| 
      
 6 
     | 
    
         
            +
              echo "usage: $0 <version>" >&2
         
     | 
| 
      
 7 
     | 
    
         
            +
              exit 1
         
     | 
| 
      
 8 
     | 
    
         
            +
            fi
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            if [ -n "$(git status --porcelain)" ]; then
         
     | 
| 
      
 11 
     | 
    
         
            +
              echo "error: stage or commit your changes." >&2
         
     | 
| 
      
 12 
     | 
    
         
            +
              exit 1;
         
     | 
| 
      
 13 
     | 
    
         
            +
            fi
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            NEW_VERSION=$1
         
     | 
| 
      
 16 
     | 
    
         
            +
            CURRENT_VERSION=$(grep VERSION lib/rspec-sharder/version.rb | cut -d'"' -f 2)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            echo "Updating from v$CURRENT_VERSION to v$NEW_VERSION. Press enter to continue."
         
     | 
| 
      
 19 
     | 
    
         
            +
            read
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            sed -E -i '' "s/VERSION = \"[^\"]+\"/VERSION = \"$NEW_VERSION\"/g" lib/rspec-sharder/version.rb
         
     | 
| 
      
 22 
     | 
    
         
            +
            gem build
         
     | 
| 
      
 23 
     | 
    
         
            +
            gem push rspec-sharder-$NEW_VERSION.gem
         
     | 
| 
      
 24 
     | 
    
         
            +
            bundle install
         
     | 
| 
      
 25 
     | 
    
         
            +
            git commit -a -m "v$NEW_VERSION Release"
         
     | 
| 
      
 26 
     | 
    
         
            +
            open "https://github.com/nicholasdower/rspec-sharder/releases/new?title=v$NEW_VERSION%20Release&tag=v$NEW_VERSION&target=$(git rev-parse HEAD)"
         
     | 
    
        metadata
    ADDED
    
    | 
         @@ -0,0 +1,70 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            --- !ruby/object:Gem::Specification
         
     | 
| 
      
 2 
     | 
    
         
            +
            name: rspec-sharder
         
     | 
| 
      
 3 
     | 
    
         
            +
            version: !ruby/object:Gem::Version
         
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.0.1
         
     | 
| 
      
 5 
     | 
    
         
            +
            platform: ruby
         
     | 
| 
      
 6 
     | 
    
         
            +
            authors:
         
     | 
| 
      
 7 
     | 
    
         
            +
            - Nick Dower
         
     | 
| 
      
 8 
     | 
    
         
            +
            autorequire:
         
     | 
| 
      
 9 
     | 
    
         
            +
            bindir: bin
         
     | 
| 
      
 10 
     | 
    
         
            +
            cert_chain: []
         
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2021-08-28 00:00:00.000000000 Z
         
     | 
| 
      
 12 
     | 
    
         
            +
            dependencies:
         
     | 
| 
      
 13 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 14 
     | 
    
         
            +
              name: rspec-core
         
     | 
| 
      
 15 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 16 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 17 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 18 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 19 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 20 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 21 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 22 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 23 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 24 
     | 
    
         
            +
                - - ">="
         
     | 
| 
      
 25 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 26 
     | 
    
         
            +
                    version: '0'
         
     | 
| 
      
 27 
     | 
    
         
            +
            description: A utility which shards specs.
         
     | 
| 
      
 28 
     | 
    
         
            +
            email:
         
     | 
| 
      
 29 
     | 
    
         
            +
            - nicholasdower@gmail.com
         
     | 
| 
      
 30 
     | 
    
         
            +
            executables:
         
     | 
| 
      
 31 
     | 
    
         
            +
            - rspec-sharder
         
     | 
| 
      
 32 
     | 
    
         
            +
            extensions: []
         
     | 
| 
      
 33 
     | 
    
         
            +
            extra_rdoc_files: []
         
     | 
| 
      
 34 
     | 
    
         
            +
            files:
         
     | 
| 
      
 35 
     | 
    
         
            +
            - Gemfile
         
     | 
| 
      
 36 
     | 
    
         
            +
            - Gemfile.lock
         
     | 
| 
      
 37 
     | 
    
         
            +
            - LICENSE
         
     | 
| 
      
 38 
     | 
    
         
            +
            - README.md
         
     | 
| 
      
 39 
     | 
    
         
            +
            - bin/rspec-sharder
         
     | 
| 
      
 40 
     | 
    
         
            +
            - lib/rspec-sharder.rb
         
     | 
| 
      
 41 
     | 
    
         
            +
            - lib/rspec-sharder/version.rb
         
     | 
| 
      
 42 
     | 
    
         
            +
            - rspec-sharder.gemspec
         
     | 
| 
      
 43 
     | 
    
         
            +
            - scripts/release.sh
         
     | 
| 
      
 44 
     | 
    
         
            +
            homepage: https://github.com/nicholasdower/rspec-sharder
         
     | 
| 
      
 45 
     | 
    
         
            +
            licenses:
         
     | 
| 
      
 46 
     | 
    
         
            +
            - MIT
         
     | 
| 
      
 47 
     | 
    
         
            +
            metadata:
         
     | 
| 
      
 48 
     | 
    
         
            +
              homepage_uri: https://github.com/nicholasdower/rspec-sharder
         
     | 
| 
      
 49 
     | 
    
         
            +
              source_code_uri: https://github.com/nicholasdower/rspec-sharder
         
     | 
| 
      
 50 
     | 
    
         
            +
              changelog_uri: https://github.com/nicholasdower/rspec-sharder/releases
         
     | 
| 
      
 51 
     | 
    
         
            +
            post_install_message:
         
     | 
| 
      
 52 
     | 
    
         
            +
            rdoc_options: []
         
     | 
| 
      
 53 
     | 
    
         
            +
            require_paths:
         
     | 
| 
      
 54 
     | 
    
         
            +
            - lib
         
     | 
| 
      
 55 
     | 
    
         
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         
     | 
| 
      
 56 
     | 
    
         
            +
              requirements:
         
     | 
| 
      
 57 
     | 
    
         
            +
              - - ">="
         
     | 
| 
      
 58 
     | 
    
         
            +
                - !ruby/object:Gem::Version
         
     | 
| 
      
 59 
     | 
    
         
            +
                  version: 2.4.0
         
     | 
| 
      
 60 
     | 
    
         
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         
     | 
| 
      
 61 
     | 
    
         
            +
              requirements:
         
     | 
| 
      
 62 
     | 
    
         
            +
              - - ">="
         
     | 
| 
      
 63 
     | 
    
         
            +
                - !ruby/object:Gem::Version
         
     | 
| 
      
 64 
     | 
    
         
            +
                  version: '0'
         
     | 
| 
      
 65 
     | 
    
         
            +
            requirements: []
         
     | 
| 
      
 66 
     | 
    
         
            +
            rubygems_version: 3.2.3
         
     | 
| 
      
 67 
     | 
    
         
            +
            signing_key:
         
     | 
| 
      
 68 
     | 
    
         
            +
            specification_version: 4
         
     | 
| 
      
 69 
     | 
    
         
            +
            summary: A utility which shards specs.
         
     | 
| 
      
 70 
     | 
    
         
            +
            test_files: []
         
     |