spec_tiller 0.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MThlMmQ3MDVmMmQwNmUwM2E3MDIyNzlmNjMwNDM1YTljMzhiY2YyNA==
5
- data.tar.gz: !binary |-
6
- YjhkMmVkNzg0N2M3YzZjMTNhMGMxNTAxMmZlNDgzOGZjZDZiMDlhYQ==
2
+ SHA1:
3
+ metadata.gz: e9b13142ceda1474ca093cd92a29fb161ea83642
4
+ data.tar.gz: 45db628b9fff9bc1c00457343564c995b4599fbd
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- Y2I1NTVhMzJkNjUxMWNjYmVmZTAxODgzZDE4MWNkZDEwMGI1NDFkOTJjZmMx
10
- ZTgxMzBkNTVjYjlkMmU2YTJkMWEzM2FhNmFhZmVlMmJlOTE3NDdmNmY0NmQw
11
- ZDNlOTgwYTJmOWVhM2RmYjllNWU3ZTBiMjU5OTUzOGQ4YTc3MzY=
12
- data.tar.gz: !binary |-
13
- MjBhMjA0ZTcyOTU4MGIzODhlYWVkMDVlMDY0ZTI5NmYzYjZhMjc2YTNjOTk1
14
- OTUxYzYzMjc5NmZmY2I1ODQ2NjRiOWVhOWRiNmVkODc3MjQzNmZmMDY4NzE2
15
- N2VjNmY1ZjVkODZhZWEzOGQxZjY0MmZlZDk4NTRjNDI1NzczYmY=
6
+ metadata.gz: c8fecb888fa57185e06e798c688d6f897ebbfc4339b9fb9f57a1e43f84c41bf5859a8f76f74a807841d69c85c6a8f154fa89fe504f449df752ca5a532b533655
7
+ data.tar.gz: 0101f1acd5dc9af7ffdb0c2e033310ed942dfde104fd89965cbc55a32b819b13c96c998a2fe2174f109c864fcf8543ae6a8d8d86825dbfd50d50d47704e3b88a
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spec_distributor.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Matt Schmaus - Greenhouse
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # SpecTiller
2
+
3
+ ## Description
4
+ This gem will parse the output of calling "rspec --perform", and then will redistribute the spec files evenly, based on file run time, across all builds established in the travis.yml file in order to optimize your test suite's run time. As of now, this gem is only compatible with Travis CI and their build matrix setup. For more information on this setup, check [here](http://docs.travis-ci.com/user/build-configuration/#The-Build-Matrix).
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's ``Gemfile`` in **both** *development* and *test* groups:
9
+
10
+ group :development, :test do
11
+ gem 'spec_tiller'
12
+ end
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ ## Set Up
19
+
20
+ #### Git Hooks
21
+ In order to make sure that the travis file remains up to date with any newly added or removed spec files, you'll have to include a rake task in your pre-commit and post-merge git hooks. Create two files in .git/hooks/ --> ``pre-commit`` and ``post-merge`` (no file extension). **Please make sure that these files are executable** (more information on [git hooks](http://git-scm.com/book/en/Customizing-Git-Git-Hooks)). In each of those two files, add the following rake task:
22
+
23
+ #!/bin/sh
24
+ rake spec_tiller:sync
25
+
26
+ Upon setting this up, any time you commit or merge, you'll notice the following output:
27
+
28
+ Syncing list of spec files...
29
+ Removed: [spec/file/removed_spec.rb]
30
+ Added: [spec/file/added_spec.rb, spec/file/another_added_spec.rb]
31
+
32
+ ####.travis.yml
33
+ In your ``.travis.yml`` file, create a top-level variable **num_builds** and set it to the number of builds you want the spec files distributed over (deafult value is 5 builds). You must also make sure that the base of your script is as follows:
34
+
35
+ bundle exec rspec $TEST_SUITE
36
+
37
+ *$TEST_SUITE* represents an environment variable. For the purpose of this gem, a list of space-separated spec files will be assigned to that variable. So, lets say that in ``.travis.yml``, *TEST_SUITE="spec/path/file_one_spec.rb spec/path/file_two_spec.rb spec/path/file_three_spec.rb"*, then the script above will execute as follows:
38
+
39
+ bundle exec rspec spec/path/file_one_spec.rb spec/path/file_two_spec.rb spec/path/file_three_spec.rb
40
+
41
+ Here is an example of what a ``.travis.yml`` file may look like after all is said and done.:
42
+
43
+ language: ruby
44
+ rvm:
45
+ - 1.9.3
46
+ branches:
47
+ only:
48
+ - develop
49
+ - master
50
+ - /^release\/.*$/
51
+ - /^hotfix\/.*$/
52
+ - /^feature\/testing-.+$/
53
+ cache: bundler
54
+ before_install:
55
+ - export DISPLAY=:99.0
56
+ - sh -e /etc/init.d/xvfb start
57
+ before_script:
58
+ - psql -c 'create database test;' -U postgres
59
+ - RAILS_ENV=test bundle exec rake --trace db:test:load db:seed
60
+ script:
61
+ - bundle exec rspec $TEST_SUITE --tag ~local_only
62
+ env:
63
+ global:
64
+ - SOME_OTHER_ENV_VAR="hello world"
65
+ matrix:
66
+ - TEST_SUITE="spec/path/file_one_spec.rb spec/path/file_two_spec.rb spec/path/file_three_spec.rb"
67
+ - TEST_SUITE="spec/path/file_four_spec.rb"
68
+ - TEST_SUITE="spec/path/file_five_spec.rb spec/path/file_six_spec.rb"
69
+ - TEST_SUITE="spec/path/file_seven_spec.rb spec/path/file_eight_spec.rb spec/path/file_nine_spec.rb spec/path/file_ten_spec.rb"
70
+ - TEST_SUITE="spec/path/file_eleven_spec.rb spec/path/file_twelve_spec.rb"
71
+ num_builds: 5
72
+
73
+ #### Redistributing Files
74
+ Initially, and every so often, you will have to redistribute the spec files (make sure everything is properly set up before running this rake task). The *spec_tiller:sync* rake task adds any new spec files to the last bucket, so after a little while, the timing of the build may not be optimal. In order to redistribute the spec files in order to optimize test suite run time, run the following rake task:
75
+
76
+ spec_tiller:redistribute
77
+
78
+ This will run your whole test suite, keeping track of how long each spec file takes to run, and the will distribute the spec files in order to maximize your test suite's run time, over the number of builds you've designated (with a default value of 5). **This rake task will not print the output of the rake task until it is complete.**
79
+ ***
80
+ ## Feature Requests & Bugs
81
+ See [http://github.com/grnhse/spec-tiller/issues](http://github.com/grnhse/spec-tiller/issues)
82
+
83
+ ## Contributing
84
+
85
+ 1. Fork it
86
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
87
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
88
+ 4. Push to the branch (`git push origin my-new-feature`)
89
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,30 @@
1
+ # Includes functions to parse build matrix strings into hashes and to compress them back into strings.'
2
+ module BuildMatrixParser
3
+ def parse_env_matrix(content)
4
+ content['env']['matrix'].map do |matrix_line|
5
+ if matrix_line.nil?
6
+ {}
7
+ else
8
+ # Input: TEST_SUITE="spec/a.rb spec/b.rb" RUN_JS="true"
9
+ # Output: { 'TEST_SUITE' => 'spec/a.rb spec/b.rb', 'RUN_JS' => 'true' }
10
+ Hash[matrix_line.scan(/\s*([^=]+)="\s*([^"]+)"/)]
11
+ end
12
+ end
13
+ end
14
+ module_function :parse_env_matrix
15
+
16
+ def format_matrix(env_matrix)
17
+ content_env_matrix = []
18
+
19
+ env_matrix.each do |var_hash|
20
+ next if var_hash.empty? || var_hash["TEST_SUITE"].empty?
21
+ line = var_hash.map { |key, value| %Q(#{key}="#{value}") }.join(' ')
22
+
23
+ content_env_matrix << line
24
+ end
25
+
26
+ content_env_matrix
27
+ end
28
+ module_function :format_matrix
29
+
30
+ end
@@ -0,0 +1,125 @@
1
+ require 'rake'
2
+ require 'yaml'
3
+
4
+ namespace :spec_tiller do
5
+ desc 'Runs whole test suite and redistributes spec files across builds according to file run time'
6
+ task :redistribute => :environment do
7
+ travis_yml_file = YAML::load(File.open('.travis.yml'))
8
+ env_variables = travis_yml_file['env']['global']
9
+ script = travis_yml_file['script'].first.gsub('$TEST_SUITE ', '')
10
+
11
+ profile_results = `#{env_variables.join(' ')} #{script} --profile 1000000000`
12
+
13
+ `echo "#{profile_results}" > spec/log/rspec_profile_output.txt`
14
+ TravisBuildMatrix::SpecDistributor.new(travis_yml_file, profile_results) do |content|
15
+ File.open('.travis.yml', 'w') { |file| file.write(content.to_yaml(:line_width => -1)) }
16
+ end
17
+ puts profile_results
18
+ end
19
+ end
20
+
21
+ module TravisBuildMatrix
22
+
23
+ DEFAULT_NUM_BUILDS = 5
24
+
25
+ class SpecFile
26
+ attr_accessor :file_path, :test_duration
27
+
28
+ def initialize(file_path, test_duration)
29
+ @test_duration = test_duration
30
+ @file_path = file_path
31
+ end
32
+ end
33
+
34
+ class TestBucket
35
+ attr_reader :spec_files, :total_duration
36
+
37
+ def initialize
38
+ @total_duration = 0.0
39
+ @spec_files = []
40
+ end
41
+
42
+ def add_to_list(spec_file)
43
+ @spec_files << spec_file
44
+ @total_duration += spec_file.test_duration
45
+ end
46
+ end
47
+
48
+ class SpecDistributor
49
+
50
+ EXTRACT_DURATION_AND_FILE_PATH = /\s{1}\(([0-9\.]*\s).*\.\/(spec.*):/
51
+
52
+ def initialize(travis_yml_file, profile_results, &block)
53
+ num_buckets = travis_yml_file['num_builds'] || DEFAULT_NUM_BUILDS
54
+
55
+ @spec_files = parse_profile_results(profile_results)
56
+ @test_buckets = Array.new(num_buckets){ |_| TestBucket.new }
57
+
58
+ distribute_tests
59
+
60
+ TravisBuildMatrix::TravisFile.new(@test_buckets, travis_yml_file, &block)
61
+ end
62
+
63
+ private
64
+
65
+ def parse_profile_results(profile_results)
66
+
67
+ #Input: Walnuts
68
+ # 9.96 seconds average (69.69 seconds / 7 examples) ./spec/features/walnut_spec.rb:3
69
+ #Output: ["9.96", "spec/features/walnut_spec.rb"]
70
+ extracted_info = profile_results.scan(EXTRACT_DURATION_AND_FILE_PATH).uniq { |spec_file| spec_file.last }
71
+
72
+ tests = extracted_info.map do |capture_groups|
73
+ test_duration = capture_groups.first.strip.to_f
74
+ test_file_path = capture_groups.last
75
+
76
+ SpecFile.new(test_file_path, test_duration)
77
+ end
78
+
79
+ tests.sort_by(&:test_duration).reverse
80
+ end
81
+
82
+ def smallest_bucket
83
+ @test_buckets.min_by(&:total_duration)
84
+ end
85
+
86
+ def distribute_tests
87
+ @spec_files.each { |test| smallest_bucket.add_to_list(test) }
88
+ end
89
+
90
+ end
91
+
92
+ class TravisFile
93
+ include BuildMatrixParser
94
+
95
+ def initialize(test_buckets, travis_yml_file, &block)
96
+ rewrite_content(test_buckets, travis_yml_file)
97
+ block.call(travis_yml_file) if block
98
+ end
99
+
100
+ private
101
+
102
+ def rewrite_content(test_buckets, content)
103
+ content['env']['matrix'] ||= [] # initialize env if not already set
104
+
105
+ env_matrix = BuildMatrixParser.parse_env_matrix(content)
106
+
107
+ if env_matrix.length > test_buckets.length
108
+ env_matrix = env_matrix.slice(0, test_buckets.length)
109
+ elsif env_matrix.length < test_buckets.length
110
+ (test_buckets.length - env_matrix.length).times {env_matrix.push({ })}
111
+ end
112
+
113
+ env_matrix.each do |var_hash|
114
+ test_bucket = test_buckets.shift
115
+
116
+ spec_file_list = test_bucket.spec_files.map(&:file_path).join(' ')
117
+ var_hash['TEST_SUITE'] = "#{spec_file_list}"
118
+ end
119
+
120
+ content['env']['matrix'] = BuildMatrixParser.format_matrix(env_matrix)
121
+ end
122
+
123
+ end
124
+
125
+ end
@@ -0,0 +1,93 @@
1
+ require 'rake'
2
+ require 'yaml'
3
+
4
+ namespace :spec_tiller do
5
+ desc 'Compares spec files in travis.yml to current list of spec files, and syncs accordingly'
6
+ task :sync do
7
+ content = YAML::load(File.open('.travis.yml'))
8
+ current_file_list = Dir.glob('spec/**/*_spec.rb').map { |file_path| file_path.slice(/(spec\/\S+$)/) }
9
+
10
+ puts "\nSyncing list of spec files..."
11
+
12
+ SyncSpecFiles.rewrite_travis_content(content, current_file_list) do |original|
13
+ write_to_file(content)
14
+ puts file_diff(original, current_file_list)
15
+ end
16
+
17
+ `git add .travis.yml`
18
+ end
19
+ end
20
+
21
+ module SyncSpecFiles
22
+ include BuildMatrixParser
23
+
24
+ def rewrite_travis_content(content, current_file_list, &block)
25
+ env_matrix = BuildMatrixParser.parse_env_matrix(content)
26
+ original = extract_spec_files(env_matrix)
27
+ after_removed = delete_removed_files(original, current_file_list)
28
+ after_added = add_new_files(original, after_removed, current_file_list)
29
+
30
+ env_matrix.each do |var_hash|
31
+ if var_hash.has_key?('TEST_SUITE')
32
+ test_bucket = after_added.shift
33
+
34
+ var_hash['TEST_SUITE'] = "#{test_bucket.join(' ')}"
35
+ end
36
+ end
37
+
38
+ content['env']['matrix'] = BuildMatrixParser.format_matrix(env_matrix)
39
+
40
+ block.call(original) if block
41
+ end
42
+
43
+ module_function :rewrite_travis_content
44
+
45
+ private
46
+
47
+ def self.extract_spec_files(env_matrix)
48
+ test_suites = env_matrix.map do |var_hash|
49
+ var_hash.has_key?('TEST_SUITE') ? var_hash['TEST_SUITE'].gsub('"', '').split(' ') : nil
50
+ end
51
+ test_suites.compact
52
+ end
53
+
54
+ def self.delete_removed_files(original, current_file_list)
55
+ deleted_files = deleted_files(original, current_file_list)
56
+
57
+ original.map do |bucket|
58
+ bucket.reject { |spec_file| deleted_files.include?(spec_file) }
59
+ end
60
+ end
61
+
62
+ def self.add_new_files(original, buckets, current_file_list)
63
+ buckets_clone = buckets.map(&:dup)
64
+
65
+ added_files(original, current_file_list).each do |spec_file|
66
+ buckets_clone.last << spec_file
67
+ end
68
+
69
+ buckets_clone
70
+ end
71
+
72
+ def self.deleted_files(original, current_file_list)
73
+ original.flatten - current_file_list
74
+ end
75
+
76
+ def self.added_files(original, current_file_list)
77
+ current_file_list - original.flatten
78
+ end
79
+
80
+ def self.write_to_file(content)
81
+ File.open('.travis.yml', 'w') { |file| file.write(content.to_yaml(:line_width => -1)) }
82
+ end
83
+
84
+ def self.file_diff(original, current_file_list)
85
+ removed_files = deleted_files(original, current_file_list).sort
86
+ removed = removed_files.empty? ? 'No spec files removed' : removed_files
87
+
88
+ added_files = added_files(original, current_file_list).sort
89
+ added = added_files.empty? ? 'No spec files added' : added_files
90
+
91
+ " Removed: #{removed}\n Added: #{added}\n\n"
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module SpecTiller
2
+ VERSION = '1.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_tiller/version'
2
+ require 'spec_tiller/build_matrix_parser'
3
+ require 'spec_tiller/distribute_spec_files'
4
+ require 'spec_tiller/sync_spec_file_list'
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+
4
+ describe BuildMatrixParser do
5
+
6
+ let(:travis_yml_file) { YAML::load(File.open('spec/documents/.travis.yml')) }
7
+
8
+ describe '#parse_env_matrix' do
9
+ let(:actual_parsed_matrix) { BuildMatrixParser.parse_env_matrix(travis_yml_file) }
10
+
11
+ it 'will maintain same number of elements' do
12
+ expect(actual_parsed_matrix.length).to eq(5)
13
+ end
14
+
15
+ it 'will parse multiple variables' do
16
+ expect(actual_parsed_matrix[0]).to eq( { "TEST_SUITE" => "spec/features/three_vars.rb", "RUN_JS" => "true", "FAKE_VAR" => "fake.value.rb" } )
17
+ end
18
+
19
+ it 'will ignore extra spaces' do
20
+ expect(actual_parsed_matrix[1]).to eq( { "TEST_SUITE" => "spec/features/space_after.rb spec/features/space_before.rb", "FIVE_TABS_BEFORE" => "tabs are ignored" } )
21
+ end
22
+
23
+ it 'will parse lowercase, numbers, and symbols' do
24
+ expect(actual_parsed_matrix[2]).to eq( { "l0w3rC@5e&nUm5&sym()1s" => "~!@#$%^&*()_+1234567890-=`{}[]|:;''<>,.?/", "TEST_SUITE" => "spec/features/test.rb" } )
25
+ end
26
+
27
+ it 'will parse empty lines' do
28
+ expect(actual_parsed_matrix[3]).to eq( {} )
29
+ end
30
+
31
+ it 'will parse long file lists' do
32
+ expect(actual_parsed_matrix[4]).to eq( { "TEST_SUITE" => "spec/test/test1.rb spec/test/test2.rb spec/test/test3.rb spec/test/test4.rb spec/test/test5.rb spec/test/test6.rb spec/test/test7.rb spec/test/test8.rb spec/test/test9.rb spec/test/test10.rb spec/test/test11.rb spec/test/test12.rb spec/test/test13.rb spec/test/test14.rb spec/test/test15.rb spec/test/test16.rb" } )
33
+ end
34
+
35
+ end
36
+
37
+ describe '#format_matrix' do
38
+ let(:actual_formatted_matrix) { BuildMatrixParser.format_matrix(BuildMatrixParser.parse_env_matrix(travis_yml_file)) }
39
+
40
+ it 'will maintain multiple variables' do
41
+ expect(actual_formatted_matrix[0]).to eq(%Q(TEST_SUITE="spec/features/three_vars.rb" RUN_JS="true" FAKE_VAR="fake.value.rb"))
42
+ end
43
+
44
+ it 'will strip spaces' do
45
+ expect(actual_formatted_matrix[1]).to eq(%Q(TEST_SUITE="spec/features/space_after.rb spec/features/space_before.rb" FIVE_TABS_BEFORE="tabs are ignored"))
46
+ end
47
+
48
+ it 'will maintain lowercase, numbers, and symbols' do
49
+ expect(actual_formatted_matrix[2]).to eq(%Q(l0w3rC@5e&nUm5&sym()1s="~!@#$%^&*()_+1234567890-=`{}[]|:;''<>,.?/" TEST_SUITE="spec/features/test.rb"))
50
+ end
51
+
52
+ it 'will remove empty lines' do
53
+ expect(actual_formatted_matrix[3]).to_not eq(nil)
54
+ end
55
+
56
+ it 'will maintain long file lists' do
57
+ expect(actual_formatted_matrix[3]).to eq(%Q(TEST_SUITE="spec/test/test1.rb spec/test/test2.rb spec/test/test3.rb spec/test/test4.rb spec/test/test5.rb spec/test/test6.rb spec/test/test7.rb spec/test/test8.rb spec/test/test9.rb spec/test/test10.rb spec/test/test11.rb spec/test/test12.rb spec/test/test13.rb spec/test/test14.rb spec/test/test15.rb spec/test/test16.rb"))
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,162 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+
4
+ describe 'TravisBuildMatrix' do
5
+
6
+ describe 'TestBucket' do
7
+ let(:bucket) { TravisBuildMatrix::TestBucket.new }
8
+
9
+ describe '::new' do
10
+
11
+ it 'will have an initial duration of 0.0' do
12
+ expect(bucket.total_duration).to eq(0.0)
13
+ end
14
+
15
+ it 'will begin with an empty file list' do
16
+ expect(bucket.spec_files).to eq([])
17
+ end
18
+
19
+ end
20
+
21
+ describe '#add_to_list' do
22
+ let(:short_spec_file) { TravisBuildMatrix::SpecFile.new('spec/features/short_spec_file.rb', 3.2) }
23
+ let(:long_spec_file) { TravisBuildMatrix::SpecFile.new('spec/features/long_spec_file.rb', 10.4) }
24
+ before(:each) do
25
+ bucket.add_to_list(short_spec_file)
26
+ bucket.add_to_list(long_spec_file)
27
+ end
28
+
29
+ it 'adds to bucket duration' do
30
+ expect(bucket.total_duration.round(1)).to eq(13.6)
31
+ end
32
+
33
+ it 'adds all files' do
34
+ expect(bucket.spec_files.length).to eq(2)
35
+ end
36
+
37
+ it 'maintains file order' do
38
+ expect(bucket.spec_files.first).to be(short_spec_file)
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ describe 'SpecDistributor' do
46
+ let!(:travis_yml_file) { YAML::load(File.open('spec/documents/.travis.yml')) }
47
+ let(:profile_results) { File.read('spec/documents/rspec_profile_results.txt') }
48
+
49
+ describe '::new' do
50
+
51
+ it 'defaults to 5 builds' do
52
+ travis_yml_file['num_builds'] = nil
53
+ TravisBuildMatrix::SpecDistributor.new(travis_yml_file, profile_results)
54
+ expect(travis_yml_file['env']['matrix'].length).to eq(5)
55
+ end
56
+
57
+ it 'runs when there are more builds than specs' do
58
+ travis_yml_file['num_builds'] = 20
59
+ TravisBuildMatrix::SpecDistributor.new(travis_yml_file, profile_results)
60
+ expect(travis_yml_file['env']['matrix'].length).to eq(10) # 10 is the number of builds in profile results.
61
+ end
62
+
63
+ it 'can find special characters' do
64
+ travis_yml_file['num_builds'] = 1
65
+ TravisBuildMatrix::SpecDistributor.new(travis_yml_file, profile_results)
66
+ expect(travis_yml_file['env']['matrix'].first).to include(%Q(~!@#$^()_+1234567890-=`{}[];',_spec.rb))
67
+ end
68
+
69
+ it 'groups builds as expected' do
70
+ travis_yml_file['num_builds'] = 5
71
+ TravisBuildMatrix::SpecDistributor.new(travis_yml_file, profile_results)
72
+ formatted_matrix = travis_yml_file['env']['matrix']
73
+ expect(formatted_matrix[0]).to include('different_ending.rb')
74
+ expect(formatted_matrix[1]).to include('peanut_spec.rb')
75
+ expect(formatted_matrix[2]).to include('almond_spec.rb')
76
+ expect(formatted_matrix[3]).to include('cashew_spec.rb')
77
+ expect(formatted_matrix[4]).to include(%Q(~!@#$^()_+1234567890-=`{}[];',_spec.rb))
78
+ expect(formatted_matrix[4]).to include('different_extension_spec.txt')
79
+ expect(formatted_matrix[3]).to include('pecan_spec.rb')
80
+ expect(formatted_matrix[4]).to include('walnut_spec.rb')
81
+ expect(formatted_matrix[3]).to include('acorn_spec.rb')
82
+ expect(formatted_matrix[3]).to include('pistachio_spec.rb')
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ describe 'TravisFile' do
90
+
91
+ describe '::new' do
92
+
93
+ def create_bucket(total_duration, spec_file_list)
94
+ bucket = TravisBuildMatrix::TestBucket.new
95
+ bucket.instance_variable_set("@total_duration", total_duration)
96
+ spec_file_hashes = spec_file_list.map do |file|
97
+ TravisBuildMatrix::SpecFile.new(file, 0) # don't care about duration, not used in this test
98
+ end
99
+ bucket.instance_variable_set("@spec_files", spec_file_hashes)
100
+ bucket
101
+ end
102
+
103
+ let!(:travis_yml_file) { YAML::load(File.open('spec/documents/.travis.yml')) }
104
+
105
+ let(:test_buckets) do
106
+ [create_bucket(8, ['spec/features/three_vars.rb']),
107
+ create_bucket(8.3, ['spec/features/space_after.rb', 'spec/features/space_before.rb']),
108
+ create_bucket(11.8, ['spec/features/test.rb']),
109
+ create_bucket(10.7, ['spec/test/test1.rb', 'spec/test/test2.rb', 'spec/test/test3.rb', 'spec/test/test4.rb', 'spec/test/test5.rb', 'spec/test/test6.rb', 'spec/test/test7.rb', 'spec/test/test8.rb', 'spec/test/test9.rb', 'spec/test/test10.rb', 'spec/test/test11.rb', 'spec/test/test12.rb', 'spec/test/test13.rb', 'spec/test/test14.rb', 'spec/test/test15.rb', 'spec/test/test16.rb']),
110
+ create_bucket(10.4, ['spec/features/long_spec.rb']),
111
+ create_bucket(9.1, ['spec/features/short_1_spec.rb', 'spec/features/short_2_spec.rb', 'spec/features/short_3_spec.rb'])]
112
+ end
113
+
114
+ context 'buckets are removed' do
115
+ before(:each) do
116
+ TravisBuildMatrix::TravisFile.new(test_buckets[0..1], travis_yml_file)
117
+ end
118
+
119
+ it 'compresses to the appropriate size' do
120
+ expect(travis_yml_file['env']['matrix'].length).to eq(2)
121
+ end
122
+
123
+ it 'maintains env variables within range' do
124
+ formatted_matrix = travis_yml_file['env']['matrix']
125
+ expect(formatted_matrix[0]).to include(%Q(RUN_JS="true"))
126
+ expect(formatted_matrix[0]).to include(%Q(FAKE_VAR="fake.value.rb"))
127
+ expect(formatted_matrix[1]).to include(%Q(FIVE_TABS_BEFORE="tabs are ignored"))
128
+ end
129
+
130
+ it 'drops env variables outside of range' do
131
+ travis_yml_file['env']['matrix'].each do |line|
132
+ expect(line).to_not include(%Q(l0w3rC@5e&nUm5&sym()1s="~!@#$%^&*()_+1234567890-=`{}[]|:;''<>,.?/"))
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ context 'buckets are added' do
140
+ before(:each) do
141
+ TravisBuildMatrix::TravisFile.new(test_buckets, travis_yml_file)
142
+ end
143
+
144
+ it 'expands to appropriate size' do
145
+ expect(travis_yml_file['env']['matrix'].length).to eq(6)
146
+ end
147
+
148
+ it 'maintains env variables within range' do
149
+ formatted_matrix = travis_yml_file['env']['matrix']
150
+ expect(formatted_matrix[0]).to include(%Q(RUN_JS="true"))
151
+ expect(formatted_matrix[0]).to include(%Q(FAKE_VAR="fake.value.rb"))
152
+ expect(formatted_matrix[1]).to include(%Q(FIVE_TABS_BEFORE="tabs are ignored"))
153
+ expect(formatted_matrix[2]).to include(%Q(l0w3rC@5e&nUm5&sym()1s="~!@#$%^&*()_+1234567890-=`{}[]|:;''<>,.?/"))
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ end
@@ -0,0 +1,34 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 2.1.2
5
+ addons:
6
+ hosts:
7
+ - example.com
8
+ branches:
9
+ only:
10
+ - develop
11
+ - master
12
+ - "/^release\\/.*$/"
13
+ - "/^hotfix\\/.*$/"
14
+ - "/^feature\\/testing-.+$/"
15
+ cache: bundler
16
+ before_install:
17
+ - export DISPLAY=:99.0
18
+ - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x16"
19
+ before_script:
20
+ - psql -c 'create database test;' -U postgres
21
+ - RAILS_ENV=test bundle exec rake --trace db:test:load db:seed
22
+ script:
23
+ - RSPEC_RETRY_COUNT=2 bundle exec rspec $TEST_SUITE --tag ~local_only
24
+ - if [[ $RUN_JS == "true" ]]; then bundle exec rake jasmine:ci; fi
25
+ env:
26
+ global:
27
+ - RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=1.1
28
+ matrix:
29
+ - TEST_SUITE="spec/features/three_vars.rb" RUN_JS="true" FAKE_VAR="fake.value.rb"
30
+ - TEST_SUITE="spec/features/space_after.rb spec/features/space_before.rb" FIVE_TABS_BEFORE="tabs are ignored"
31
+ - l0w3rC@5e&nUm5&sym()1s="~!@#$%^&*()_+1234567890-=`{}[]|:;''<>,.?/" TEST_SUITE="spec/features/test.rb"
32
+ -
33
+ - TEST_SUITE="spec/test/test1.rb spec/test/test2.rb spec/test/test3.rb spec/test/test4.rb spec/test/test5.rb spec/test/test6.rb spec/test/test7.rb spec/test/test8.rb spec/test/test9.rb spec/test/test10.rb spec/test/test11.rb spec/test/test12.rb spec/test/test13.rb spec/test/test14.rb spec/test/test15.rb spec/test/test16.rb"
34
+ num_builds: 10
@@ -0,0 +1,23 @@
1
+ Below is the relevant part of profile results:
2
+
3
+ Top 10 slowest example groups:
4
+ Walnut
5
+ 0.35161 seconds average (42.19 seconds / 120 examples) ./spec/feature/walnut_spec.rb:4
6
+ Acorn
7
+ 0.1916 seconds average (4.6 seconds / 24 examples) ./spec/models/acorn_spec.rb:3
8
+ Pistachio
9
+ 0.00297 seconds average (0.06826 seconds / 23 examples) ./spec/lib/pistachio_spec.rb:3
10
+ Cashew
11
+ 12.55 seconds average (87.85 seconds / 7 examples) ./spec/features/cashew_spec.rb:3
12
+ Peanut
13
+ 12.22 seconds average (305.54 seconds / 25 examples) ./spec/features/peanut_spec.rb:102
14
+ Pecan
15
+ 12.17 seconds average (48.68 seconds / 4 examples) ./spec/features/pecan_spec.rb:3
16
+ Almond
17
+ 11.8 seconds average (259.54 seconds / 22 examples) ./spec/features/almond_spec.rb:84
18
+ Special Chars
19
+ 11.55 seconds average (69.28 seconds / 6 examples) ./spec/features/~!@#$^()_+1234567890-=`{}[];',_spec.rb:3
20
+ Different Ending
21
+ 11.43 seconds average (331.49 seconds / 29 examples) ./spec/features/different_ending.rb:3
22
+ Different Extension
23
+ 11.16 seconds average (66.98 seconds / 6 examples) ./spec/features/different_extension_spec.txt:3
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'spec_tiller' # and any other gems you need
5
+
6
+ RSpec.configure do |config|
7
+ # some (optional) config here
8
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+
4
+ describe 'SyncSpecFiles' do
5
+
6
+ describe '#rewrite_travis_content' do
7
+
8
+ let(:current_file_list) do
9
+ ['spec/test/new3.rb',
10
+ 'spec/features/space_after.rb', 'spec/features/space_before.rb',
11
+ 'spec/test/test1.rb', 'spec/test/test2.rb', 'spec/test/test3.rb', 'spec/test/test4.rb', 'spec/test/test5.rb', 'spec/test/test6.rb', 'spec/test/test7.rb', 'spec/test/test8.rb', 'spec/test/test9.rb', 'spec/test/test10.rb', 'spec/test/test11.rb', 'spec/test/test12.rb', 'spec/test/test13.rb', 'spec/test/test14.rb', 'spec/test/test15.rb', 'spec/test/test16.rb',
12
+ 'spec/test/new1.rb', 'spec/test2/new2.rb']
13
+ end
14
+ let(:travis_yaml) { YAML::load(File.open('spec/documents/.travis.yml')) }
15
+
16
+ before(:each) do
17
+ SyncSpecFiles.rewrite_travis_content(travis_yaml, current_file_list)
18
+ end
19
+
20
+ it 'adds new files to the last line' do
21
+ last_line = travis_yaml['env']['matrix'].last
22
+
23
+ expect(last_line).to include('spec/test/new1.rb','spec/test2/new2.rb','spec/test/new3.rb')
24
+ end
25
+
26
+ it "doesn't add new files to other lines" do
27
+ travis_yaml['env']['matrix'][0..-2].each do |bucket|
28
+ expect(bucket).not_to include('spec/test/new1.rb')
29
+ expect(bucket).not_to include('spec/test2/new2.rb')
30
+ expect(bucket).not_to include('spec/test/new3.rb')
31
+ end
32
+
33
+ end
34
+
35
+ it 'removes unused specs' do
36
+ travis_yaml['env']['matrix'].each do |bucket|
37
+ expect(bucket).not_to include('spec/features/three_vars.rb')
38
+ end
39
+
40
+ end
41
+
42
+ it 'removes unused buckets' do
43
+ expect(travis_yaml['env']['matrix'].length).to eq(2)
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'spec_tiller/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "spec_tiller"
8
+ spec.version = SpecTiller::VERSION
9
+ spec.authors = ["Matt Schmaus","Josh Bazemore"]
10
+ spec.email = ["mschmaus201@gmail.com","jbazemore@greenhouse.io"]
11
+ spec.description = <<-EOF
12
+ This gem will parse the output of calling "rspec --perform", then will redistribute
13
+ the spec files evenly, based on file run time, across all builds established
14
+ in the travis.yml file.
15
+ EOF
16
+ spec.summary = 'Distribute spec files evenly across Travis builds, based on file run time'
17
+ spec.homepage = ""
18
+ spec.license = "MIT"
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ end
metadata CHANGED
@@ -1,52 +1,86 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spec_tiller
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Schmaus
8
+ - Josh Bazemore
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2014-06-30 00:00:00.000000000 Z
12
+ date: 2014-11-20 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: bundler
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
- - - ~>
18
+ - - "~>"
18
19
  - !ruby/object:Gem::Version
19
20
  version: '1.3'
20
21
  type: :development
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
- - - ~>
25
+ - - "~>"
25
26
  - !ruby/object:Gem::Version
26
27
  version: '1.3'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: rake
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - ! '>='
32
+ - - ">="
32
33
  - !ruby/object:Gem::Version
33
34
  version: '0'
34
35
  type: :development
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
- - - ! '>='
39
+ - - ">="
39
40
  - !ruby/object:Gem::Version
40
41
  version: '0'
41
- description: ! " This gem will parse the output of calling \"rspec --perform\",
42
- then will redistribute\n the spec files evenly, based on file run time, across
43
- all builds established\n in the travis.yml file.\n"
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: |2
57
+ This gem will parse the output of calling "rspec --perform", then will redistribute
58
+ the spec files evenly, based on file run time, across all builds established
59
+ in the travis.yml file.
44
60
  email:
45
61
  - mschmaus201@gmail.com
62
+ - jbazemore@greenhouse.io
46
63
  executables: []
47
64
  extensions: []
48
65
  extra_rdoc_files: []
49
- files: []
66
+ files:
67
+ - ".gitignore"
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - lib/spec_tiller.rb
73
+ - lib/spec_tiller/build_matrix_parser.rb
74
+ - lib/spec_tiller/distribute_spec_files.rb
75
+ - lib/spec_tiller/sync_spec_file_list.rb
76
+ - lib/spec_tiller/version.rb
77
+ - spec/build_matrix_parser_spec.rb
78
+ - spec/distribute_spec_files_spec.rb
79
+ - spec/documents/.travis.yml
80
+ - spec/documents/rspec_profile_results.txt
81
+ - spec/spec_helper.rb
82
+ - spec/sync_spec_file_list_spec.rb
83
+ - spec_tiller.gemspec
50
84
  homepage: ''
51
85
  licenses:
52
86
  - MIT
@@ -57,18 +91,24 @@ require_paths:
57
91
  - lib
58
92
  required_ruby_version: !ruby/object:Gem::Requirement
59
93
  requirements:
60
- - - ! '>='
94
+ - - ">="
61
95
  - !ruby/object:Gem::Version
62
96
  version: '0'
63
97
  required_rubygems_version: !ruby/object:Gem::Requirement
64
98
  requirements:
65
- - - ! '>='
99
+ - - ">="
66
100
  - !ruby/object:Gem::Version
67
101
  version: '0'
68
102
  requirements: []
69
103
  rubyforge_project:
70
- rubygems_version: 2.1.10
104
+ rubygems_version: 2.2.2
71
105
  signing_key:
72
106
  specification_version: 4
73
107
  summary: Distribute spec files evenly across Travis builds, based on file run time
74
- test_files: []
108
+ test_files:
109
+ - spec/build_matrix_parser_spec.rb
110
+ - spec/distribute_spec_files_spec.rb
111
+ - spec/documents/.travis.yml
112
+ - spec/documents/rspec_profile_results.txt
113
+ - spec/spec_helper.rb
114
+ - spec/sync_spec_file_list_spec.rb