├── .github ├── assets │ ├── knapsack-diamonds.png │ ├── with_knapsack.png │ └── without_knapsack.png └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── MIGRATE_TO_KNAPSACK_PRO.md ├── README.md ├── Rakefile ├── bin └── knapsack ├── docs └── images │ ├── logos │ ├── knapsack-@2.png │ ├── knapsack-big.png │ ├── knapsack-logo-@2.png │ ├── knapsack-logo-big.png │ ├── knapsack-logo.png │ └── knapsack.png │ ├── with_knapsack.png │ └── without_knapsack.png ├── knapsack.gemspec ├── knapsack_minitest_report.json ├── knapsack_rspec_report.json ├── knapsack_spinach_report.json ├── lib ├── knapsack.rb ├── knapsack │ ├── adapters │ │ ├── base_adapter.rb │ │ ├── cucumber_adapter.rb │ │ ├── minitest_adapter.rb │ │ ├── rspec_adapter.rb │ │ └── spinach_adapter.rb │ ├── allocator.rb │ ├── allocator_builder.rb │ ├── config │ │ ├── env.rb │ │ └── tracker.rb │ ├── distributors │ │ ├── base_distributor.rb │ │ ├── leftover_distributor.rb │ │ └── report_distributor.rb │ ├── extensions │ │ └── time.rb │ ├── logger.rb │ ├── presenter.rb │ ├── report.rb │ ├── runners │ │ ├── cucumber_runner.rb │ │ ├── minitest_runner.rb │ │ ├── rspec_runner.rb │ │ └── spinach_runner.rb │ ├── task_loader.rb │ ├── tracker.rb │ └── version.rb └── tasks │ ├── knapsack_cucumber.rake │ ├── knapsack_minitest.rake │ ├── knapsack_rspec.rake │ └── knapsack_spinach.rake ├── spec ├── knapsack │ ├── adapters │ │ ├── base_adapter_spec.rb │ │ ├── cucumber_adapter_spec.rb │ │ ├── minitest_adapter_spec.rb │ │ ├── rspec_adapter_spec.rb │ │ └── spinach_adapter_spec.rb │ ├── allocator_builder_spec.rb │ ├── allocator_spec.rb │ ├── config │ │ ├── env_spec.rb │ │ └── tracker_spec.rb │ ├── distributors │ │ ├── base_distributor_spec.rb │ │ ├── leftover_distributor_spec.rb │ │ └── report_distributor_spec.rb │ ├── extensions │ │ └── time_spec.rb │ ├── logger_spec.rb │ ├── presenter_spec.rb │ ├── report_spec.rb │ ├── task_loader_spec.rb │ └── tracker_spec.rb ├── knapsack_spec.rb ├── spec_helper.rb └── support │ ├── env_helper.rb │ ├── fakes │ ├── cucumber.rb │ └── minitest.rb │ └── shared_examples │ └── adapter.rb ├── spec_engine_examples └── 1_spec.rb ├── spec_examples ├── fast │ ├── 1_spec.rb │ ├── 2_spec.rb │ ├── 3_spec.rb │ ├── 4_spec.rb │ ├── 5_spec.rb │ ├── 6_spec.rb │ └── use_shared_example_spec.rb ├── leftover │ ├── 1_spec.rb │ └── a_spec.rb ├── slow │ ├── a_spec.rb │ ├── b_spec.rb │ └── c_spec.rb ├── spec_helper.rb └── support │ └── shared_examples │ └── common_example.rb ├── spinach_examples ├── scenario1.feature ├── scenario2.feature ├── steps │ ├── test_how_spinach_works_for_first_test.rb │ └── test_how_spinach_works_for_second_test.rb └── support │ └── env.rb └── test_examples ├── fast ├── shared_examples_test.rb ├── spec_test.rb └── unit_test.rb ├── slow └── slow_test.rb └── test_helper.rb /.github/assets/knapsack-diamonds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/.github/assets/knapsack-diamonds.png -------------------------------------------------------------------------------- /.github/assets/with_knapsack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/.github/assets/with_knapsack.png -------------------------------------------------------------------------------- /.github/assets/without_knapsack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/.github/assets/without_knapsack.png -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0' 17 | ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0', 3.2, truffleruby-head] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 26 | 27 | # Test for knapsack gem 28 | - name: Run specs for Knapsack gem 29 | run: bundle exec rspec spec 30 | 31 | # Tests for example rspec test suite 32 | - name: Generate knapsack report 33 | run: KNAPSACK_GENERATE_REPORT=true bundle exec rspec --default-path spec_examples --tag focus 34 | 35 | - name: Run specs with enabled time offset warning 36 | run: bundle exec rspec --default-path spec_examples 37 | 38 | - name: Run rake task for the first CI node 39 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=0 KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 40 | - name: Run rake task for the second CI node 41 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=1 KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 42 | 43 | - name: Check passing arguments to rspec. Run only specs with custom_focus tag (1/2) 44 | run: KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake "knapsack:rspec[--tag custom_focus]" 45 | - name: Check passing arguments to rspec. Run only specs with custom_focus tag (2/2) 46 | run: KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bin/knapsack rspec "--tag custom_focus --profile" 47 | 48 | - name: Run specs with custom knapsack logger 49 | run: CUSTOM_LOGGER=true KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 50 | 51 | - name: Run specs for custom knapsack report path 52 | run: | 53 | cp knapsack_rspec_report.json knapsack_custom_rspec_report.json 54 | KNAPSACK_REPORT_PATH="knapsack_custom_rspec_report.json" KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 55 | 56 | - name: Run specs when spec file was removed and still exists in knapsack report json 57 | run: | 58 | rm spec_examples/fast/1_spec.rb 59 | KNAPSACK_TEST_FILE_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 60 | 61 | - name: Run specs from multiple directories with manually specified test_dir 62 | run: KNAPSACK_TEST_DIR=spec_examples KNAPSACK_TEST_FILE_PATTERN="{spec_examples,spec_engine_examples}/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 63 | 64 | # Tests for example minitest test suite 65 | - name: Generate knapsack report 66 | run: KNAPSACK_GENERATE_REPORT=true bundle exec rake test 67 | 68 | - name: Run tests with enabled time offset warning 69 | run: bundle exec rake test 70 | 71 | - name: Run rake task for the first CI node 72 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=0 KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake knapsack:minitest 73 | - name: Run rake task for the second CI node 74 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=1 KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake knapsack:minitest 75 | 76 | - name: Check passing arguments to minitest. Run verbose tests 77 | run: | 78 | KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake "knapsack:minitest[--verbose]" 79 | KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bin/knapsack minitest "--verbose --pride" 80 | 81 | - name: Run tests with custom knapsack logger 82 | run: CUSTOM_LOGGER=true KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake knapsack:minitest 83 | 84 | - name: Run tests for custom knapsack report path 85 | run: | 86 | cp knapsack_minitest_report.json knapsack_custom_minitest_report.json 87 | KNAPSACK_REPORT_PATH="knapsack_custom_minitest_report.json" KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake knapsack:minitest 88 | 89 | - name: Run tests when test file was removed and still exists in knapsack report json 90 | run: | 91 | rm test_examples/fast/unit_test.rb 92 | KNAPSACK_TEST_FILE_PATTERN="test_examples/**{,/*/**}/*_test.rb" bundle exec rake knapsack:minitest 93 | 94 | # Tests for example spinach test suite 95 | - name: Generate knapsack report 96 | run: KNAPSACK_GENERATE_REPORT=true bundle exec spinach -f spinach_examples 97 | 98 | - name: Run tests with enabled time offset warning 99 | run: bundle exec spinach -f spinach_examples 100 | 101 | - name: Run rake task for the first CI node 102 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=0 KNAPSACK_TEST_FILE_PATTERN="spinach_examples/**{,/*/**}/*.feature" bundle exec rake "knapsack:spinach[-f spinach_examples]" 103 | - name: Run rake task for the second CI node 104 | run: CI_NODE_TOTAL=2 CI_NODE_INDEX=1 KNAPSACK_TEST_FILE_PATTERN="spinach_examples/**{,/*/**}/*.feature" bundle exec rake "knapsack:spinach[-f spinach_examples]" 105 | 106 | - name: Run tests with custom knapsack logger 107 | run: CUSTOM_LOGGER=true KNAPSACK_TEST_FILE_PATTERN="spinach_examples/**{,/*/**}/*.feature" bundle exec rake "knapsack:spinach[-f spinach_examples]" 108 | 109 | - name: Run tests for custom knapsack report path 110 | run: | 111 | cp knapsack_spinach_report.json knapsack_custom_spinach_report.json 112 | KNAPSACK_REPORT_PATH="knapsack_custom_spinach_report.json" KNAPSACK_TEST_FILE_PATTERN="spinach_examples/**{,/*/**}/*.feature" bundle exec rake "knapsack:spinach[-f spinach_examples]" 113 | 114 | - name: Run tests when test file was removed and still exists in knapsack report json 115 | run: | 116 | rm spinach_examples/scenario1.feature 117 | KNAPSACK_TEST_FILE_PATTERN="spinach_examples/**{,/*/**}/*.feature" bundle exec rake "knapsack:spinach[-f spinach_examples]" 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | .idea/ 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | #--warnings 3 | --require spec_helper 4 | --format documentation 5 | #--profile 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### unreleased 2 | 3 | * TODO 4 | 5 | ### 4.0.0 6 | 7 | * __(breaking change)__ Remove support for RSpec 2.x. This change was already done by accident in [the pull request](https://github.com/KnapsackPro/knapsack/pull/107) when we added the RSpec `context` hook, which is available only since RSpec 3.x. 8 | * Use RSpec `example` block argument instead of the global `RSpec.current_example`. This allows to run tests with the `async-rspec` gem. 9 | 10 | https://github.com/KnapsackPro/knapsack/pull/117 11 | 12 | https://github.com/KnapsackPro/knapsack/compare/v3.1.0...v4.0.0 13 | 14 | ### 3.1.0 15 | 16 | * Sorting Algorithm: round robin to least connections 17 | 18 | https://github.com/KnapsackPro/knapsack/pull/99 19 | 20 | https://github.com/KnapsackPro/knapsack/compare/v3.0.0...v3.1.0 21 | 22 | ### 3.0.0 23 | 24 | * __(breaking change)__ Require minimum Ruby 2.2 version 25 | 26 | https://github.com/KnapsackPro/knapsack/pull/115 27 | 28 | * __(breaking change)__ Drop support for Minitest 4.x. Force to use minitest 5.x even on CI. 29 | 30 | https://github.com/KnapsackPro/knapsack/pull/114 31 | 32 | * Replace Travis CI with GitHub Actions 33 | 34 | https://github.com/KnapsackPro/knapsack/pull/112 35 | 36 | https://github.com/KnapsackPro/knapsack/compare/v2.0.0...v3.0.0 37 | 38 | ### 2.0.0 39 | 40 | * __(breaking change)__ Ruby 2.1 is a minimum required version 41 | 42 | https://github.com/KnapsackPro/knapsack/pull/113 43 | 44 | * Use Ruby 3 and add small development adjustments in codebase 45 | 46 | https://github.com/KnapsackPro/knapsack/pull/110 47 | 48 | https://github.com/KnapsackPro/knapsack/compare/v1.22.0...v2.0.0 49 | 50 | ### 1.22.0 51 | 52 | * Update time offset warning 53 | 54 | https://github.com/KnapsackPro/knapsack/pull/105 55 | 56 | https://github.com/KnapsackPro/knapsack/compare/v1.21.1...v1.22.0 57 | 58 | ### 1.21.1 59 | 60 | * Fix a bug with tracking time for pending specs in RSpec 61 | 62 | https://github.com/KnapsackPro/knapsack/pull/109 63 | 64 | https://github.com/KnapsackPro/knapsack/compare/v1.21.0...v1.21.1 65 | 66 | ### 1.21.0 67 | 68 | * Track time in before and after `:context` hooks 69 | 70 | https://github.com/KnapsackPro/knapsack/pull/107 71 | 72 | https://github.com/KnapsackPro/knapsack/compare/v1.20.0...v1.21.0 73 | 74 | ### 1.20.0 75 | 76 | * Use `Process.clock_gettime` to measure track execution time 77 | 78 | https://github.com/KnapsackPro/knapsack/pull/100 79 | 80 | https://github.com/KnapsackPro/knapsack/compare/v1.19.0...v1.20.0 81 | 82 | ### 1.19.0 83 | 84 | * Add support for Bitbucket Pipelines 85 | 86 | https://github.com/KnapsackPro/knapsack/pull/97 87 | 88 | https://github.com/KnapsackPro/knapsack/compare/v1.18.0...v1.19.0 89 | 90 | ### 1.18.0 91 | 92 | * Add support for Semaphore 2.0 93 | 94 | https://github.com/KnapsackPro/knapsack/pull/92 95 | 96 | https://github.com/KnapsackPro/knapsack/compare/v1.17.2...v1.18.0 97 | 98 | ### 1.17.2 99 | 100 | * Allow for new `bundler` in development 101 | * Test Ruby 2.6 on CI 102 | * Add info about Knapsack Pro Queue Mode in knapsack output 103 | * Update URL to FAQ in knapsack output 104 | 105 | https://github.com/KnapsackPro/knapsack/pull/90 106 | 107 | https://github.com/KnapsackPro/knapsack/compare/v1.17.1...v1.17.2 108 | 109 | ### 1.17.1 110 | 111 | * Fix RSpec signal handling by replacing process 112 | 113 | https://github.com/KnapsackPro/knapsack/pull/86 114 | 115 | https://github.com/KnapsackPro/knapsack/compare/v1.17.0...v1.17.1 116 | 117 | ### 1.17.0 118 | 119 | * Add support for GitLab CI ENV variable `CI_NODE_INDEX` starting from 1. 120 | 121 | https://github.com/KnapsackPro/knapsack/pull/83 122 | 123 | https://github.com/KnapsackPro/knapsack/compare/v1.16.0...v1.17.0 124 | 125 | ### 1.16.0 126 | 127 | * Add support for Ruby >= 1.9.3. 128 | 129 | https://github.com/KnapsackPro/knapsack/pull/77 130 | 131 | https://github.com/KnapsackPro/knapsack/compare/v1.15.0...v1.16.0 132 | 133 | ### 1.15.0 134 | 135 | * Add support for Cucumber 3. 136 | 137 | https://github.com/KnapsackPro/knapsack/pull/68 138 | 139 | https://github.com/KnapsackPro/knapsack/compare/v1.14.1...v1.15.0 140 | 141 | ### 1.14.1 142 | 143 | * Update RSpec timing adapter to be more resilient. 144 | 145 | https://github.com/KnapsackPro/knapsack/pull/64 146 | 147 | https://github.com/KnapsackPro/knapsack/compare/v1.14.0...v1.14.1 148 | 149 | ### 1.14.0 150 | 151 | * Moves Timecop to development dependency. 152 | 153 | https://github.com/KnapsackPro/knapsack/pull/61 154 | 155 | https://github.com/KnapsackPro/knapsack/compare/v1.13.3...v1.14.0 156 | 157 | ### 1.13.3 158 | 159 | * Fix: Trailing slash should be removed from allocator test_dir. 160 | 161 | https://github.com/KnapsackPro/knapsack/issues/57 162 | 163 | https://github.com/KnapsackPro/knapsack/compare/v1.13.2...v1.13.3 164 | 165 | ### 1.13.2 166 | 167 | * Add support for test files in directory with spaces. 168 | 169 | Related: 170 | https://github.com/KnapsackPro/knapsack_pro-ruby/issues/27 171 | 172 | https://github.com/KnapsackPro/knapsack/compare/v1.13.1...v1.13.2 173 | 174 | ### 1.13.1 175 | 176 | * Fix: Get rid of call #zero? method on $?.exitstatus in test runners tasks 177 | 178 | https://github.com/KnapsackPro/knapsack/pull/52 179 | 180 | https://github.com/KnapsackPro/knapsack/compare/v1.13.0...v1.13.1 181 | 182 | ### 1.13.0 183 | 184 | * Add KNAPSACK_LOG_LEVEL option 185 | 186 | https://github.com/KnapsackPro/knapsack/pull/49 187 | 188 | https://github.com/KnapsackPro/knapsack/compare/v1.12.2...v1.13.0 189 | 190 | ### 1.12.2 191 | 192 | * Fix support for turnip >= 2.x 193 | 194 | https://github.com/KnapsackPro/knapsack/pull/47 195 | 196 | https://github.com/KnapsackPro/knapsack/compare/v1.12.1...v1.12.2 197 | 198 | ### 1.12.1 199 | 200 | * Cucumber and Spinach should load files from proper folder in case when you use custom test directory. 201 | 202 | https://github.com/KnapsackPro/knapsack/compare/v1.12.0...v1.12.1 203 | 204 | ### 1.12.0 205 | 206 | * Add support for Minitest::SharedExamples 207 | 208 | https://github.com/KnapsackPro/knapsack/pull/46 209 | 210 | https://github.com/KnapsackPro/knapsack/compare/v1.11.1...v1.12.0 211 | 212 | ### 1.11.1 213 | 214 | * Require spinach in spec helper so tests will pass but don't require it in spinach adapter because it breaks for users who don't use spinach and they don't want to add it to their Gemfile 215 | 216 | Related PR: 217 | https://github.com/KnapsackPro/knapsack/pull/41 218 | 219 | https://github.com/KnapsackPro/knapsack/compare/v1.11.0...v1.11.1 220 | 221 | ### 1.11.0 222 | 223 | * Add support for Spinach 224 | 225 | https://github.com/KnapsackPro/knapsack/pull/41 226 | 227 | https://github.com/KnapsackPro/knapsack/compare/v1.10.0...v1.11.0 228 | 229 | ### 1.10.0 230 | 231 | * Log the time offset warning at INFO if time not exceeded 232 | 233 | https://github.com/KnapsackPro/knapsack/pull/40 234 | 235 | https://github.com/KnapsackPro/knapsack/compare/v1.9.0...v1.10.0 236 | 237 | ### 1.9.0 238 | 239 | * Use Knapsack.logger for runner output 240 | 241 | https://github.com/KnapsackPro/knapsack/pull/39 242 | 243 | https://github.com/KnapsackPro/knapsack/compare/v1.8.0...v1.9.0 244 | 245 | ### 1.8.0 246 | 247 | * Add support for older cucumber versions than 1.3 248 | 249 | https://github.com/KnapsackPro/knapsack_pro-ruby/issues/5 250 | 251 | https://github.com/KnapsackPro/knapsack/compare/v1.7.0...v1.8.0 252 | 253 | ### 1.7.0 254 | 255 | * Add ability to run tests from multiple directories 256 | 257 | https://github.com/KnapsackPro/knapsack/pull/35 258 | 259 | https://github.com/KnapsackPro/knapsack/compare/v1.6.1...v1.7.0 260 | 261 | ### 1.6.1 262 | 263 | * Changed rake task in minitest_runner.rb to have no warnings output 264 | 265 | https://github.com/KnapsackPro/knapsack_pro-ruby/pull/4 266 | 267 | https://github.com/KnapsackPro/knapsack/compare/v1.6.0...v1.6.1 268 | 269 | ### 1.6.0 270 | 271 | * Add support for Cucumber 2 272 | 273 | https://github.com/KnapsackPro/knapsack/issues/30 274 | 275 | https://github.com/KnapsackPro/knapsack/compare/v1.5.1...v1.6.0 276 | 277 | ### 1.5.1 278 | 279 | * Add link to FAQ at the end of time offset warning 280 | 281 | https://github.com/KnapsackPro/knapsack/compare/v1.5.0...v1.5.1 282 | 283 | ### 1.5.0 284 | 285 | * Add support for snap-ci.com 286 | 287 | https://github.com/KnapsackPro/knapsack/compare/v1.4.1...v1.5.0 288 | 289 | ### 1.4.1 290 | 291 | * Update test file pattern in tests also. Related PR https://github.com/KnapsackPro/knapsack/pull/27 292 | * Ensure there are no duplicates in leftover tests because of new test file pattern 293 | 294 | https://github.com/KnapsackPro/knapsack/compare/v1.4.0...v1.4.1 295 | 296 | ### 1.4.0 297 | 298 | * Rename RspecAdapter to RSpecAdapter so that it is consistent 299 | 300 | https://github.com/KnapsackPro/knapsack/pull/28 301 | 302 | * Change file path patterns to support 1-level symlinks by default 303 | 304 | https://github.com/KnapsackPro/knapsack/pull/27 305 | 306 | https://github.com/KnapsackPro/knapsack/compare/v1.3.4...v1.4.0 307 | 308 | ### 1.3.4 309 | 310 | * Make knapsack backwards compatible with earlier version of minitest 311 | 312 | https://github.com/KnapsackPro/knapsack/pull/26 313 | 314 | https://github.com/KnapsackPro/knapsack/compare/v1.3.3...v1.3.4 315 | 316 | ### 1.3.3 317 | 318 | * Fix wrong dependency for timecop 319 | 320 | https://github.com/KnapsackPro/knapsack/compare/v1.3.2...v1.3.3 321 | 322 | ### 1.3.2 323 | 324 | * Use Timecop as dependency and always use Time.now_without_mock_time to avoid problem when someone did stub on Time without using Timecop. 325 | * Don't exit on successful RSpec and Cucumber runs 326 | 327 | https://github.com/KnapsackPro/knapsack/pull/25 328 | 329 | https://github.com/KnapsackPro/knapsack/compare/v1.3.1...v1.3.2 330 | 331 | ### 1.3.1 332 | 333 | * Treat KNAPSACK_GENERATE_REPORT=false as generate_report -> false 334 | 335 | https://github.com/KnapsackPro/knapsack/pull/22 336 | 337 | https://github.com/KnapsackPro/knapsack/compare/v1.3.0...v1.3.1 338 | 339 | ### 1.3.0 340 | 341 | * Add knapsack binary 342 | 343 | https://github.com/KnapsackPro/knapsack/pull/21 344 | 345 | https://github.com/KnapsackPro/knapsack/compare/v1.2.1...v1.3.0 346 | 347 | ### 1.2.1 348 | 349 | * Add support for Turnip features 350 | 351 | https://github.com/KnapsackPro/knapsack/pull/19 352 | 353 | https://github.com/KnapsackPro/knapsack/compare/v1.2.0...v1.2.1 354 | 355 | ### 1.2.0 356 | 357 | * Add minitest adapter. 358 | * Fix bug with missing global time execution when tests took less than second. 359 | 360 | https://github.com/KnapsackPro/knapsack/compare/v1.1.1...v1.2.0 361 | 362 | ### 1.1.1 363 | 364 | * Use `system` instead of `exec` in rake tasks so we can return exit code from command. 365 | 366 | https://github.com/KnapsackPro/knapsack/compare/v1.1.0...v1.1.1 367 | 368 | ### 1.1.0 369 | 370 | * Add support for Buildkite.com ENV variables `BUILDKITE_PARALLEL_JOB_COUNT` and `BUILDKITE_PARALLEL_JOB`. 371 | 372 | ### 1.0.4 373 | 374 | * Pull request #12 - Raise error when CI_NODE_INDEX >= CI_NODE_TOTAL 375 | 376 | https://github.com/KnapsackPro/knapsack/pull/12 377 | 378 | ### 1.0.3 379 | 380 | * Fix bug #11 - Track properly time when using Timecop gem in tests. 381 | 382 | https://github.com/KnapsackPro/knapsack/issues/11 383 | 384 | https://github.com/KnapsackPro/knapsack/issues/9 385 | 386 | ### 1.0.2 387 | 388 | * Fix bug #8 - Sort all tests just in case to avoid wrong order of files when running tests on machines where `Dir.glob` has different implementation. 389 | 390 | ### 1.0.1 391 | 392 | * Fix bug - Add support for Cucumber Scenario Outline. 393 | 394 | ### 1.0.0 395 | 396 | * Add cucumber support. 397 | * Rename environment variable KNAPSACK_SPEC_PATTERN to KNAPSACK_TEST_FILE_PATTERN. 398 | * Default name of knapsack report json file is based on adapter name so for RSpec the default report name is `knapsack_rspec_report.json` and for Cucumber the report name is `knapsack_cucumber_report.json`. 399 | 400 | ### 0.5.0 401 | 402 | * Allow passing arguments to rspec via knapsack:rspec task. 403 | 404 | ### 0.4.0 405 | 406 | * Add support for RSpec 2. 407 | 408 | ### 0.3.0 409 | 410 | * Add support for semaphoreapp.com thread ENV variables. 411 | 412 | ### 0.2.0 413 | 414 | * Add knapsack logger. Allow to use custom logger. 415 | 416 | ### 0.1.4 417 | 418 | * Fix wrong time presentation for negative seconds. 419 | 420 | ### 0.1.3 421 | 422 | * Better time presentation instead of seconds. 423 | 424 | ### 0.1.2 425 | 426 | * Fix case when someone removes spec file which exists in knapsack report. 427 | * Extract config to separate class and fix wrong node time execution on CI. 428 | 429 | ### 0.1.1 430 | 431 | * Fix assigning time execution to right spec file when call RSpec shared example. 432 | 433 | ### 0.1.0 434 | 435 | * Gem ready to use it! 436 | 437 | ### 0.0.3 438 | 439 | * Test release. Not ready to use it. 440 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in knapsack.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Artur Trzop 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. 23 | -------------------------------------------------------------------------------- /MIGRATE_TO_KNAPSACK_PRO.md: -------------------------------------------------------------------------------- 1 | # Migration steps: from Knapsack to Knapsack Pro 2 | 3 | Follow these steps to migrate from `knapsack` to `knapsack_pro` in 10 minutes. 4 | 5 | Commands are provided to help you with each step. 6 | 7 | > [!TIP] 8 | > On Linux, you need to remove the `''` part from the `sed` commands. Also, you can ignore the `sed: no input files` warning that is printed when there's no substitution to perform. 9 | 10 | ## Steps 11 | 12 | - [ ] Remove the `knapsack` gem: 13 | ```bash 14 | bundle remove knapsack 15 | ``` 16 | 17 | - [ ] Remove `Knapsack.load_tasks` from the `Rakefile` if present: 18 | ```bash 19 | sed -i '' '/Knapsack\.load_tasks/d' Rakefile 20 | ``` 21 | 22 | - [ ] Replace `require "knapsack"` with `require "knapsack_pro"`: 23 | ```bash 24 | grep --files-with-matches --recursive "require.*knapsack" . | xargs sed -i '' "s/'knapsack'/'knapsack_pro'/g" 25 | grep --files-with-matches --recursive "require.*knapsack" . | xargs sed -i '' 's/"knapsack"/"knapsack_pro"/g' 26 | ``` 27 | 28 | - [ ] Remove the following code from the test runner configuration: 29 | ```diff 30 | - Knapsack.tracker.config({ 31 | - enable_time_offset_warning: true, 32 | - time_offset_in_seconds: 30 33 | - }) 34 | 35 | - Knapsack.report.config({ 36 | - test_file_pattern: 'spec/**{,/*/**}/*_spec.rb', # ⬅️ Take note of this one for later 37 | - report_path: 'knapsack_custom_report.json' 38 | - }) 39 | ``` 40 | 41 | - [ ] Replace `Knapsack` with `KnapsackPro`: 42 | ```bash 43 | grep --files-with-matches --recursive "Knapsack\." . | xargs sed -i '' 's/Knapsack\./KnapsackPro./g' 44 | grep --files-with-matches --recursive "Knapsack::" . | xargs sed -i '' 's/Knapsack::/KnapsackPro::/g' 45 | ``` 46 | 47 | - [ ] Rename `KnapsackPro::Adapters::RspecAdapter` to `KnapsackPro::Adapters::RSpecAdapter`: 48 | ```bash 49 | grep --files-with-matches --recursive "KnapsackPro::Adapters::RspecAdapter" . | xargs sed -i '' 's/RspecAdapter/RSpecAdapter/g' 50 | ``` 51 | 52 | - [ ] Remove any line that mentions `KNAPSACK_GENERATE_REPORT` or `KNAPSACK_REPORT_PATH`: 53 | ```bash 54 | grep --files-with-matches --recursive "KNAPSACK_GENERATE_REPORT" . | xargs sed -i '' '/KNAPSACK_GENERATE_REPORT/d' 55 | grep --files-with-matches --recursive "KNAPSACK_REPORT_PATH" . | xargs sed -i '' '/KNAPSACK_REPORT_PATH/d' 56 | ``` 57 | 58 | - [ ] Rename ENVs from `KNAPSACK_X` to `KNAPSACK_PRO_X`: 59 | ```bash 60 | grep --files-with-matches --recursive "KNAPSACK_" . | xargs sed -i '' 's/KNAPSACK_/KNAPSACK_PRO_/g' 61 | ``` 62 | 63 | - [ ] Remove all the reports: 64 | ```bash 65 | rm knapsack_*_report.json 66 | ``` 67 | 68 | - [ ] [Configure Knapsack Pro](https://docs.knapsackpro.com/knapsack_pro-ruby/guide/) 69 | 70 | - [ ] Ensure all the CI commands are updated: 71 | ```bash 72 | grep --files-with-matches --recursive "knapsack:spinach" . | xargs sed -i '' 's/knapsack:spinach/knapsack_pro:spinach/g' 73 | grep --files-with-matches --recursive "knapsack:" . | xargs sed -i '' 's/knapsack:/knapsack_pro:queue:/g' 74 | grep --files-with-matches --recursive "CI_NODE_TOTAL" . | xargs sed -i '' 's/CI_NODE_TOTAL/KNAPSACK_PRO_CI_NODE_TOTAL/g' 75 | grep --files-with-matches --recursive "CI_NODE_INDEX" . | xargs sed -i '' 's/CI_NODE_INDEX/KNAPSACK_PRO_CI_NODE_INDEX/g' 76 | ``` 77 | 78 | - [ ] If you removed `test_file_pattern` when deleting `Knapsack.report.config`, use [`KNAPSACK_PRO_TEST_FILE_PATTERN`](https://docs.knapsackpro.com/ruby/reference/#knapsack_pro_test_file_pattern) instead 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > Knapsack is [archived](https://knapsackpro.com/knapsack_gem?utm_source=github&utm_medium=readme&utm_campaign=knapsack_gem_archived&utm_content=warning_knapsack_gem). But [Knapsack Pro](https://knapsackpro.com?utm_source=github&utm_medium=readme&utm_campaign=knapsack_gem_archived&utm_content=warning_knapsack_pro) is available. 3 | > 4 | > Knapsack Pro comes with a free plan and discounts on paid plans for people coming from Knapsack (see [how to migrate in 10 minutes](./MIGRATE_TO_KNAPSACK_PRO.md)). 5 | > 6 | > This repository remains available to fork and the gem hosted on RubyGems, so your existing setup won't be affected. 7 | 8 |

9 | 10 | Knapsack 11 | 12 |

13 | 14 |

Speed up your tests

15 |

Run your 1-hour test suite in 2 minutes with optimal parallelisation on your existing CI infrastructure

16 | 17 | --- 18 | 19 |
20 | 21 | Gem Version 22 | 23 |
24 | 25 |
26 |
27 | 28 | Knapsack wraps your current test runner and works with your existing CI infrastructure to split tests optimally. 29 | 30 | It comes in two flavors, `knapsack` and `knapsack_pro`: 31 | 32 | | | `knapsack` | `knapsack_pro` | 33 | | ------------------------------- | ---------- | --------------------------------------- | 34 | | Free | ✅ | ✅ [Free plan](https://knapsackpro.com?utm_source=github&utm_medium=readme&utm_campaign=knapsack_gem_archived&utm_content=free_plan) | 35 | | Static split | ✅ | ✅ | 36 | | [Dynamic split](https://docs.knapsackpro.com/overview/#queue-mode-dynamic-split) | ❌ | ✅ | 37 | | [Split by test examples](https://docs.knapsackpro.com/ruby/split-by-test-examples/) | ❌ | ✅ | 38 | | Graphs, metrics, and stats | ❌ | ✅ | 39 | | Programming languages | 🤞 (Ruby) | ✅ (Ruby, Cypress, Jest, SDK/API) | 40 | | CI providers | 🤞 Limited | ✅ (All) | 41 | | [Heroku add-on](https://elements.heroku.com/addons/knapsack-pro) | ❌ | ✅ | 42 | | Automated execution time recording | ❌ | ✅ | 43 | | Test split based on most recent execution times | ❌ | ✅ | 44 | | Support for spot/preemptible CI nodes | ❌ | ✅ | 45 | | Additional features | ❌ | 🤘 ([Overview](https://docs.knapsackpro.com/overview/)) | 46 | | | [Install](#knapsack) | [Install](https://docs.knapsackpro.com) | 47 | 48 | ## Migrate from `knapsack` to `knapsack_pro` 49 | 50 | If you already use `knapsack` and want to give `knapsack_pro` a try, here's [how to migrate in 10 minutes](./MIGRATE_TO_KNAPSACK_PRO.md). 51 | 52 | ## `knapsack` 53 | 54 | Knapsack generates a test time execution report and uses it for future test runs. 55 | 56 | The `knapsack` gem supports: 57 | 58 | * [RSpec](http://rspec.info) 59 | * [Cucumber](https://cucumber.io) 60 | * [Minitest](http://docs.seattlerb.org/minitest/) 61 | * [Spinach](https://github.com/codegram/spinach) 62 | * [Turnip](https://github.com/jnicklas/turnip) 63 | 64 | ### Without Knapsack - bad test suite split 65 | 66 | ![Unbalanced CI nodes without Knapsack gem](./.github/assets/without_knapsack.png) 67 | 68 | ### With Knapsack - better test suite split 69 | 70 | ![Balanced CI nodes with Knapsack gem](./.github/assets/with_knapsack.png) 71 | 72 | ### Requirements 73 | 74 | `>= Ruby 2.1.0` 75 | 76 | --- 77 | 78 | 79 | 80 | 81 | - [Update](#update) 82 | - [Installation](#installation) 83 | - [Usage](#usage) 84 | - [Step for RSpec](#step-for-rspec) 85 | - [Step for Cucumber](#step-for-cucumber) 86 | - [Step for Minitest](#step-for-minitest) 87 | - [Step for Spinach](#step-for-spinach) 88 | - [Custom configuration](#custom-configuration) 89 | - [Common step](#common-step) 90 | - [Adding or removing tests](#adding-or-removing-tests) 91 | - [Set up your CI server](#set-up-your-ci-server) 92 | - [Info about ENV variables](#info-about-env-variables) 93 | - [Passing arguments to the Rake task](#passing-arguments-to-the-rake-task) 94 | - [Passing arguments to RSpec](#passing-arguments-to-rspec) 95 | - [Passing arguments to Cucumber](#passing-arguments-to-cucumber) 96 | - [Passing arguments to Minitest](#passing-arguments-to-minitest) 97 | - [Passing arguments to Spinach](#passing-arguments-to-spinach) 98 | - [Knapsack binary](#knapsack-binary) 99 | - [CircleCI](#circleci) 100 | - [Step 1](#step-1) 101 | - [Step 2](#step-2) 102 | - [Travis](#travis) 103 | - [Step 1](#step-1-1) 104 | - [Step 2](#step-2-1) 105 | - [Semaphore](#semaphore) 106 | - [Step 1](#step-1-2) 107 | - [Step 2](#step-2-2) 108 | - [Semaphore 2.0](#semaphore-20) 109 | - [Buildkite](#buildkite) 110 | - [Step 1](#step-1-3) 111 | - [Step 2](#step-2-3) 112 | - [GitLab CI](#gitlab-ci) 113 | - [Step 1](#step-1-4) 114 | - [Step 2](#step-2-4) 115 | - [Info for Jenkins](#info-for-jenkins) 116 | - [Info for BitBucket Pipelines](#info-for-bitbucket-pipelines) 117 | - [Step 1](#step-1-5) 118 | - [Step 2](#step-2-5) 119 | - [FAQ](#faq) 120 | - [What does time offset warning mean?](#what-does-time-offset-warning-mean) 121 | - [How to generate the Knapsack report?](#how-to-generate-the-knapsack-report) 122 | - [What does "leftover specs" mean?](#what-does-leftover-specs-mean) 123 | - [Why are there "leftover specs" after I generate a new report?](#why-are-there-leftover-specs-after-i-generate-a-new-report) 124 | - [How can I run tests from multiple directories?](#how-can-i-run-tests-from-multiple-directories) 125 | - [How to update the existing Knapsack report for a few test files?](#how-to-update-the-existing-knapsack-report-for-a-few-test-files) 126 | - [How to run tests for particular CI node in your development environment](#how-to-run-tests-for-particular-ci-node-in-your-development-environment) 127 | - [How can I change the log level?](#how-can-i-change-the-log-level) 128 | - [Gem tests](#gem-tests) 129 | - [Spec](#spec) 130 | - [Spec examples](#spec-examples) 131 | - [Acknowledgements](#acknowledgements) 132 | - [Mentions](#mentions) 133 | 134 | 135 | 136 | 137 | ## Update 138 | 139 | Please check [CHANGELOG.md](./CHANGELOG.md) before updating the gem. Knapsack follows [semantic versioning](http://semver.org). 140 | 141 | ## Installation 142 | 143 | Add these lines to your application's Gemfile: 144 | 145 | ```ruby 146 | group :test, :development do 147 | gem 'knapsack' 148 | end 149 | ``` 150 | 151 | And then execute: 152 | 153 | ```sh 154 | bundle 155 | ``` 156 | 157 | Add this line at the bottom of `Rakefile`: 158 | 159 | ```ruby 160 | Knapsack.load_tasks if defined?(Knapsack) 161 | ``` 162 | 163 | ## Usage 164 | 165 | Here's an example of a Rails app with Knapsack. 166 | 167 | [https://github.com/KnapsackPro/rails-app-with-knapsack](https://github.com/KnapsackPro/rails-app-with-knapsack) 168 | 169 | ### Step for RSpec 170 | 171 | Add at the beginning of your `spec_helper.rb`: 172 | 173 | ```ruby 174 | require 'knapsack' 175 | 176 | # CUSTOM_CONFIG_GOES_HERE 177 | 178 | Knapsack::Adapters::RSpecAdapter.bind 179 | ``` 180 | 181 | ### Step for Cucumber 182 | 183 | Create `features/support/knapsack.rb`: 184 | 185 | ```ruby 186 | require 'knapsack' 187 | 188 | # CUSTOM_CONFIG_GOES_HERE 189 | 190 | Knapsack::Adapters::CucumberAdapter.bind 191 | ``` 192 | 193 | ### Step for Minitest 194 | 195 | Add the Knapsack code after you load the app environment in `test/test_helper.rb`: 196 | 197 | ```ruby 198 | ENV['RAILS_ENV'] ||= 'test' 199 | require File.expand_path('../../config/environment', __FILE__) 200 | require 'rails/test_help' 201 | 202 | require 'knapsack' 203 | 204 | # CUSTOM_CONFIG_GOES_HERE 205 | 206 | knapsack_adapter = Knapsack::Adapters::MinitestAdapter.bind 207 | knapsack_adapter.set_test_helper_path(__FILE__) 208 | ``` 209 | 210 | ### Step for Spinach 211 | 212 | Create `features/support/env.rb`: 213 | 214 | ```ruby 215 | require 'knapsack' 216 | 217 | # CUSTOM_CONFIG_GOES_HERE 218 | 219 | Knapsack::Adapters::SpinachAdapter.bind 220 | ``` 221 | 222 | ### Custom configuration 223 | 224 | You can change the default Knapsack configuration for RSpec, Cucumber, Minitest, or Spinach tests. 225 | 226 | Here are some examples (that you can insert in `CUSTOM_CONFIG_GOES_HERE`): 227 | 228 | ```ruby 229 | Knapsack.tracker.config({ 230 | enable_time_offset_warning: true, 231 | time_offset_in_seconds: 30 232 | }) 233 | 234 | Knapsack.report.config({ 235 | test_file_pattern: 'spec/**{,/*/**}/*_spec.rb', # default value based on adapter 236 | report_path: 'knapsack_custom_report.json' 237 | }) 238 | 239 | # You can use your logger: 240 | require 'logger' 241 | Knapsack.logger = Logger.new(STDOUT) 242 | Knapsack.logger.level = Logger::INFO 243 | ``` 244 | 245 | ### Common step 246 | 247 | Generate the time execution report for your test files. Run the command below on one of your CI nodes: 248 | 249 | ```sh 250 | # Step for RSpec: 251 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 252 | 253 | # Step for Cucumber: 254 | KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features 255 | 256 | # Step for Minitest: 257 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 258 | 259 | # If you use Rails 5.0.x then run this instead: 260 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 261 | 262 | # If you use Rails >= 5.1's SystemTest, run both unit and system tests: 263 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system 264 | 265 | # Step for Spinach: 266 | KNAPSACK_GENERATE_REPORT=true bundle exec spinach 267 | ``` 268 | 269 | Commit the generated report `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json` into your repository. 270 | 271 | This report should be updated after you add a lot of new slow tests or you change existing ones, which causes a big time execution difference between CI nodes. 272 | 273 | You will get a time offset warning at the end of the RSpec/Cucumber/Minitest run, which reminds you when it’s a good time to regenerate the Knapsack report. 274 | 275 | `KNAPSACK_GENERATE_REPORT` is truthy with `"true"` or `0`. All other values are falsy, though [`"false"` and `1` are semantically preferrable](https://en.wikipedia.org/wiki/True_and_false_(commands)). 276 | 277 | #### Adding or removing tests 278 | 279 | There is no need to regenerate the report every time you add/remove test files. 280 | 281 | If you remove a test file, Knapsack will ignore its entry in the report. If you add a new test file that is not listed in the report, the test file will be assigned to one of the CI nodes. 282 | 283 | You'll want to regenerate your execution report whenever you remove or add a test file with a long time execution time that would affect one of the CI nodes. Knapsack warns you when it's a good time to regenerate the report. 284 | 285 | ## Set up your CI server 286 | 287 | On your CI server, run the following command for the first CI node (increase `CI_NODE_INDEX` for the next nodes): 288 | 289 | ```sh 290 | # Step for RSpec: 291 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:rspec 292 | 293 | # Step for Cucumber: 294 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:cucumber 295 | 296 | # Step for Minitest: 297 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:minitest 298 | 299 | # Step for Spinach: 300 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:spinach 301 | ``` 302 | 303 | You can add `KNAPSACK_TEST_FILE_PATTERN` if your tests are not in the default directory: 304 | 305 | ```sh 306 | # Step for RSpec: 307 | KNAPSACK_TEST_FILE_PATTERN="directory_with_specs/**{,/*/**}/*_spec.rb" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:rspec 308 | 309 | # Step for Cucumber: 310 | KNAPSACK_TEST_FILE_PATTERN="directory_with_features/**/*.feature" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:cucumber 311 | 312 | # Step for Minitest: 313 | KNAPSACK_TEST_FILE_PATTERN="directory_with_tests/**{,/*/**}/*_spec.rb" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:minitest 314 | 315 | # Step for Spinach: 316 | KNAPSACK_TEST_FILE_PATTERN="directory_with_features/**/*.feature" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:spinach 317 | ``` 318 | 319 | You can set `KNAPSACK_REPORT_PATH` if your Knapsack report was saved in a non-default location: 320 | 321 | ```sh 322 | # Step for RSpec: 323 | KNAPSACK_REPORT_PATH="knapsack_custom_report.json" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:rspec 324 | 325 | # Step for Cucumber: 326 | KNAPSACK_REPORT_PATH="knapsack_custom_report.json" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:cucumber 327 | 328 | # Step for Minitest: 329 | KNAPSACK_REPORT_PATH="knapsack_custom_report.json" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:minitest 330 | 331 | # Step for Spinach: 332 | KNAPSACK_REPORT_PATH="knapsack_custom_report.json" CI_NODE_TOTAL=2 CI_NODE_INDEX=0 bundle exec rake knapsack:spinach 333 | ``` 334 | 335 | ### Info about ENV variables 336 | 337 | `CI_NODE_TOTAL` - total number of CI nodes you have. 338 | 339 | `CI_NODE_INDEX` - index of the current CI node starting from 0 (ie, the second CI node should have `CI_NODE_INDEX=1`). 340 | 341 | Some CI providers like GitLab CI have the same name of environment variable like `CI_NODE_INDEX`, which starts from 1 instead of 0. Knapsack will automatically pick it up and change it from 1 to 0. 342 | 343 | ### Passing arguments to the Rake task 344 | 345 | #### Passing arguments to RSpec 346 | 347 | Knapsack allows you to pass arguments through to RSpec. For example, if you want to run only specs that have the tag `focus`. If you do this with RSpec directly, it would look like: 348 | 349 | ```sh 350 | bundle exec rake rspec --tag focus 351 | ``` 352 | 353 | To do this with Knapsack, you simply add your RSpec arguments as parameters to the Knapsack Rake task: 354 | 355 | ```sh 356 | bundle exec rake "knapsack:rspec[--tag focus]" 357 | ``` 358 | 359 | Remember that using tags to limit which specs get run will affect the time each file takes to run. One solution to this is to generate a new `knapsack_rspec_report.json` for the commonly run scenarios. 360 | 361 | #### Passing arguments to Cucumber 362 | 363 | ```sh 364 | bundle exec rake "knapsack:cucumber[--name feature]" 365 | ``` 366 | 367 | #### Passing arguments to Minitest 368 | 369 | ```sh 370 | bundle exec rake "knapsack:minitest[--arg_name value]" 371 | ``` 372 | 373 | For instance, to run verbose tests: 374 | 375 | ```sh 376 | bundle exec rake "knapsack:minitest[--verbose]" 377 | ``` 378 | 379 | #### Passing arguments to Spinach 380 | 381 | ```sh 382 | bundle exec rake "knapsack:spinach[--name feature]" 383 | ``` 384 | 385 | ### Knapsack binary 386 | 387 | You can install `knapsack` globally and use the binary: 388 | 389 | ```sh 390 | knapsack rspec "--tag custom_tag_name --profile" 391 | knapsack cucumber 392 | knapsack minitest "--verbose --pride" 393 | knapsack spinach "-f spinach_examples" 394 | ``` 395 | 396 | Here's an [example](https://github.com/KnapsackPro/knapsack/pull/21) when it might be useful. 397 | 398 | ### CircleCI 399 | 400 | If you are using circleci.com, you can omit `CI_NODE_TOTAL` and `CI_NODE_INDEX`. Knapsack will use the `CIRCLE_NODE_TOTAL` and `CIRCLE_NODE_INDEX` provided by CircleCI. 401 | 402 | Here is an example for test configuration in your `.circleci/config.yml` file: 403 | 404 | #### Step 1 405 | 406 | Run all the tests on a single CI node with the enabled report generator: 407 | 408 | ```yaml 409 | # CircleCI 2.0 410 | - run: 411 | name: Step for RSpec 412 | command: | 413 | # export is important here 414 | export KNAPSACK_GENERATE_REPORT=true 415 | bundle exec rspec spec 416 | 417 | - run: 418 | name: Step for Cucumber 419 | command: | 420 | # export is important here 421 | export KNAPSACK_GENERATE_REPORT=true 422 | bundle exec cucumber features 423 | 424 | - run: 425 | name: Step for Minitest 426 | command: | 427 | # export is important here 428 | export KNAPSACK_GENERATE_REPORT=true 429 | bundle exec rake test 430 | # For Rails 5.1 runs unit and system tests 431 | bundle exec rake test test:system 432 | 433 | - run: 434 | name: Step for Spinach 435 | command: | 436 | # export is important here 437 | export KNAPSACK_GENERATE_REPORT=true 438 | bundle exec rspec spinach 439 | ``` 440 | 441 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 442 | 443 | #### Step 2 444 | 445 | Update the test command and enable parallelism (remember to add additional containers for your project in the CircleCI settings): 446 | 447 | ```yaml 448 | # CircleCI 2.0 449 | - run: 450 | name: Step for RSpec 451 | command: bundle exec rake knapsack:rspec 452 | 453 | - run: 454 | name: Step for Cucumber 455 | command: bundle exec rake knapsack:cucumber 456 | 457 | - run: 458 | name: Step for Minitest 459 | command: bundle exec rake knapsack:minitest 460 | 461 | - run: 462 | name: Step for Spinach 463 | command: bundle exec rake knapsack:spinach 464 | ``` 465 | 466 | ### Travis 467 | 468 | #### Step 1 469 | 470 | Run all the tests on a single CI node with the enabled report generator. Edit `.travis.yml`: 471 | 472 | ```yaml 473 | script: 474 | # Step for RSpec: 475 | - "KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec" 476 | 477 | # Step for Cucumber: 478 | - "KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features" 479 | 480 | # Step for Minitest: 481 | - "KNAPSACK_GENERATE_REPORT=true bundle exec rake test" 482 | - "KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system" # For Rails 5.1 runs unit and system tests 483 | 484 | # Step for Spinach: 485 | - "KNAPSACK_GENERATE_REPORT=true bundle exec spinach" 486 | ``` 487 | 488 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 489 | 490 | #### Step 2 491 | 492 | You can parallelize your builds across virtual machines with the [Travis matrix feature](http://docs.travis-ci.com/user/speeding-up-the-build/#Parallelizing-your-builds-across-virtual-machines). Edit `.travis.yml`: 493 | 494 | ```yaml 495 | script: 496 | # Step for RSpec: 497 | - "bundle exec rake knapsack:rspec" 498 | 499 | # Step for Cucumber: 500 | - "bundle exec rake knapsack:cucumber" 501 | 502 | # Step for Minitest: 503 | - "bundle exec rake knapsack:minitest" 504 | 505 | # Step for Spinach: 506 | - "bundle exec rake knapsack:spinach" 507 | 508 | env: 509 | - CI_NODE_TOTAL=2 CI_NODE_INDEX=0 510 | - CI_NODE_TOTAL=2 CI_NODE_INDEX=1 511 | ``` 512 | 513 | If you want to have both global and matrix ENVs: 514 | 515 | ```yaml 516 | script: 517 | # Step for RSpec: 518 | - "bundle exec rake knapsack:rspec" 519 | 520 | # Step for Cucumber: 521 | - "bundle exec rake knapsack:cucumber" 522 | 523 | # Step for Minitest: 524 | - "bundle exec rake knapsack:minitest" 525 | 526 | # Step for Spinach: 527 | - "bundle exec rake knapsack:spinach" 528 | 529 | env: 530 | global: 531 | - RAILS_ENV=test 532 | - MY_GLOBAL_VAR=123 533 | - CI_NODE_TOTAL=2 534 | jobs: 535 | - CI_NODE_INDEX=0 536 | - CI_NODE_INDEX=1 537 | ``` 538 | 539 | Such configuration will generate a matrix with the two following rows: 540 | 541 | ```sh 542 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 RAILS_ENV=test MY_GLOBAL_VAR=123 543 | CI_NODE_TOTAL=2 CI_NODE_INDEX=1 RAILS_ENV=test MY_GLOBAL_VAR=123 544 | ``` 545 | 546 | More info about global and matrix ENV configuration in the [Travis docs](http://docs.travis-ci.com/user/build-configuration/#Environment-variables). 547 | 548 | ### Semaphore 549 | 550 | #### Step 1 551 | 552 | Run all the tests on a single CI node with the enabled report generator: 553 | 554 | ```sh 555 | # Step for RSpec 556 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 557 | 558 | # Step for Cucumber 559 | KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features 560 | 561 | # Step for Minitest 562 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 563 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system # For Rails 5.1 runs unit and system tests 564 | 565 | # Step for Spinach 566 | KNAPSACK_GENERATE_REPORT=true bundle exec spinach 567 | ``` 568 | 569 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 570 | 571 | #### Step 2 572 | 573 | ##### Semaphore 2.0 574 | 575 | Knapsack supports the environment variables provided by Semaphore CI 2.0. Edit `.semaphore/semaphore.yml`: 576 | 577 | ```yaml 578 | # .semaphore/semaphore.yml 579 | 580 | # Use the latest stable version of Semaphore 2.0 YML syntax: 581 | version: v1.0 582 | 583 | # Name your pipeline. In case you connect multiple pipelines with promotions, 584 | # the name will help you differentiate between, for example, a CI build phase 585 | # and delivery phases. 586 | name: Demo Rails 5 app 587 | 588 | # An agent defines the environment in which your code runs. 589 | # It is a combination of one of available machine types and operating 590 | # system images. 591 | # See https://docs.semaphoreci.com/article/20-machine-types 592 | # and https://docs.semaphoreci.com/article/32-ubuntu-1804-image 593 | agent: 594 | machine: 595 | type: e1-standard-2 596 | os_image: ubuntu1804 597 | 598 | # Blocks are the heart of a pipeline and are executed sequentially. 599 | # Each block has a task that defines one or more jobs. Jobs define the 600 | # commands to execute. 601 | # See https://docs.semaphoreci.com/article/62-concepts 602 | blocks: 603 | - name: Setup 604 | task: 605 | env_vars: 606 | - name: RAILS_ENV 607 | value: test 608 | jobs: 609 | - name: bundle 610 | commands: 611 | # Checkout code from Git repository. This step is mandatory if the 612 | # job is to work with your code. 613 | # Optionally you may use --use-cache flag to avoid roundtrip to 614 | # remote repository. 615 | # See https://docs.semaphoreci.com/article/54-toolbox-reference#libcheckout 616 | - checkout 617 | # Restore dependencies from cache. 618 | # Read about caching: https://docs.semaphoreci.com/article/54-toolbox-reference#cache 619 | - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH-,gems-master- 620 | # Set Ruby version: 621 | - sem-version ruby 2.6.1 622 | - bundle install --jobs=4 --retry=3 --path vendor/bundle 623 | # Store the latest version of dependencies in cache, 624 | # to be used in next blocks and future workflows: 625 | - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle 626 | 627 | - name: RSpec tests 628 | task: 629 | env_vars: 630 | - name: RAILS_ENV 631 | value: test 632 | - name: PGHOST 633 | value: 127.0.0.1 634 | - name: PGUSER 635 | value: postgres 636 | # This block runs two jobs in parallel and they both share common 637 | # setup steps. We can group them in a prologue. 638 | # See https://docs.semaphoreci.com/article/50-pipeline-yaml#prologue 639 | prologue: 640 | commands: 641 | - checkout 642 | - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH-,gems-master- 643 | # Start Postgres database service. 644 | # See https://docs.semaphoreci.com/article/54-toolbox-reference#sem-service 645 | - sem-service start postgres 646 | - sem-version ruby 2.6.1 647 | - bundle install --jobs=4 --retry=3 --path vendor/bundle 648 | - bundle exec rake db:setup 649 | 650 | jobs: 651 | - name: Run tests with Knapsack 652 | parallelism: 2 653 | commands: 654 | # Step for RSpec: 655 | - bundle exec rake knapsack:rspec 656 | # Step for Cucumber: 657 | - bundle exec rake knapsack:cucumber 658 | # Step for Minitest: 659 | - bundle exec rake knapsack:minitest 660 | # Step for Spinach: 661 | - bundle exec rake knapsack:spinach 662 | ``` 663 | 664 | ### Buildkite 665 | 666 | #### Step 1 667 | 668 | Run all the tests on a single CI node with the enabled report generator. Run the following commands locally: 669 | 670 | ```sh 671 | # Step for RSpec: 672 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 673 | 674 | # Step for Cucumber: 675 | KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features 676 | 677 | # Step for Minitest: 678 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 679 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system # For Rails 5.1 runs unit and system tests 680 | 681 | # Step for Spinach: 682 | KNAPSACK_GENERATE_REPORT=true bundle exec spinach 683 | ``` 684 | 685 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 686 | 687 | #### Step 2 688 | 689 | Knapsack supports the Buildkite ENVs `BUILDKITE_PARALLEL_JOB_COUNT` and `BUILDKITE_PARALLEL_JOB`. Just configure the parallelism parameter in your build step and run the appropriate command in your build: 690 | 691 | ```sh 692 | # Step for RSpec: 693 | bundle exec rake knapsack:rspec 694 | 695 | # Step for Cucumber: 696 | bundle exec rake knapsack:cucumber 697 | 698 | # Step for Minitest: 699 | bundle exec rake knapsack:minitest 700 | 701 | # Step for Spinach: 702 | bundle exec rake knapsack:spinach 703 | ``` 704 | 705 | When using the `docker-compose` plugin on Buildkite, you have to pass the following environment variables: 706 | 707 | ```yaml 708 | steps: 709 | - label: "Test" 710 | parallelism: 2 711 | plugins: 712 | - docker-compose#3.0.3: 713 | run: app 714 | # Use the proper Knapsack command for your test runner: 715 | command: bundle exec rake knapsack:rspec 716 | config: docker-compose.test.yml 717 | env: 718 | - BUILDKITE_PARALLEL_JOB_COUNT 719 | - BUILDKITE_PARALLEL_JOB 720 | ``` 721 | 722 | ### GitLab CI 723 | 724 | If you are using GitLab >= 11.5, you can omit `CI_NODE_TOTAL` and `CI_NODE_INDEX`. Knapsack will use the `CI_NODE_TOTAL` and `CI_NODE_INDEX` provided by GitLab if you use the [`parallel`](https://docs.gitlab.com/ee/ci/yaml/#parallel) option in GitLab CI. 725 | 726 | #### Step 1 727 | 728 | Run all the tests on a single CI node with the enabled report generator: 729 | 730 | ```yaml 731 | test: 732 | script: KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 733 | ``` 734 | 735 | Here are other commands you could use instead of RSpec: 736 | 737 | ```sh 738 | # Step for Cucumber 739 | KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features 740 | 741 | # Step for Minitest 742 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 743 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system # For Rails 5.1 runs unit and system tests 744 | 745 | # Step for Spinach 746 | KNAPSACK_GENERATE_REPORT=true bundle exec spinach 747 | ``` 748 | 749 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 750 | 751 | #### Step 2 752 | 753 | Update test command and [enable parallelism](https://docs.gitlab.com/ee/ci/yaml/#parallel) (remember to set the proper parallel value for your project): 754 | 755 | ```yaml 756 | test: 757 | script: bundle exec rake knapsack:rspec 758 | parallel: 2 759 | ``` 760 | 761 | Here are other commands you could use instead of Knapsack for RSpec: 762 | 763 | ```sh 764 | # Step for Cucumber 765 | bundle exec rake knapsack:cucumber 766 | 767 | # Step for Minitest 768 | bundle exec rake knapsack:minitest 769 | 770 | # Step for Spinach 771 | bundle exec rake knapsack:spinach 772 | ``` 773 | 774 | ### Info for Jenkins 775 | 776 | To run parallel jobs with Jenkins you should use Jenkins Pipeline. 777 | 778 | You can learn the basics in [Parallelism and Distributed Builds with Jenkins](https://www.cloudbees.com/blog/parallelism-and-distributed-builds-jenkins). 779 | 780 | Here is an example [`Jenkinsfile`](https://github.com/mknapik/jenkins-pipeline-knapsack/blob/master/Jenkinsfile) using Jenkins Pipeline and Knapsack. 781 | 782 | More tips can be found in this [issue](https://github.com/KnapsackPro/knapsack/issues/42). 783 | 784 | ### Info for BitBucket Pipelines 785 | 786 | #### Step 1 787 | 788 | Run all the tests on a single CI node with the enabled report generator. Run the following commands locally: 789 | 790 | ```sh 791 | # Step for RSpec 792 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 793 | 794 | # Step for Cucumber 795 | KNAPSACK_GENERATE_REPORT=true bundle exec cucumber features 796 | 797 | # Step for Minitest 798 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test 799 | KNAPSACK_GENERATE_REPORT=true bundle exec rake test test:system # For Rails 5.1 runs unit and system tests 800 | 801 | # Step for Spinach 802 | KNAPSACK_GENERATE_REPORT=true bundle exec spinach 803 | ``` 804 | 805 | After the tests pass, you should copy the Knapsack JSON report and commit it into your repository as `knapsack_rspec_report.json`, `knapsack_cucumber_report.json`, `knapsack_minitest_report.json` or `knapsack_spinach_report.json`. 806 | 807 | #### Step 2 808 | 809 | Knapsack supports BitBucket Pipelines ENVs `BITBUCKET_PARALLEL_STEP_COUNT` and `BITBUCKET_PARALLEL_STEP`. Just configure the parallelism parameter in your build step and run the appropriate command in your build: 810 | 811 | ```sh 812 | # Step for RSpec: 813 | bundle exec rake knapsack:rspec 814 | 815 | # Step for Cucumber: 816 | bundle exec rake knapsack:cucumber 817 | 818 | # Step for Minitest: 819 | bundle exec rake knapsack:minitest 820 | 821 | # Step for Spinach: 822 | bundle exec rake knapsack:spinach 823 | ``` 824 | 825 | ## FAQ 826 | 827 | ### What does time offset warning mean? 828 | 829 | At the end of a test run, you may see the following warning: 830 | 831 | ``` 832 | ========= Knapsack Time Offset Warning ========== 833 | Time offset: 30s 834 | Max allowed node time execution: 02m 30s 835 | Exceeded time: 37s 836 | ``` 837 | 838 | `Time offset: 30s` is the current time offset value (by default it's 30s). 839 | 840 | Let’s assume the whole test suite takes 4 minutes, and you split across 2 CI nodes. The optimal split would be 2 minutes per node. 841 | 842 | With `Time offset: 30s`, you'll see a warning to regenerate the Knapsack report when tests on single CI node take longer than 2 minutes and 30s. 843 | 844 | `Max allowed node time execution: 02m 30s` is the average time execution of tests per CI node + time offset. In this case, the average tests time execution per CI node is 2 minutes. 845 | 846 | `Exceeded time: 37s` means that tests on this particular CI node took 37s longer than `max allowed node time execution`. Sometimes this value is negative when tests are executed faster than `max allowed node time execution`. 847 | 848 | ### How to generate the Knapsack report? 849 | 850 | If you want to regenerate the report, take a look at [Common step](#common-step). 851 | 852 | ```sh 853 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec 854 | ``` 855 | 856 | On your development machine, the time execution might be different than CI. For this reason, you should generate the report on a single CI node. 857 | 858 | ### What does "leftover specs" mean? 859 | 860 | When you run your specs with Knapsack, you'll see in the output something like: 861 | 862 | ``` 863 | Report specs: 864 | spec/models/user_spec.rb 865 | spec/controllers/users_controller_spec.rb 866 | 867 | Leftover specs: 868 | spec/models/book_spec.rb 869 | spec/models/author_spec.rb 870 | ``` 871 | 872 | The leftover specs are the ones that don't have recorded time execution. 873 | 874 | The reason might be: 875 | 876 | * The test file was added after Knapsack generated the report 877 | * Empty spec file with no test cases 878 | 879 | Leftover specs are distributed across CI nodes based on file name instead of execution time (which is missing). 880 | 881 | If you have many leftover specs, you can [generate the Knapsack report again](#how-to-generate-the-knapsack-report) to improve the test distribution across CI nodes. 882 | 883 | ### Why are there "leftover specs" after I generate a new report? 884 | 885 | If the test file is empty or only contains pending tests, it cannot be recorded and will end up in leftover specs. 886 | 887 | ### How can I run tests from multiple directories? 888 | 889 | The test file pattern config option supports any glob pattern handled by [`Dir.glob`](http://ruby-doc.org/core-2.2.0/Dir.html#method-c-glob) and can be configured to pull test files from multiple directories. 890 | 891 | For example, you may want to use `"{spec,engines/**/spec}/**{,/*/**}/*_spec.rb"`. In this case, the test directory must also be specified manually using the `KNAPSACK_TEST_DIR` environment variable: 892 | 893 | ```sh 894 | KNAPSACK_TEST_DIR=spec KNAPSACK_TEST_FILE_PATTERN="{spec,engines/**/spec}/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 895 | ``` 896 | 897 | `KNAPSACK_TEST_DIR` will be the default path for RSpec, where `spec_helper.rb` is expected to be found. Ensure you require it in your test files this way: 898 | 899 | ```ruby 900 | # Good: 901 | require_relative 'spec_helper' 902 | 903 | # Bad - won't work: 904 | require 'spec_helper' 905 | ``` 906 | 907 | ### How to update the existing Knapsack report for a few test files? 908 | 909 | You may want to look at this [monkey patch](https://github.com/KnapsackPro/knapsack/issues/34). 910 | 911 | ### How to run tests for particular CI node in your development environment 912 | 913 | In your development environment, you can debug tests that were run on a particular CI node: 914 | 915 | ```sh 916 | CI_NODE_TOTAL=2 \ 917 | CI_NODE_INDEX=0 \ 918 | bundle exec rake "knapsack:rspec[--seed 123]" 919 | ``` 920 | 921 | ### How can I change the log level? 922 | 923 | You can change the log level by specifying the `KNAPSACK_LOG_LEVEL` environment variable: 924 | 925 | ```sh 926 | KNAPSACK_LOG_LEVEL=warn bundle exec rake knapsack:rspec 927 | ``` 928 | 929 | Available values are `debug`, `info`, and `warn`. The default log level is `info`. 930 | 931 | ## Gem tests 932 | 933 | ### Spec 934 | 935 | To run the specs for Knapsack: 936 | 937 | ```sh 938 | bundle exec rspec spec 939 | ``` 940 | 941 | ### Spec examples 942 | 943 | The directory `spec_examples` contains examples of fast and slow specs. 944 | 945 | To generate a new Knapsack report for specs with `focus` tag (only the specs in `spec_examples/leftover` have no `focus` tag): 946 | 947 | ```sh 948 | KNAPSACK_GENERATE_REPORT=true bundle exec rspec --default-path spec_examples --tag focus 949 | ``` 950 | 951 | **Warning:** The current `knapsack_rspec_report.json` file was generated for `spec_examples` excluding `spec_examples/leftover/` to see how leftover specs are badly distributed across CI nodes. 952 | 953 | To see specs distributed for the first CI node: 954 | 955 | ```sh 956 | CI_NODE_TOTAL=2 CI_NODE_INDEX=0 KNAPSACK_SPEC_PATTERN="spec_examples/**{,/*/**}/*_spec.rb" bundle exec rake knapsack:rspec 957 | ``` 958 | 959 | Specs in `spec_examples/leftover` take more than 3 seconds. This should cause a Knapsack time offset warning because we set `time_offset_in_seconds` to 3 in `spec_examples/spec_helper.rb`: 960 | 961 | ```sh 962 | bundle exec rspec --default-path spec_examples 963 | ``` 964 | 965 | ## Acknowledgements 966 | 967 | [Małgorzata Nowak](https://github.com/informatykgosia) for the beautiful logo. 968 | 969 | ## Mentions 970 | 971 | * Lunar Logic Blog | [Parallel your specs and don’t waste time](http://blog.lunarlogic.io/2014/parallel-your-specs-and-dont-waste-time/) 972 | * Travis CI | [Parallelizing RSpec and Cucumber on multiple VMs](http://docs.travis-ci.com/user/speeding-up-the-build/#Parallelizing-RSpec-and-Cucumber-on-multiple-VMs) 973 | * Buildkite | [Libraries](https://buildkite.com/docs/guides/parallelizing-builds#libraries) 974 | * CircleCI | [Test splitting documentation](https://circleci.com/docs/2.0/parallelism-faster-jobs/#other-ways-to-split-tests) 975 | * GitLab | [How we used parallel CI/CD jobs to increase our productivity](https://about.gitlab.com/blog/2021/01/20/using-run-parallel-jobs/) 976 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | require 'knapsack' 4 | 5 | Knapsack.load_tasks 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << 'test_examples' 9 | t.pattern = 'test_examples/**{,/*/**}/*_test.rb' 10 | end 11 | -------------------------------------------------------------------------------- /bin/knapsack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative '../lib/knapsack' 4 | 5 | runner = ARGV[0] 6 | arguments = ARGV[1] 7 | 8 | MAP = { 9 | 'rspec' => Knapsack::Runners::RSpecRunner, 10 | 'cucumber' => Knapsack::Runners::CucumberRunner, 11 | 'minitest' => Knapsack::Runners::MinitestRunner, 12 | 'spinach' => Knapsack::Runners::SpinachRunner, 13 | } 14 | 15 | runner_class = MAP[runner] 16 | 17 | if runner_class 18 | runner_class.run(arguments) 19 | else 20 | raise 'Undefined runner. Please provide runner name and optional arguments, for instance: knapsack rspec "--color --profile"' 21 | end 22 | -------------------------------------------------------------------------------- /docs/images/logos/knapsack-@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack-@2.png -------------------------------------------------------------------------------- /docs/images/logos/knapsack-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack-big.png -------------------------------------------------------------------------------- /docs/images/logos/knapsack-logo-@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack-logo-@2.png -------------------------------------------------------------------------------- /docs/images/logos/knapsack-logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack-logo-big.png -------------------------------------------------------------------------------- /docs/images/logos/knapsack-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack-logo.png -------------------------------------------------------------------------------- /docs/images/logos/knapsack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/logos/knapsack.png -------------------------------------------------------------------------------- /docs/images/with_knapsack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/with_knapsack.png -------------------------------------------------------------------------------- /docs/images/without_knapsack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KnapsackPro/knapsack/cfa60892a34b1007f961b4c85ea9fd1aa08460b1/docs/images/without_knapsack.png -------------------------------------------------------------------------------- /knapsack.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'knapsack/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "knapsack" 8 | spec.version = Knapsack::VERSION 9 | spec.authors = ["ArturT"] 10 | spec.email = ["arturtrzop@gmail.com"] 11 | spec.summary = %q{Knapsack splits tests across CI nodes and makes sure that tests will run comparable time on each node.} 12 | spec.description = %q{Parallel tests across CI server nodes based on each test file's time execution. It generates a test time execution report and uses it for future test runs.} 13 | spec.homepage = "https://github.com/KnapsackPro/knapsack" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = '>= 2.2' 22 | 23 | spec.add_dependency 'rake', '>= 0' 24 | 25 | spec.add_development_dependency 'bundler', '>= 1.6' 26 | spec.add_development_dependency 'rspec', '~> 3.0' 27 | spec.add_development_dependency 'rspec-its', '~> 1.3' 28 | spec.add_development_dependency 'cucumber', '>= 0' 29 | spec.add_development_dependency 'spinach', '>= 0.8' 30 | spec.add_development_dependency 'minitest', '>= 5.0.0' 31 | spec.add_development_dependency 'pry', '~> 0' 32 | spec.add_development_dependency 'timecop', '>= 0.9.4' 33 | end 34 | -------------------------------------------------------------------------------- /knapsack_minitest_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_examples/slow/slow_test.rb": 2.5080618858337402, 3 | "test_examples/fast/spec_test.rb": 0.00015115737915039062, 4 | "test_examples/fast/shared_examples_test.rb": 0.616192102432251, 5 | "test_examples/fast/unit_test.rb": 1.6202473640441895 6 | } -------------------------------------------------------------------------------- /knapsack_rspec_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_examples/fast/3_spec.rb": 7.82012939453125e-05, 3 | "spec_examples/fast/1_spec.rb": 1.5974044799804688e-05, 4 | "spec_examples/slow/c_spec.rb": 1.002314567565918, 5 | "spec_examples/slow/a_spec.rb": 1.6023659706115723, 6 | "spec_examples/fast/2_spec.rb": 6.985664367675781e-05, 7 | "spec_examples/slow/b_spec.rb": 0.9009926319122314, 8 | "spec_examples/fast/6_spec.rb": 0.00015687942504882812, 9 | "spec_examples/fast/4_spec.rb": 9.894371032714844e-05, 10 | "spec_examples/fast/use_shared_example_spec.rb": 0.10031008720397949, 11 | "spec_examples/fast/5_spec.rb": 0.00011754035949707031 12 | } -------------------------------------------------------------------------------- /knapsack_spinach_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "spinach_examples/scenario1.feature": 0.0017168521881103516, 3 | "spinach_examples/scenario2.feature": 0.002157926559448242 4 | } -------------------------------------------------------------------------------- /lib/knapsack.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require 'rake/testtask' 3 | require_relative 'knapsack/version' 4 | require_relative 'knapsack/extensions/time' 5 | require_relative 'knapsack/config/env' 6 | require_relative 'knapsack/config/tracker' 7 | require_relative 'knapsack/logger' 8 | require_relative 'knapsack/tracker' 9 | require_relative 'knapsack/presenter' 10 | require_relative 'knapsack/report' 11 | require_relative 'knapsack/allocator' 12 | require_relative 'knapsack/allocator_builder' 13 | require_relative 'knapsack/task_loader' 14 | require_relative 'knapsack/distributors/base_distributor' 15 | require_relative 'knapsack/distributors/report_distributor' 16 | require_relative 'knapsack/distributors/leftover_distributor' 17 | require_relative 'knapsack/adapters/base_adapter' 18 | require_relative 'knapsack/adapters/rspec_adapter' 19 | require_relative 'knapsack/adapters/cucumber_adapter' 20 | require_relative 'knapsack/adapters/minitest_adapter' 21 | require_relative 'knapsack/adapters/spinach_adapter' 22 | require_relative 'knapsack/runners/rspec_runner' 23 | require_relative 'knapsack/runners/cucumber_runner' 24 | require_relative 'knapsack/runners/minitest_runner' 25 | require_relative 'knapsack/runners/spinach_runner' 26 | 27 | module Knapsack 28 | class << self 29 | @@logger = nil 30 | 31 | def tracker 32 | Knapsack::Tracker.instance 33 | end 34 | 35 | def report 36 | Knapsack::Report.instance 37 | end 38 | 39 | def root 40 | File.expand_path('../..', __FILE__) 41 | end 42 | 43 | def load_tasks 44 | task_loader = Knapsack::TaskLoader.new 45 | task_loader.load_tasks 46 | end 47 | 48 | def logger 49 | return @@logger if @@logger 50 | log = Knapsack::Logger.new 51 | log.level = Knapsack::Config::Env.log_level 52 | @@logger = log 53 | end 54 | 55 | def logger=(value) 56 | @@logger = value 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/knapsack/adapters/base_adapter.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Adapters 3 | class BaseAdapter 4 | # Just examples, please overwrite constants in subclasses 5 | TEST_DIR_PATTERN = 'test/**{,/*/**}/*_test.rb' 6 | REPORT_PATH = 'knapsack_base_report.json' 7 | 8 | def self.bind 9 | adapter = new 10 | adapter.bind 11 | adapter 12 | end 13 | 14 | def bind 15 | update_report_config 16 | 17 | if tracker.config[:generate_report] 18 | Knapsack.logger.info 'Knapsack report generator started!' 19 | bind_time_tracker 20 | bind_report_generator 21 | elsif tracker.config[:enable_time_offset_warning] 22 | Knapsack.logger.info 'Knapsack time offset warning enabled!' 23 | bind_time_tracker 24 | bind_time_offset_warning 25 | else 26 | Knapsack.logger.warn 'Knapsack adapter is off!' 27 | end 28 | end 29 | 30 | def bind_time_tracker 31 | raise NotImplementedError 32 | end 33 | 34 | def bind_report_generator 35 | raise NotImplementedError 36 | end 37 | 38 | def bind_time_offset_warning 39 | raise NotImplementedError 40 | end 41 | 42 | private 43 | 44 | def tracker 45 | Knapsack.tracker 46 | end 47 | 48 | def update_report_config 49 | current_test_file_pattern = Knapsack.report.config[:test_file_pattern] 50 | current_report_path = Knapsack.report.config[:report_path] 51 | 52 | Knapsack.report.config({ 53 | test_file_pattern: Knapsack::Config::Env.test_file_pattern || current_test_file_pattern || self.class::TEST_DIR_PATTERN, 54 | report_path: Knapsack::Config::Env.report_path || current_report_path || self.class::REPORT_PATH 55 | }) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/knapsack/adapters/cucumber_adapter.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Adapters 3 | class CucumberAdapter < BaseAdapter 4 | TEST_DIR_PATTERN = 'features/**{,/*/**}/*.feature' 5 | REPORT_PATH = 'knapsack_cucumber_report.json' 6 | 7 | def bind_time_tracker 8 | Around do |object, block| 9 | Knapsack.tracker.test_path = CucumberAdapter.test_path(object) 10 | Knapsack.tracker.start_timer 11 | block.call 12 | Knapsack.tracker.stop_timer 13 | end 14 | 15 | ::Kernel.at_exit do 16 | Knapsack.logger.info(Presenter.global_time) 17 | end 18 | end 19 | 20 | def bind_report_generator 21 | ::Kernel.at_exit do 22 | Knapsack.report.save 23 | Knapsack.logger.info(Presenter.report_details) 24 | end 25 | end 26 | 27 | def bind_time_offset_warning 28 | ::Kernel.at_exit do 29 | Knapsack.logger.log( 30 | Presenter.time_offset_log_level, 31 | Presenter.time_offset_warning 32 | ) 33 | end 34 | end 35 | 36 | def self.test_path(object) 37 | if ::Cucumber::VERSION.to_i >= 2 38 | test_case = object 39 | test_case.location.file 40 | else 41 | if object.respond_to?(:scenario_outline) 42 | if object.scenario_outline.respond_to?(:feature) 43 | # Cucumber < 1.3 44 | object.scenario_outline.feature.file 45 | else 46 | # Cucumber >= 1.3 47 | object.scenario_outline.file 48 | end 49 | else 50 | if object.respond_to?(:feature) 51 | # Cucumber < 1.3 52 | object.feature.file 53 | else 54 | # Cucumber >= 1.3 55 | object.file 56 | end 57 | end 58 | end 59 | end 60 | 61 | private 62 | 63 | def Around(*tag_expressions, &proc) 64 | if ::Cucumber::VERSION.to_i >= 3 65 | ::Cucumber::Glue::Dsl.register_rb_hook('around', tag_expressions, proc) 66 | else 67 | ::Cucumber::RbSupport::RbDsl.register_rb_hook('around', tag_expressions, proc) 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/knapsack/adapters/minitest_adapter.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Adapters 3 | class MinitestAdapter < BaseAdapter 4 | TEST_DIR_PATTERN = 'test/**{,/*/**}/*_test.rb' 5 | REPORT_PATH = 'knapsack_minitest_report.json' 6 | @@parent_of_test_dir = nil 7 | 8 | # See how to write hooks and plugins 9 | # https://github.com/seattlerb/minitest/blob/master/lib/minitest/test.rb 10 | module BindTimeTrackerMinitestPlugin 11 | def before_setup 12 | super 13 | Knapsack.tracker.test_path = MinitestAdapter.test_path(self) 14 | Knapsack.tracker.start_timer 15 | end 16 | 17 | def after_teardown 18 | Knapsack.tracker.stop_timer 19 | super 20 | end 21 | end 22 | 23 | def bind_time_tracker 24 | ::Minitest::Test.send(:include, BindTimeTrackerMinitestPlugin) 25 | 26 | Minitest.after_run do 27 | Knapsack.logger.info(Presenter.global_time) 28 | end 29 | end 30 | 31 | def bind_report_generator 32 | Minitest.after_run do 33 | Knapsack.report.save 34 | Knapsack.logger.info(Presenter.report_details) 35 | end 36 | end 37 | 38 | def bind_time_offset_warning 39 | Minitest.after_run do 40 | Knapsack.logger.log( 41 | Presenter.time_offset_log_level, 42 | Presenter.time_offset_warning 43 | ) 44 | end 45 | end 46 | 47 | def set_test_helper_path(file_path) 48 | test_dir_path = File.dirname(file_path) 49 | @@parent_of_test_dir = File.expand_path('../', test_dir_path) 50 | end 51 | 52 | def self.test_path(obj) 53 | # Pick the first public method in the class itself, that starts with "test_" 54 | test_method_name = obj.public_methods(false).select{|m| m =~ /^test_/ }.first 55 | if test_method_name.nil? 56 | # case for shared examples 57 | method_object = obj.method(obj.location.sub(/.*?test_/, 'test_')) 58 | else 59 | method_object = obj.method(test_method_name) 60 | end 61 | full_test_path = method_object.source_location.first 62 | parent_of_test_dir_regexp = Regexp.new("^#{@@parent_of_test_dir}") 63 | test_path = full_test_path.gsub(parent_of_test_dir_regexp, '.') 64 | # test_path will look like ./test/dir/unit_test.rb 65 | test_path 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/knapsack/adapters/rspec_adapter.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Adapters 3 | class RSpecAdapter < BaseAdapter 4 | TEST_DIR_PATTERN = 'spec/**{,/*/**}/*_spec.rb' 5 | REPORT_PATH = 'knapsack_rspec_report.json' 6 | 7 | def bind_time_tracker 8 | ::RSpec.configure do |config| 9 | config.prepend_before(:context) do 10 | Knapsack.tracker.start_timer 11 | end 12 | 13 | config.prepend_before(:each) do |example| 14 | Knapsack.tracker.test_path = RSpecAdapter.test_path(example) 15 | end 16 | 17 | config.append_after(:context) do 18 | Knapsack.tracker.stop_timer 19 | end 20 | 21 | config.after(:suite) do 22 | Knapsack.logger.info(Presenter.global_time) 23 | end 24 | end 25 | end 26 | 27 | def bind_report_generator 28 | ::RSpec.configure do |config| 29 | config.after(:suite) do 30 | Knapsack.report.save 31 | Knapsack.logger.info(Presenter.report_details) 32 | end 33 | end 34 | end 35 | 36 | def bind_time_offset_warning 37 | ::RSpec.configure do |config| 38 | config.after(:suite) do 39 | Knapsack.logger.log( 40 | Presenter.time_offset_log_level, 41 | Presenter.time_offset_warning 42 | ) 43 | end 44 | end 45 | end 46 | 47 | def self.test_path(example) 48 | example_group = example.metadata[:example_group] 49 | 50 | if defined?(::Turnip) && Gem::Version.new(::Turnip::VERSION) < Gem::Version.new('2.0.0') 51 | unless example_group[:turnip] 52 | until example_group[:parent_example_group].nil? 53 | example_group = example_group[:parent_example_group] 54 | end 55 | end 56 | else 57 | until example_group[:parent_example_group].nil? 58 | example_group = example_group[:parent_example_group] 59 | end 60 | end 61 | 62 | example_group[:file_path] 63 | end 64 | end 65 | 66 | # This is added to provide backwards compatibility 67 | class RspecAdapter < RSpecAdapter 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/knapsack/adapters/spinach_adapter.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Adapters 3 | class SpinachAdapter < BaseAdapter 4 | TEST_DIR_PATTERN = 'features/**{,/*/**}/*.feature' 5 | REPORT_PATH = 'knapsack_spinach_report.json' 6 | 7 | def bind_time_tracker 8 | ::Spinach.hooks.before_scenario do |scenario_data, step_definitions| 9 | Knapsack.tracker.test_path = SpinachAdapter.test_path(scenario_data) 10 | Knapsack.tracker.start_timer 11 | end 12 | 13 | ::Spinach.hooks.after_scenario do 14 | Knapsack.tracker.stop_timer 15 | end 16 | 17 | ::Spinach.hooks.after_run do 18 | Knapsack.logger.info(Presenter.global_time) 19 | end 20 | end 21 | 22 | def bind_report_generator 23 | ::Spinach.hooks.after_run do 24 | Knapsack.report.save 25 | Knapsack.logger.info(Presenter.report_details) 26 | end 27 | end 28 | 29 | def bind_time_offset_warning 30 | ::Spinach.hooks.after_run do 31 | Knapsack.logger.log( 32 | Presenter.time_offset_log_level, 33 | Presenter.time_offset_warning 34 | ) 35 | end 36 | end 37 | 38 | def self.test_path(scenario) 39 | scenario.feature.filename 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/knapsack/allocator.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | class Allocator 3 | def initialize(args={}) 4 | @report_distributor = Knapsack::Distributors::ReportDistributor.new(args) 5 | @leftover_distributor = Knapsack::Distributors::LeftoverDistributor.new(args) 6 | end 7 | 8 | def report_node_tests 9 | @report_node_tests ||= @report_distributor.tests_for_current_node 10 | end 11 | 12 | def leftover_node_tests 13 | @leftover_node_tests ||= @leftover_distributor.tests_for_current_node 14 | end 15 | 16 | def node_tests 17 | @node_tests ||= report_node_tests + leftover_node_tests 18 | end 19 | 20 | def stringify_node_tests 21 | node_tests 22 | .map do |test_file| 23 | %{"#{test_file}"} 24 | end.join(' ') 25 | end 26 | 27 | def test_dir 28 | Knapsack::Config::Env.test_dir || @report_distributor.test_file_pattern.split('/').first 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/knapsack/allocator_builder.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | class AllocatorBuilder 3 | def initialize(adapter_class) 4 | @adapter_class = adapter_class 5 | set_report_path 6 | end 7 | 8 | def allocator 9 | Knapsack::Allocator.new({ 10 | report: Knapsack.report.open, 11 | test_file_pattern: test_file_pattern, 12 | ci_node_total: Knapsack::Config::Env.ci_node_total, 13 | ci_node_index: Knapsack::Config::Env.ci_node_index 14 | }) 15 | end 16 | 17 | def test_dir 18 | Knapsack::Config::Env.test_dir || test_file_pattern.split('/').first 19 | end 20 | 21 | private 22 | 23 | def set_report_path 24 | Knapsack.report.config({ 25 | report_path: report_path 26 | }) 27 | end 28 | 29 | def report_path 30 | Knapsack::Config::Env.report_path || @adapter_class::REPORT_PATH 31 | end 32 | 33 | def test_file_pattern 34 | Knapsack::Config::Env.test_file_pattern || @adapter_class::TEST_DIR_PATTERN 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/knapsack/config/env.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Config 3 | class Env 4 | class << self 5 | def report_path 6 | ENV['KNAPSACK_REPORT_PATH'] 7 | end 8 | 9 | def ci_node_total 10 | ENV['CI_NODE_TOTAL'] || ENV['CIRCLE_NODE_TOTAL'] || ENV['SEMAPHORE_JOB_COUNT'] || ENV['SEMAPHORE_THREAD_COUNT'] || ENV['BUILDKITE_PARALLEL_JOB_COUNT'] || ENV['SNAP_WORKER_TOTAL'] || ENV['BITBUCKET_PARALLEL_STEP_COUNT'] || 1 11 | end 12 | 13 | def ci_node_index 14 | gitlab_ci_node_index || ENV['CI_NODE_INDEX'] || ENV['CIRCLE_NODE_INDEX'] || semaphore_job_index || semaphore_current_thread || ENV['BUILDKITE_PARALLEL_JOB'] || snap_ci_worker_index || ENV['BITBUCKET_PARALLEL_STEP'] || 0 15 | end 16 | 17 | def test_file_pattern 18 | ENV['KNAPSACK_TEST_FILE_PATTERN'] 19 | end 20 | 21 | def test_dir 22 | ENV['KNAPSACK_TEST_DIR'] 23 | end 24 | 25 | def log_level 26 | { 27 | "debug" => Knapsack::Logger::DEBUG, 28 | "info" => Knapsack::Logger::INFO, 29 | "warn" => Knapsack::Logger::WARN, 30 | }[ENV['KNAPSACK_LOG_LEVEL']] || Knapsack::Logger::INFO 31 | end 32 | 33 | private 34 | 35 | def index_starting_from_one(index) 36 | index.to_i - 1 if index 37 | end 38 | 39 | def semaphore_job_index 40 | index_starting_from_one(ENV['SEMAPHORE_JOB_INDEX']) 41 | end 42 | 43 | def semaphore_current_thread 44 | index_starting_from_one(ENV['SEMAPHORE_CURRENT_THREAD']) 45 | end 46 | 47 | def snap_ci_worker_index 48 | index_starting_from_one(ENV['SNAP_WORKER_INDEX']) 49 | end 50 | 51 | def gitlab_ci_node_index 52 | return unless ENV['GITLAB_CI'] 53 | 54 | index_starting_from_one(ENV['CI_NODE_INDEX']) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/knapsack/config/tracker.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Config 3 | class Tracker 4 | class << self 5 | def enable_time_offset_warning 6 | true 7 | end 8 | 9 | def time_offset_in_seconds 10 | 30 11 | end 12 | 13 | def generate_report 14 | !!(ENV['KNAPSACK_GENERATE_REPORT'] =~ /\Atrue|0\z/i) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/knapsack/distributors/base_distributor.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Distributors 3 | class BaseDistributor 4 | attr_reader :report, :node_tests, :test_file_pattern 5 | 6 | def initialize(args={}) 7 | @report = args[:report] || raise('Missing report') 8 | @test_file_pattern = args[:test_file_pattern] || raise('Missing test_file_pattern') 9 | @ci_node_total = args[:ci_node_total] || raise('Missing ci_node_total') 10 | @ci_node_index = args[:ci_node_index] || raise('Missing ci_node_index') 11 | 12 | if ci_node_index >= ci_node_total 13 | raise("Node indexes are 0-based. Can't be higher or equal to the total number of nodes.") 14 | end 15 | end 16 | 17 | def ci_node_total 18 | @ci_node_total.to_i 19 | end 20 | 21 | def ci_node_index 22 | @ci_node_index.to_i 23 | end 24 | 25 | def tests_for_current_node 26 | tests_for_node(ci_node_index) 27 | end 28 | 29 | def tests_for_node(node_index) 30 | assign_test_files_to_node 31 | post_tests_for_node(node_index) 32 | end 33 | 34 | def assign_test_files_to_node 35 | default_node_tests 36 | post_assign_test_files_to_node 37 | end 38 | 39 | def all_tests 40 | @all_tests ||= Dir.glob(test_file_pattern).uniq.sort 41 | end 42 | 43 | protected 44 | 45 | def post_tests_for_node(node_index) 46 | raise NotImplementedError 47 | end 48 | 49 | def post_assign_test_files_to_node 50 | raise NotImplementedError 51 | end 52 | 53 | def default_node_tests 54 | raise NotImplementedError 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/knapsack/distributors/leftover_distributor.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Distributors 3 | class LeftoverDistributor < BaseDistributor 4 | def report_tests 5 | @report_tests ||= report.keys 6 | end 7 | 8 | def leftover_tests 9 | @leftover_tests ||= all_tests - report_tests 10 | end 11 | 12 | private 13 | 14 | def post_assign_test_files_to_node 15 | node_index = 0 16 | leftover_tests.each do |test_file| 17 | node_tests[node_index] << test_file 18 | node_index += 1 19 | node_index %= ci_node_total 20 | end 21 | end 22 | 23 | def post_tests_for_node(node_index) 24 | test_files = node_tests[node_index] 25 | return unless test_files 26 | test_files 27 | end 28 | 29 | def default_node_tests 30 | @node_tests = [] 31 | ci_node_total.times do |index| 32 | @node_tests[index] = [] 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/knapsack/distributors/report_distributor.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Distributors 3 | class ReportDistributor < BaseDistributor 4 | def sorted_report 5 | @sorted_report ||= report.sort_by { |_test_path, time| -time } 6 | end 7 | 8 | def sorted_report_with_existing_tests 9 | @sorted_report_with_existing_tests ||= sorted_report.select { |test_path, time| all_tests.include?(test_path) } 10 | end 11 | 12 | def total_time_execution 13 | @total_time_execution ||= sorted_report_with_existing_tests.map { |_test_path, time| time }.reduce(0, :+).to_f 14 | end 15 | 16 | def node_time_execution 17 | @node_time_execution ||= total_time_execution / ci_node_total 18 | end 19 | 20 | private 21 | 22 | def post_assign_test_files_to_node 23 | assign_test_files 24 | sort_assigned_test_files 25 | end 26 | 27 | def sort_assigned_test_files 28 | node_tests.map do |node| 29 | node[:test_files_with_time] 30 | .sort_by! { |file_name, _time| file_name } 31 | .reverse! 32 | .sort_by! { |_file_name, time| time } 33 | .reverse! 34 | end 35 | end 36 | 37 | def post_tests_for_node(node_index) 38 | node_test = node_tests[node_index] 39 | return unless node_test 40 | node_test[:test_files_with_time].map { |file_name, _time| file_name } 41 | end 42 | 43 | def default_node_tests 44 | @node_tests = Array.new(ci_node_total) do |index| 45 | { 46 | node_index: index, 47 | time_left: node_time_execution, 48 | test_files_with_time: [], 49 | weight: 0 50 | } 51 | end 52 | end 53 | 54 | def assign_test_files 55 | sorted_report_with_existing_tests.map do |test_file_with_time| 56 | test_execution_time = test_file_with_time.last 57 | 58 | current_lightest_node = node_tests.min_by { |node| node[:weight] } 59 | 60 | updated_node_data = { 61 | time_left: current_lightest_node[:time_left] - test_execution_time, 62 | weight: current_lightest_node[:weight] + test_execution_time, 63 | test_files_with_time: current_lightest_node[:test_files_with_time] << test_file_with_time 64 | } 65 | 66 | current_lightest_node.merge!(updated_node_data) 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/knapsack/extensions/time.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | class Time 4 | class << self 5 | alias_method :raw_now, :now 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/knapsack/logger.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | class Logger 3 | attr_accessor :level 4 | 5 | DEBUG = 0 6 | INFO = 1 7 | WARN = 2 8 | 9 | UnknownLogLevel = Class.new(StandardError) 10 | 11 | def log(level, text=nil) 12 | level_method = 13 | case level 14 | when DEBUG then :debug 15 | when INFO then :info 16 | when WARN then :warn 17 | else raise UnknownLogLevel 18 | end 19 | 20 | public_send(level_method, text) 21 | end 22 | 23 | def debug(text=nil) 24 | return if level != DEBUG 25 | puts text 26 | end 27 | 28 | def info(text=nil) 29 | return if level > INFO 30 | puts text 31 | end 32 | 33 | def warn(text=nil) 34 | return if level > WARN 35 | puts text 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/knapsack/presenter.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'json' 3 | 4 | module Knapsack 5 | class Presenter 6 | class << self 7 | def report_yml 8 | Knapsack.tracker.test_files_with_time.to_yaml 9 | end 10 | 11 | def report_json 12 | JSON.pretty_generate(Knapsack.tracker.test_files_with_time) 13 | end 14 | 15 | def report_details 16 | "Knapsack report was generated. Preview:\n" + Presenter.report_json 17 | end 18 | 19 | def global_time 20 | global_time = pretty_seconds(Knapsack.tracker.global_time) 21 | "\nKnapsack global time execution for tests: #{global_time}" 22 | end 23 | 24 | def time_offset 25 | "Time offset: #{Knapsack.tracker.config[:time_offset_in_seconds]}s" 26 | end 27 | 28 | def max_allowed_node_time_execution 29 | max_node_time_execution = pretty_seconds(Knapsack.tracker.max_node_time_execution) 30 | "Max allowed node time execution: #{max_node_time_execution}" 31 | end 32 | 33 | def exceeded_time 34 | exceeded_time = pretty_seconds(Knapsack.tracker.exceeded_time) 35 | "Exceeded time: #{exceeded_time}" 36 | end 37 | 38 | def time_offset_log_level 39 | if Knapsack.tracker.time_exceeded? 40 | Knapsack::Logger::WARN 41 | else 42 | Knapsack::Logger::INFO 43 | end 44 | end 45 | 46 | def time_offset_warning 47 | str = %{\n========= Knapsack Time Offset Warning ========== 48 | #{Presenter.time_offset} 49 | #{Presenter.max_allowed_node_time_execution} 50 | #{Presenter.exceeded_time} 51 | } 52 | if Knapsack.tracker.time_exceeded? 53 | str << %{ 54 | Test on this CI node ran for longer than the max allowed node time execution. 55 | Please regenerate your knapsack report. 56 | 57 | If that doesn't help, you can split your slowest test files into smaller files, or bump up the time_offset_in_seconds setting. 58 | 59 | You can also allow the knapsack_pro gem to automatically divide your slow test files across parallel CI nodes. 60 | https://knapsackpro.com/faq/question/how-to-auto-split-test-files-by-test-cases-on-parallel-jobs-ci-nodes?utm_source=knapsack_gem&utm_medium=knapsack_gem_output&utm_campaign=knapsack_gem_time_offset_warning 61 | } 62 | else 63 | str << %{ 64 | Global time execution for this CI node is fine. 65 | Happy testing!} 66 | end 67 | str << "\n\nNeed explanation? See FAQ:" 68 | str << "\nhttps://docs.knapsackpro.com/ruby/knapsack#faq" 69 | str << "\n=================================================\n" 70 | str << %{Read up on the benefits of a dynamic test split with Knapsack Pro Queue Mode: 71 | https://docs.knapsackpro.com/2020/how-to-speed-up-ruby-and-javascript-tests-with-ci-parallelisation 72 | 73 | Sign up for Knapsack Pro here: 74 | https://knapsackpro.com} 75 | str << "\n=================================================\n" 76 | str 77 | end 78 | 79 | def pretty_seconds(seconds) 80 | sign = '' 81 | 82 | if seconds < 0 83 | seconds = seconds*-1 84 | sign = '-' 85 | end 86 | 87 | return "#{sign}#{seconds}s" if seconds.abs < 1 88 | 89 | time = Time.at(seconds).gmtime.strftime('%Hh %Mm %Ss') 90 | time_without_zeros = time.gsub(/00(h|m|s)/, '').strip 91 | sign + time_without_zeros 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/knapsack/report.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | class Report 3 | include Singleton 4 | 5 | def config(args={}) 6 | @config ||= args 7 | @config.merge!(args) 8 | end 9 | 10 | def report_path 11 | config[:report_path] || raise('Missing report_path') 12 | end 13 | 14 | def test_file_pattern 15 | config[:test_file_pattern] || raise('Missing test_file_pattern') 16 | end 17 | 18 | def save 19 | File.open(report_path, 'w+') do |f| 20 | f.write(report_json) 21 | end 22 | end 23 | 24 | def open 25 | report = File.read(report_path) 26 | JSON.parse(report) 27 | rescue Errno::ENOENT 28 | raise "Knapsack report file #{report_path} doesn't exist. Please generate report first!" 29 | end 30 | 31 | private 32 | 33 | def report_json 34 | Presenter.report_json 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/knapsack/runners/cucumber_runner.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Runners 3 | class CucumberRunner 4 | def self.run(args) 5 | allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::CucumberAdapter).allocator 6 | 7 | Knapsack.logger.info 8 | Knapsack.logger.info 'Report features:' 9 | Knapsack.logger.info allocator.report_node_tests 10 | Knapsack.logger.info 11 | Knapsack.logger.info 'Leftover features:' 12 | Knapsack.logger.info allocator.leftover_node_tests 13 | Knapsack.logger.info 14 | 15 | cmd = %Q[bundle exec cucumber #{args} --require #{allocator.test_dir} -- #{allocator.stringify_node_tests}] 16 | 17 | system(cmd) 18 | exit($?.exitstatus) unless $?.exitstatus == 0 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/knapsack/runners/minitest_runner.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Runners 3 | class MinitestRunner 4 | def self.run(args) 5 | allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::MinitestAdapter).allocator 6 | 7 | Knapsack.logger.info 8 | Knapsack.logger.info 'Report tests:' 9 | Knapsack.logger.info allocator.report_node_tests 10 | Knapsack.logger.info 11 | Knapsack.logger.info 'Leftover tests:' 12 | Knapsack.logger.info allocator.leftover_node_tests 13 | Knapsack.logger.info 14 | 15 | task_name = 'knapsack:minitest_run' 16 | 17 | if Rake::Task.task_defined?(task_name) 18 | Rake::Task[task_name].clear 19 | end 20 | 21 | Rake::TestTask.new(task_name) do |t| 22 | t.warning = false 23 | t.libs << allocator.test_dir 24 | t.test_files = allocator.node_tests 25 | t.options = args 26 | end 27 | 28 | Rake::Task[task_name].invoke 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/knapsack/runners/rspec_runner.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Runners 3 | class RSpecRunner 4 | def self.run(args) 5 | allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator 6 | 7 | Knapsack.logger.info 8 | Knapsack.logger.info 'Report specs:' 9 | Knapsack.logger.info allocator.report_node_tests 10 | Knapsack.logger.info 11 | Knapsack.logger.info 'Leftover specs:' 12 | Knapsack.logger.info allocator.leftover_node_tests 13 | Knapsack.logger.info 14 | 15 | cmd = %Q[bundle exec rspec #{args} --default-path #{allocator.test_dir} -- #{allocator.stringify_node_tests}] 16 | 17 | exec(cmd) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/knapsack/runners/spinach_runner.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | module Runners 3 | class SpinachRunner 4 | def self.run(args) 5 | allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::SpinachAdapter).allocator 6 | 7 | Knapsack.logger.info 8 | Knapsack.logger.info 'Report features:' 9 | Knapsack.logger.info allocator.report_node_tests 10 | Knapsack.logger.info 11 | Knapsack.logger.info 'Leftover features:' 12 | Knapsack.logger.info allocator.leftover_node_tests 13 | Knapsack.logger.info 14 | 15 | cmd = %Q[bundle exec spinach #{args} --features_path #{allocator.test_dir} -- #{allocator.stringify_node_tests}] 16 | 17 | system(cmd) 18 | exit($?.exitstatus) unless $?.exitstatus == 0 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/knapsack/task_loader.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | module Knapsack 4 | class TaskLoader 5 | include ::Rake::DSL 6 | 7 | def load_tasks 8 | Dir.glob("#{Knapsack.root}/lib/tasks/*.rake").each { |r| import r } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/knapsack/tracker.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | class Tracker 3 | include Singleton 4 | 5 | attr_reader :global_time, :test_files_with_time 6 | attr_writer :test_path 7 | 8 | def initialize 9 | set_defaults 10 | end 11 | 12 | def config(opts={}) 13 | @config ||= default_config 14 | @config.merge!(opts) 15 | end 16 | 17 | def reset! 18 | set_defaults 19 | end 20 | 21 | def start_timer 22 | @start_time = now_without_mock_time.to_f 23 | end 24 | 25 | def stop_timer 26 | execution_time = now_without_mock_time.to_f - @start_time 27 | 28 | if test_path 29 | update_global_time(execution_time) 30 | update_test_file_time(execution_time) 31 | @test_path = nil 32 | end 33 | 34 | execution_time 35 | end 36 | 37 | def test_path 38 | @test_path.sub(/^\.\//, '') if @test_path 39 | end 40 | 41 | def time_exceeded? 42 | global_time > max_node_time_execution 43 | end 44 | 45 | def max_node_time_execution 46 | report_distributor.node_time_execution + config[:time_offset_in_seconds] 47 | end 48 | 49 | def exceeded_time 50 | global_time - max_node_time_execution 51 | end 52 | 53 | private 54 | 55 | def default_config 56 | { 57 | enable_time_offset_warning: Config::Tracker.enable_time_offset_warning, 58 | time_offset_in_seconds: Config::Tracker.time_offset_in_seconds, 59 | generate_report: Config::Tracker.generate_report 60 | } 61 | end 62 | 63 | def set_defaults 64 | @global_time = 0 65 | @test_files_with_time = {} 66 | @test_path = nil 67 | end 68 | 69 | def update_global_time(execution_time) 70 | @global_time += execution_time 71 | end 72 | 73 | def update_test_file_time(execution_time) 74 | @test_files_with_time[test_path] ||= 0 75 | @test_files_with_time[test_path] += execution_time 76 | end 77 | 78 | def report_distributor 79 | @report_distributor ||= Knapsack::Distributors::ReportDistributor.new({ 80 | report: Knapsack.report.open, 81 | test_file_pattern: Knapsack::Config::Env.test_file_pattern || Knapsack.report.config[:test_file_pattern], 82 | ci_node_total: Knapsack::Config::Env.ci_node_total, 83 | ci_node_index: Knapsack::Config::Env.ci_node_index 84 | }) 85 | end 86 | 87 | def now_without_mock_time 88 | Process.clock_gettime(Process::CLOCK_MONOTONIC) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/knapsack/version.rb: -------------------------------------------------------------------------------- 1 | module Knapsack 2 | VERSION = '4.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/knapsack_cucumber.rake: -------------------------------------------------------------------------------- 1 | require 'knapsack' 2 | 3 | namespace :knapsack do 4 | task :cucumber, [:cucumber_args] do |_, args| 5 | Knapsack::Runners::CucumberRunner.run(args[:cucumber_args]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tasks/knapsack_minitest.rake: -------------------------------------------------------------------------------- 1 | require 'knapsack' 2 | 3 | namespace :knapsack do 4 | task :minitest, [:minitest_args] do |_, args| 5 | Knapsack::Runners::MinitestRunner.run(args[:minitest_args]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tasks/knapsack_rspec.rake: -------------------------------------------------------------------------------- 1 | require 'knapsack' 2 | 3 | namespace :knapsack do 4 | task :rspec, [:rspec_args] do |_, args| 5 | Knapsack::Runners::RSpecRunner.run(args[:rspec_args]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tasks/knapsack_spinach.rake: -------------------------------------------------------------------------------- 1 | require 'knapsack' 2 | 3 | namespace :knapsack do 4 | task :spinach, [:spinach_args] do |_, args| 5 | Knapsack::Runners::SpinachRunner.run(args[:spinach_args]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/knapsack/adapters/base_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Adapters::BaseAdapter do 2 | describe '.bind' do 3 | let(:adapter) { instance_double(described_class) } 4 | 5 | subject { described_class.bind } 6 | 7 | before do 8 | expect(described_class).to receive(:new).and_return(adapter) 9 | expect(adapter).to receive(:bind) 10 | end 11 | 12 | it { should eql adapter } 13 | end 14 | 15 | describe '#bind' do 16 | let(:tracker) { instance_double(Knapsack::Tracker) } 17 | 18 | before do 19 | allow(subject).to receive(:tracker).and_return(tracker) 20 | end 21 | 22 | context 'when generate report' do 23 | before do 24 | expect(tracker).to receive(:config).and_return({ generate_report: true }) 25 | end 26 | 27 | it do 28 | expect(subject).to receive(:bind_time_tracker) 29 | expect(subject).to receive(:bind_report_generator) 30 | expect(subject).not_to receive(:bind_time_offset_warning) 31 | subject.bind 32 | end 33 | end 34 | 35 | context 'when enable time offset warning' do 36 | before do 37 | expect(tracker).to receive(:config).twice.and_return({ 38 | generate_report: false, 39 | enable_time_offset_warning: true 40 | }) 41 | end 42 | 43 | it do 44 | expect(subject).to receive(:bind_time_tracker) 45 | expect(subject).to receive(:bind_time_offset_warning) 46 | expect(subject).not_to receive(:bind_report_generator) 47 | subject.bind 48 | end 49 | end 50 | 51 | context 'when adapter is off' do 52 | before do 53 | expect(tracker).to receive(:config).twice.and_return({ 54 | generate_report: false, 55 | enable_time_offset_warning: false 56 | }) 57 | end 58 | 59 | it do 60 | expect(subject).not_to receive(:bind_time_tracker) 61 | expect(subject).not_to receive(:bind_report_generator) 62 | expect(subject).not_to receive(:bind_time_offset_warning) 63 | subject.bind 64 | end 65 | end 66 | end 67 | 68 | describe '#bind_time_tracker' do 69 | it do 70 | expect { 71 | subject.bind_time_tracker 72 | }.to raise_error(NotImplementedError) 73 | end 74 | end 75 | 76 | describe '#bind_report_generator' do 77 | it do 78 | expect { 79 | subject.bind_report_generator 80 | }.to raise_error(NotImplementedError) 81 | end 82 | end 83 | 84 | describe '#bind_time_offset_warning' do 85 | it do 86 | expect { 87 | subject.bind_time_offset_warning 88 | }.to raise_error(NotImplementedError) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/knapsack/adapters/cucumber_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Adapters::CucumberAdapter do 2 | context do 3 | context 'when Cucumber version 1' do 4 | before do 5 | stub_const('Cucumber::VERSION', '1.3.20') 6 | allow(::Cucumber::RbSupport::RbDsl).to receive(:register_rb_hook) 7 | allow(Kernel).to receive(:at_exit) 8 | end 9 | 10 | it_behaves_like 'adapter' 11 | end 12 | 13 | context 'when Cucumber version 2' do 14 | before do 15 | stub_const('Cucumber::VERSION', '2') 16 | allow(::Cucumber::RbSupport::RbDsl).to receive(:register_rb_hook) 17 | allow(Kernel).to receive(:at_exit) 18 | end 19 | 20 | it_behaves_like 'adapter' 21 | end 22 | 23 | context 'when Cucumber version 3' do 24 | before do 25 | stub_const('Cucumber::VERSION', '3.0.0') 26 | allow(::Cucumber::Glue::Dsl).to receive(:register_rb_hook) 27 | allow(Kernel).to receive(:at_exit) 28 | end 29 | 30 | it_behaves_like 'adapter' 31 | end 32 | end 33 | 34 | describe 'bind methods' do 35 | let(:logger) { instance_double(Knapsack::Logger) } 36 | 37 | before do 38 | allow(Knapsack).to receive(:logger).and_return(logger) 39 | end 40 | 41 | describe '#bind_time_tracker' do 42 | let(:file) { 'features/a.feature' } 43 | let(:block) { double } 44 | let(:global_time) { 'Global time: 01m 05s' } 45 | let(:tracker) { instance_double(Knapsack::Tracker) } 46 | 47 | context 'when Cucumber version 1' do 48 | let(:scenario) { double(file: file) } 49 | 50 | before { stub_const('Cucumber::VERSION', '1.3.20') } 51 | 52 | it do 53 | expect(subject).to receive(:Around).and_yield(scenario, block) 54 | allow(Knapsack).to receive(:tracker).and_return(tracker) 55 | expect(tracker).to receive(:test_path=).with(file) 56 | expect(tracker).to receive(:start_timer) 57 | expect(block).to receive(:call) 58 | expect(tracker).to receive(:stop_timer) 59 | 60 | expect(::Kernel).to receive(:at_exit).and_yield 61 | expect(Knapsack::Presenter).to receive(:global_time).and_return(global_time) 62 | expect(logger).to receive(:info).with(global_time) 63 | 64 | subject.bind_time_tracker 65 | end 66 | end 67 | 68 | context 'when Cucumber version 2' do 69 | let(:test_case) { double(location: double(file: file)) } 70 | 71 | # complex version name to ensure we can catch that too 72 | before { stub_const('Cucumber::VERSION', '2.0.0.rc.5') } 73 | 74 | it do 75 | expect(subject).to receive(:Around).and_yield(test_case, block) 76 | allow(Knapsack).to receive(:tracker).and_return(tracker) 77 | expect(tracker).to receive(:test_path=).with(file) 78 | expect(tracker).to receive(:start_timer) 79 | expect(block).to receive(:call) 80 | expect(tracker).to receive(:stop_timer) 81 | 82 | expect(::Kernel).to receive(:at_exit).and_yield 83 | expect(Knapsack::Presenter).to receive(:global_time).and_return(global_time) 84 | expect(logger).to receive(:info).with(global_time) 85 | 86 | subject.bind_time_tracker 87 | end 88 | end 89 | end 90 | 91 | describe '#bind_report_generator' do 92 | let(:report) { instance_double(Knapsack::Report) } 93 | let(:report_details) { 'Report details' } 94 | 95 | it do 96 | expect(::Kernel).to receive(:at_exit).and_yield 97 | expect(Knapsack).to receive(:report).and_return(report) 98 | expect(report).to receive(:save) 99 | 100 | expect(Knapsack::Presenter).to receive(:report_details).and_return(report_details) 101 | expect(logger).to receive(:info).with(report_details) 102 | 103 | subject.bind_report_generator 104 | end 105 | end 106 | 107 | describe '#bind_time_offset_warning' do 108 | let(:time_offset_warning) { 'Time offset warning' } 109 | let(:log_level) { :info } 110 | 111 | it 'creates an at-exit callback to log the time offset message at the specified log level' do 112 | expect(::Kernel).to receive(:at_exit).and_yield 113 | expect(Knapsack::Presenter).to receive(:time_offset_warning).and_return(time_offset_warning) 114 | expect(Knapsack::Presenter).to receive(:time_offset_log_level).and_return(log_level) 115 | expect(logger).to receive(:log).with(log_level, time_offset_warning) 116 | 117 | subject.bind_time_offset_warning 118 | end 119 | end 120 | end 121 | 122 | describe '.test_path' do 123 | context 'when Cucumber version 1' do 124 | subject { described_class.test_path(scenario_or_outline_table) } 125 | 126 | before { stub_const('Cucumber::VERSION', '1') } 127 | 128 | context 'when cucumber >= 1.3' do 129 | context 'when scenario' do 130 | let(:scenario_file) { 'features/scenario.feature' } 131 | let(:scenario_or_outline_table) { double(file: scenario_file) } 132 | 133 | it { should eql scenario_file } 134 | end 135 | 136 | context 'when scenario outline' do 137 | let(:scenario_outline_file) { 'features/scenario_outline.feature' } 138 | let(:scenario_or_outline_table) do 139 | double(scenario_outline: double(file: scenario_outline_file)) 140 | end 141 | 142 | it { should eql scenario_outline_file } 143 | end 144 | end 145 | 146 | context 'when cucumber < 1.3' do 147 | context 'when scenario' do 148 | let(:scenario_file) { 'features/scenario.feature' } 149 | let(:scenario_or_outline_table) { double(feature: double(file: scenario_file)) } 150 | 151 | it { should eql scenario_file } 152 | end 153 | 154 | context 'when scenario outline' do 155 | let(:scenario_outline_file) { 'features/scenario_outline.feature' } 156 | let(:scenario_or_outline_table) do 157 | double(scenario_outline: double(feature: double(file: scenario_outline_file))) 158 | end 159 | 160 | it { should eql scenario_outline_file } 161 | end 162 | end 163 | end 164 | 165 | context 'when Cucumber version 2' do 166 | let(:file) { 'features/a.feature' } 167 | let(:test_case) { double(location: double(file: file)) } # Cucumber 2 168 | 169 | subject { described_class.test_path(test_case) } 170 | 171 | before { stub_const('Cucumber::VERSION', '2') } 172 | 173 | it { should eql file } 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/knapsack/adapters/minitest_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | module FakeMinitest 2 | class Test < ::Minitest::Test 3 | include Knapsack::Adapters::MinitestAdapter::BindTimeTrackerMinitestPlugin 4 | end 5 | end 6 | 7 | describe Knapsack::Adapters::MinitestAdapter do 8 | describe 'BindTimeTrackerMinitestPlugin' do 9 | let(:tracker) { instance_double(Knapsack::Tracker) } 10 | 11 | subject { ::FakeMinitest::Test.new } 12 | 13 | before do 14 | allow(Knapsack).to receive(:tracker).and_return(tracker) 15 | end 16 | 17 | describe '#before_setup' do 18 | let(:file) { 'test/models/user_test.rb' } 19 | 20 | it do 21 | expect(described_class).to receive(:test_path).with(subject).and_return(file) 22 | expect(tracker).to receive(:test_path=).with(file) 23 | expect(tracker).to receive(:start_timer) 24 | 25 | subject.before_setup 26 | end 27 | end 28 | 29 | describe '#after_teardown' do 30 | it do 31 | expect(tracker).to receive(:stop_timer) 32 | 33 | subject.after_teardown 34 | end 35 | end 36 | end 37 | 38 | describe 'bind methods' do 39 | let(:logger) { instance_double(Knapsack::Logger) } 40 | let(:global_time) { 'Global time: 01m 05s' } 41 | 42 | before do 43 | expect(Knapsack).to receive(:logger).and_return(logger) 44 | end 45 | 46 | describe '#bind_time_tracker' do 47 | it do 48 | expect(::Minitest::Test).to receive(:send).with(:include, Knapsack::Adapters::MinitestAdapter::BindTimeTrackerMinitestPlugin) 49 | 50 | expect(::Minitest).to receive(:after_run).and_yield 51 | expect(Knapsack::Presenter).to receive(:global_time).and_return(global_time) 52 | expect(logger).to receive(:info).with(global_time) 53 | 54 | subject.bind_time_tracker 55 | end 56 | end 57 | 58 | describe '#bind_report_generator' do 59 | let(:report) { instance_double(Knapsack::Report) } 60 | let(:report_details) { 'Report details' } 61 | 62 | it do 63 | expect(::Minitest).to receive(:after_run).and_yield 64 | 65 | expect(Knapsack).to receive(:report).and_return(report) 66 | expect(report).to receive(:save) 67 | 68 | expect(Knapsack::Presenter).to receive(:report_details).and_return(report_details) 69 | expect(logger).to receive(:info).with(report_details) 70 | 71 | subject.bind_report_generator 72 | end 73 | end 74 | 75 | describe '#bind_time_offset_warning' do 76 | let(:time_offset_warning) { 'Time offset warning' } 77 | let(:log_level) { :info } 78 | 79 | it 'creates a post-run callback to log the time offset message at the specified log level' do 80 | expect(::Minitest).to receive(:after_run).and_yield 81 | 82 | expect(Knapsack::Presenter).to receive(:time_offset_warning).and_return(time_offset_warning) 83 | expect(Knapsack::Presenter).to receive(:time_offset_log_level).and_return(log_level) 84 | expect(logger).to receive(:log).with(log_level, time_offset_warning) 85 | 86 | subject.bind_time_offset_warning 87 | end 88 | end 89 | end 90 | 91 | describe '#set_test_helper_path' do 92 | let(:adapter) { described_class.new } 93 | let(:test_helper_path) { '/code/project/test/test_helper.rb' } 94 | 95 | subject { adapter.set_test_helper_path(test_helper_path) } 96 | 97 | after do 98 | expect(described_class.class_variable_get(:@@parent_of_test_dir)).to eq '/code/project' 99 | end 100 | 101 | it { should eql '/code/project' } 102 | end 103 | 104 | describe '.test_path' do 105 | subject { described_class.test_path(obj) } 106 | 107 | before do 108 | parent_of_test_dir = File.expand_path('../../../', File.dirname(__FILE__)) 109 | parent_of_test_dir_regexp = Regexp.new("^#{parent_of_test_dir}") 110 | described_class.class_variable_set(:@@parent_of_test_dir, parent_of_test_dir_regexp) 111 | end 112 | 113 | context 'when regular test' do 114 | class FakeUserTest 115 | def test_user_age; end 116 | 117 | # method provided by Minitest 118 | # it returns test method name 119 | def name 120 | :test_user_age 121 | end 122 | end 123 | 124 | let(:obj) { FakeUserTest.new } 125 | 126 | it { should eq './spec/knapsack/adapters/minitest_adapter_spec.rb' } 127 | end 128 | 129 | context 'when shared examples test' do 130 | module FakeSharedExamples 131 | def test_from_shared_example; end 132 | end 133 | 134 | class FakeSharedExamplesUserTest 135 | include FakeSharedExamples 136 | 137 | def location 138 | "test that use FakeSharedExamples#test_from_shared_example" 139 | end 140 | end 141 | 142 | let(:obj) { FakeSharedExamplesUserTest.new } 143 | 144 | it { should eq './spec/knapsack/adapters/minitest_adapter_spec.rb' } 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/knapsack/adapters/rspec_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | describe Knapsack::Adapters::RSpecAdapter do 4 | context do 5 | before { expect(::RSpec).to receive(:configure) } 6 | it_behaves_like 'adapter' 7 | end 8 | 9 | describe 'bind methods' do 10 | let(:config) { double } 11 | let(:logger) { instance_double(Knapsack::Logger) } 12 | 13 | before do 14 | expect(Knapsack).to receive(:logger).and_return(logger) 15 | end 16 | 17 | describe '#bind_time_tracker' do 18 | let(:tracker) { instance_double(Knapsack::Tracker) } 19 | let(:test_path) { 'spec/a_spec.rb' } 20 | let(:global_time) { 'Global time: 01m 05s' } 21 | let(:current_example) { double } 22 | 23 | it do 24 | expect(config).to receive(:prepend_before).with(:context).and_yield 25 | expect(config).to receive(:prepend_before).with(:each).and_yield(current_example) 26 | expect(config).to receive(:append_after).with(:context).and_yield 27 | expect(config).to receive(:after).with(:suite).and_yield 28 | expect(::RSpec).to receive(:configure).and_yield(config) 29 | 30 | expect(described_class).to receive(:test_path).with(current_example).and_return(test_path) 31 | 32 | allow(Knapsack).to receive(:tracker).and_return(tracker) 33 | expect(tracker).to receive(:start_timer).ordered 34 | expect(tracker).to receive(:test_path=).with(test_path).ordered 35 | expect(tracker).to receive(:stop_timer).ordered 36 | 37 | expect(Knapsack::Presenter).to receive(:global_time).and_return(global_time) 38 | expect(logger).to receive(:info).with(global_time) 39 | 40 | subject.bind_time_tracker 41 | end 42 | end 43 | 44 | describe '#bind_report_generator' do 45 | let(:report) { instance_double(Knapsack::Report) } 46 | let(:report_details) { 'Report details' } 47 | 48 | it do 49 | expect(config).to receive(:after).with(:suite).and_yield 50 | expect(::RSpec).to receive(:configure).and_yield(config) 51 | 52 | expect(Knapsack).to receive(:report).and_return(report) 53 | expect(report).to receive(:save) 54 | 55 | expect(Knapsack::Presenter).to receive(:report_details).and_return(report_details) 56 | expect(logger).to receive(:info).with(report_details) 57 | 58 | subject.bind_report_generator 59 | end 60 | end 61 | 62 | describe '#bind_time_offset_warning' do 63 | let(:time_offset_warning) { 'Time offset warning' } 64 | let(:log_level) { :info } 65 | 66 | it 'creates a post-suite callback to log the time offset message at the specified log level' do 67 | expect(config).to receive(:after).with(:suite).and_yield 68 | expect(::RSpec).to receive(:configure).and_yield(config) 69 | 70 | expect(Knapsack::Presenter).to receive(:time_offset_warning).and_return(time_offset_warning) 71 | expect(Knapsack::Presenter).to receive(:time_offset_log_level).and_return(log_level) 72 | expect(logger).to receive(:log).with(log_level, time_offset_warning) 73 | 74 | subject.bind_time_offset_warning 75 | end 76 | end 77 | end 78 | 79 | describe '.test_path' do 80 | let(:example_group) do 81 | { 82 | file_path: '1_shared_example.rb', 83 | parent_example_group: { 84 | file_path: '2_shared_example.rb', 85 | parent_example_group: { 86 | file_path: 'a_spec.rb' 87 | } 88 | } 89 | } 90 | end 91 | let(:current_example) do 92 | OpenStruct.new(metadata: { 93 | example_group: example_group 94 | }) 95 | end 96 | 97 | subject { described_class.test_path(current_example) } 98 | 99 | it { should eql 'a_spec.rb' } 100 | 101 | context 'with turnip features' do 102 | describe 'when the turnip version is less than 2' do 103 | let(:example_group) do 104 | { 105 | file_path: "./spec/features/logging_in.feature", 106 | turnip: true, 107 | parent_example_group: { 108 | file_path: "gems/turnip-1.2.4/lib/turnip/rspec.rb" 109 | } 110 | } 111 | end 112 | 113 | before { stub_const("Turnip::VERSION", '1.2.4') } 114 | 115 | it { should eql './spec/features/logging_in.feature' } 116 | end 117 | 118 | describe 'when turnip is version 2 or greater' do 119 | let(:example_group) do 120 | { 121 | file_path: "gems/turnip-2.0.0/lib/turnip/rspec.rb", 122 | turnip: true, 123 | parent_example_group: { 124 | file_path: "./spec/features/logging_in.feature", 125 | } 126 | } 127 | end 128 | 129 | before { stub_const("Turnip::VERSION", '2.0.0') } 130 | 131 | it { should eql './spec/features/logging_in.feature' } 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/knapsack/adapters/spinach_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Adapters::SpinachAdapter do 2 | context do 3 | it_behaves_like 'adapter' 4 | end 5 | 6 | describe 'bind methods' do 7 | let(:config) { double } 8 | let(:logger) { instance_double(Knapsack::Logger) } 9 | 10 | before do 11 | expect(Knapsack).to receive(:logger).and_return(logger) 12 | end 13 | 14 | describe '#bind_time_tracker' do 15 | let(:tracker) { instance_double(Knapsack::Tracker) } 16 | let(:global_time) { 'Global time: 01m 05s' } 17 | let(:test_path) { 'features/a.feature' } 18 | let(:scenario_data) do 19 | double(feature: double(filename: test_path)) 20 | end 21 | 22 | it do 23 | allow(Knapsack).to receive(:tracker).and_return(tracker) 24 | 25 | expect(Spinach.hooks).to receive(:before_scenario).and_yield(scenario_data, nil) 26 | expect(described_class).to receive(:test_path).with(scenario_data).and_return(test_path) 27 | expect(tracker).to receive(:test_path=).with(test_path) 28 | expect(tracker).to receive(:start_timer) 29 | 30 | expect(Spinach.hooks).to receive(:after_scenario).and_yield 31 | expect(tracker).to receive(:stop_timer) 32 | 33 | expect(Spinach.hooks).to receive(:after_run).and_yield 34 | expect(Knapsack::Presenter).to receive(:global_time).and_return(global_time) 35 | expect(logger).to receive(:info).with(global_time) 36 | 37 | subject.bind_time_tracker 38 | end 39 | end 40 | 41 | describe '#bind_report_generator' do 42 | let(:report) { instance_double(Knapsack::Report) } 43 | let(:report_details) { 'Report details' } 44 | 45 | it do 46 | expect(Spinach.hooks).to receive(:after_run).and_yield 47 | 48 | expect(Knapsack).to receive(:report).and_return(report) 49 | expect(report).to receive(:save) 50 | 51 | expect(Knapsack::Presenter).to receive(:report_details).and_return(report_details) 52 | expect(logger).to receive(:info).with(report_details) 53 | 54 | subject.bind_report_generator 55 | end 56 | end 57 | 58 | describe '#bind_time_offset_warning' do 59 | let(:time_offset_warning) { 'Time offset warning' } 60 | let(:log_level) { :info } 61 | 62 | it do 63 | expect(Spinach.hooks).to receive(:after_run).and_yield 64 | 65 | expect(Knapsack::Presenter).to receive(:time_offset_warning).and_return(time_offset_warning) 66 | expect(Knapsack::Presenter).to receive(:time_offset_log_level).and_return(log_level) 67 | expect(logger).to receive(:log).with(log_level, time_offset_warning) 68 | 69 | subject.bind_time_offset_warning 70 | end 71 | end 72 | end 73 | 74 | describe '.test_path' do 75 | let(:scenario_data) do 76 | double(feature: double(filename: 'a.feature')) 77 | end 78 | 79 | subject { described_class.test_path(scenario_data) } 80 | 81 | it { should eql 'a.feature' } 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/knapsack/allocator_builder_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::AllocatorBuilder do 2 | let(:allocator_builder) { described_class.new(adapter_class) } 3 | let(:allocator) { double } 4 | 5 | let(:report) { double } 6 | let(:knapsack_report) { instance_double(Knapsack::Report) } 7 | 8 | let(:adapter_report_path) { adapter_class::REPORT_PATH } 9 | let(:adapter_test_file_pattern) { adapter_class::TEST_DIR_PATTERN } 10 | 11 | let(:env_ci_node_total) { double } 12 | let(:env_ci_node_index) { double } 13 | let(:env_report_path) { nil } 14 | let(:env_test_file_pattern) { nil } 15 | 16 | describe '#allocator' do 17 | subject { allocator_builder.allocator } 18 | 19 | before do 20 | expect(Knapsack::Config::Env).to receive(:report_path).and_return(env_report_path) 21 | expect(Knapsack::Config::Env).to receive(:test_file_pattern).and_return(env_test_file_pattern) 22 | expect(Knapsack::Config::Env).to receive(:ci_node_total).and_return(env_ci_node_total) 23 | expect(Knapsack::Config::Env).to receive(:ci_node_index).and_return(env_ci_node_index) 24 | 25 | expect(Knapsack).to receive(:report).twice.and_return(knapsack_report) 26 | expect(knapsack_report).to receive(:open).and_return(report) 27 | 28 | expect(knapsack_report).to receive(:config).with(report_config) 29 | expect(Knapsack::Allocator).to receive(:new).with(allocator_args).and_return(allocator) 30 | end 31 | 32 | shared_examples 'allocator builder' do 33 | context 'when ENVs are nil' do 34 | let(:report_config) { { report_path: adapter_report_path } } 35 | let(:allocator_args) do 36 | { 37 | report: report, 38 | test_file_pattern: adapter_test_file_pattern, 39 | ci_node_total: env_ci_node_total, 40 | ci_node_index: env_ci_node_index 41 | } 42 | end 43 | 44 | it { should eql allocator } 45 | end 46 | 47 | context 'when ENV report_path has value' do 48 | let(:env_report_path) { 'knapsack_custom_report.json' } 49 | let(:report_config) { { report_path: env_report_path } } 50 | let(:allocator_args) do 51 | { 52 | report: report, 53 | test_file_pattern: adapter_test_file_pattern, 54 | ci_node_total: env_ci_node_total, 55 | ci_node_index: env_ci_node_index 56 | } 57 | end 58 | 59 | it { should eql allocator } 60 | end 61 | 62 | context 'when ENV test_file_pattern has value' do 63 | let(:env_test_file_pattern) { 'custom_spec/**{,/*/**}/*_spec.rb' } 64 | let(:report_config) { { report_path: adapter_report_path } } 65 | let(:allocator_args) do 66 | { 67 | report: report, 68 | test_file_pattern: env_test_file_pattern, 69 | ci_node_total: env_ci_node_total, 70 | ci_node_index: env_ci_node_index 71 | } 72 | end 73 | 74 | it { should eql allocator } 75 | end 76 | end 77 | 78 | context 'when RSpecAdapter' do 79 | let(:adapter_class) { Knapsack::Adapters::RSpecAdapter } 80 | it_behaves_like 'allocator builder' 81 | end 82 | 83 | # To make sure we do not break backwards compatibility 84 | context 'when RspecAdapter' do 85 | let(:adapter_class) { Knapsack::Adapters::RspecAdapter } 86 | it_behaves_like 'allocator builder' 87 | end 88 | 89 | context 'when CucumberAdapter' do 90 | let(:adapter_class) { Knapsack::Adapters::CucumberAdapter } 91 | it_behaves_like 'allocator builder' 92 | end 93 | end 94 | 95 | describe '#test_dir' do 96 | let(:adapter_class) { Knapsack::Adapters::RSpecAdapter } 97 | 98 | subject { allocator_builder.test_dir } 99 | 100 | context 'when ENV test_dir has value' do 101 | before do 102 | expect(Knapsack::Config::Env).to receive(:test_dir).and_return("custom_spec") 103 | end 104 | 105 | it { should eq 'custom_spec' } 106 | end 107 | 108 | context 'when ENV test_dir has no value' do 109 | before do 110 | expect(Knapsack::Config::Env).to receive(:test_file_pattern).and_return(env_test_file_pattern) 111 | end 112 | 113 | context 'when ENV test_file_pattern has value' do 114 | let(:env_test_file_pattern) { 'custom_spec/**{,/*/**}/*_spec.rb' } 115 | 116 | it { should eq 'custom_spec' } 117 | end 118 | 119 | context 'when ENV test_file_pattern has no value' do 120 | it { should eq 'spec' } 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/knapsack/allocator_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Allocator do 2 | let(:test_file_pattern) { nil } 3 | let(:args) do 4 | { 5 | ci_node_total: nil, 6 | ci_node_index: nil, 7 | test_file_pattern: test_file_pattern, 8 | report: nil 9 | } 10 | end 11 | let(:report_distributor) { instance_double(Knapsack::Distributors::ReportDistributor) } 12 | let(:leftover_distributor) { instance_double(Knapsack::Distributors::LeftoverDistributor) } 13 | let(:report_tests) { ['a_spec.rb', 'b_spec.rb'] } 14 | let(:leftover_tests) { ['c_spec.rb', 'd_spec.rb'] } 15 | let(:node_tests) { report_tests + leftover_tests } 16 | let(:allocator) { described_class.new(args) } 17 | 18 | before do 19 | expect(Knapsack::Distributors::ReportDistributor).to receive(:new).with(args).and_return(report_distributor) 20 | expect(Knapsack::Distributors::LeftoverDistributor).to receive(:new).with(args).and_return(leftover_distributor) 21 | allow(report_distributor).to receive(:tests_for_current_node).and_return(report_tests) 22 | allow(leftover_distributor).to receive(:tests_for_current_node).and_return(leftover_tests) 23 | end 24 | 25 | describe '#report_node_tests' do 26 | subject { allocator.report_node_tests } 27 | it { should eql report_tests } 28 | end 29 | 30 | describe '#leftover_node_tests' do 31 | subject { allocator.leftover_node_tests } 32 | it { should eql leftover_tests } 33 | end 34 | 35 | describe '#node_tests' do 36 | subject { allocator.node_tests } 37 | it { should eql node_tests } 38 | end 39 | 40 | describe '#stringify_node_tests' do 41 | subject { allocator.stringify_node_tests } 42 | it { should eql %{"a_spec.rb" "b_spec.rb" "c_spec.rb" "d_spec.rb"} } 43 | end 44 | 45 | describe '#test_dir' do 46 | subject { allocator.test_dir } 47 | 48 | context 'when ENV test_dir has value' do 49 | let(:test_dir) { "custom_dir" } 50 | 51 | before do 52 | expect(Knapsack::Config::Env).to receive(:test_dir).and_return(test_dir) 53 | end 54 | 55 | it { should eql 'custom_dir' } 56 | end 57 | 58 | context 'when ENV test_dir has no value' do 59 | let(:test_file_pattern) { "test_dir/**{,/*/**}/*_spec.rb" } 60 | 61 | before do 62 | expect(report_distributor).to receive(:test_file_pattern).and_return(test_file_pattern) 63 | end 64 | 65 | it { should eql 'test_dir' } 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/knapsack/config/env_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Config::Env do 2 | describe '.report_path' do 3 | subject { described_class.report_path } 4 | 5 | context 'when ENV exists' do 6 | let(:report_path) { 'knapsack_custom_report.json' } 7 | before { stub_const("ENV", { 'KNAPSACK_REPORT_PATH' => report_path }) } 8 | it { should eql report_path } 9 | end 10 | 11 | context "when ENV doesn't exist" do 12 | it { should be_nil } 13 | end 14 | end 15 | 16 | describe '.ci_node_total' do 17 | subject { described_class.ci_node_total } 18 | 19 | context 'when ENV exists' do 20 | context 'when CI_NODE_TOTAL has value' do 21 | before { stub_const("ENV", { 'CI_NODE_TOTAL' => 5 }) } 22 | it { should eql 5 } 23 | end 24 | 25 | context 'when CIRCLE_NODE_TOTAL has value' do 26 | before { stub_const("ENV", { 'CIRCLE_NODE_TOTAL' => 4 }) } 27 | it { should eql 4 } 28 | end 29 | 30 | context 'when SEMAPHORE_JOB_COUNT has value' do 31 | before { stub_const("ENV", { 'SEMAPHORE_JOB_COUNT' => 3 }) } 32 | it { should eql 3 } 33 | end 34 | 35 | context 'when SEMAPHORE_THREAD_COUNT has value' do 36 | before { stub_const("ENV", { 'SEMAPHORE_THREAD_COUNT' => 3 }) } 37 | it { should eql 3 } 38 | end 39 | 40 | context 'when BUILDKITE_PARALLEL_JOB_COUNT has value' do 41 | before { stub_const("ENV", { 'BUILDKITE_PARALLEL_JOB_COUNT' => 4 }) } 42 | it { should eql 4 } 43 | end 44 | 45 | context 'when SNAP_WORKER_TOTAL has value' do 46 | before { stub_const("ENV", { 'SNAP_WORKER_TOTAL' => 6 }) } 47 | it { should eql 6 } 48 | end 49 | 50 | context 'when BITBUCKET_PARALLEL_STEP_COUNT has value' do 51 | before { stub_const("ENV", { 'BITBUCKET_PARALLEL_STEP_COUNT' => 8 }) } 52 | it { should eql 8 } 53 | end 54 | end 55 | 56 | context "when ENV doesn't exist" do 57 | it { should eql 1 } 58 | end 59 | end 60 | 61 | describe '.ci_node_index' do 62 | subject { described_class.ci_node_index } 63 | 64 | context 'when ENV exists' do 65 | context 'when CI_NODE_INDEX has value' do 66 | before { stub_const("ENV", { 'CI_NODE_INDEX' => 3 }) } 67 | it { should eql 3 } 68 | end 69 | 70 | context 'when CI_NODE_INDEX has value and is in GitLab CI' do 71 | before { stub_const("ENV", { 'CI_NODE_INDEX' => 3, 'GITLAB_CI' => 'true' }) } 72 | it { should eql 2 } 73 | end 74 | 75 | context 'when CIRCLE_NODE_INDEX has value' do 76 | before { stub_const("ENV", { 'CIRCLE_NODE_INDEX' => 2 }) } 77 | it { should eql 2 } 78 | end 79 | 80 | context 'when SEMAPHORE_JOB_INDEX has value' do 81 | before { stub_const("ENV", { 'SEMAPHORE_JOB_INDEX' => 3 }) } 82 | it { should eql 2 } 83 | end 84 | 85 | context 'when SEMAPHORE_CURRENT_THREAD has value' do 86 | before { stub_const("ENV", { 'SEMAPHORE_CURRENT_THREAD' => 1 }) } 87 | it { should eql 0 } 88 | end 89 | 90 | context 'when BUILDKITE_PARALLEL_JOB has value' do 91 | before { stub_const("ENV", { 'BUILDKITE_PARALLEL_JOB' => 2 }) } 92 | it { should eql 2 } 93 | end 94 | 95 | context 'when SNAP_WORKER_INDEX has value' do 96 | before { stub_const("ENV", { 'SNAP_WORKER_INDEX' => 4 }) } 97 | it { should eql 3 } 98 | end 99 | 100 | context 'when BITBUCKET_PARALLEL_STEP has value' do 101 | before { stub_const("ENV", { 'BITBUCKET_PARALLEL_STEP' => 7 }) } 102 | it { should eql 7 } 103 | end 104 | end 105 | 106 | context "when ENV doesn't exist" do 107 | it { should eql 0 } 108 | end 109 | end 110 | 111 | describe '.test_file_pattern' do 112 | subject { described_class.test_file_pattern } 113 | 114 | context 'when ENV exists' do 115 | let(:test_file_pattern) { 'custom_spec/**{,/*/**}/*_spec.rb' } 116 | before { stub_const("ENV", { 'KNAPSACK_TEST_FILE_PATTERN' => test_file_pattern }) } 117 | it { should eql test_file_pattern } 118 | end 119 | 120 | context "when ENV doesn't exist" do 121 | it { should be_nil } 122 | end 123 | end 124 | 125 | describe '.test_dir' do 126 | subject { described_class.test_dir } 127 | 128 | context 'when ENV exists' do 129 | let(:test_dir) { 'spec' } 130 | before { stub_const("ENV", { 'KNAPSACK_TEST_DIR' => test_dir }) } 131 | it { should eql test_dir } 132 | end 133 | 134 | context "when ENV doesn't exist" do 135 | it { should be_nil } 136 | end 137 | end 138 | 139 | describe '.log_level' do 140 | subject { described_class.log_level } 141 | 142 | context 'when ENV exists' do 143 | let(:log_level) { 'debug' } 144 | before { stub_const("ENV", { 'KNAPSACK_LOG_LEVEL' => log_level }) } 145 | it { should eql Knapsack::Logger::DEBUG } 146 | end 147 | 148 | context "when ENV doesn't exist" do 149 | it { should eql Knapsack::Logger::INFO } 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /spec/knapsack/config/tracker_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Config::Tracker do 2 | describe '.enable_time_offset_warning' do 3 | subject { described_class.enable_time_offset_warning } 4 | it { should be true } 5 | end 6 | 7 | describe '.time_offset_in_seconds' do 8 | subject { described_class.time_offset_in_seconds } 9 | it { should eql 30 } 10 | end 11 | 12 | describe '.generate_report' do 13 | subject { described_class.generate_report } 14 | 15 | context 'when ENV exists' do 16 | it 'should be true when KNAPSACK_GENERATE_REPORT=true' do 17 | with_env 'KNAPSACK_GENERATE_REPORT' => 'true' do 18 | expect(subject).to eq(true) 19 | end 20 | end 21 | 22 | it 'should be true when KNAPSACK_GENERATE_REPORT=0' do 23 | with_env 'KNAPSACK_GENERATE_REPORT' => '0' do 24 | expect(subject).to eq(true) 25 | end 26 | end 27 | 28 | it 'should be false when KNAPSACK_GENERATE_REPORT is ""' do 29 | with_env 'KNAPSACK_GENERATE_REPORT' => '' do 30 | expect(subject).to eq(false) 31 | end 32 | end 33 | 34 | it 'should be false when KNAPSACK_GENERATE_REPORT is "false"' do 35 | with_env 'KNAPSACK_GENERATE_REPORT' => 'false' do 36 | expect(subject).to eq(false) 37 | end 38 | end 39 | 40 | it 'should be false when KNAPSACK_GENERATE_REPORT is not "true" or "0"' do 41 | with_env 'KNAPSACK_GENERATE_REPORT' => '1' do 42 | expect(subject).to eq(false) 43 | end 44 | end 45 | end 46 | 47 | context "when ENV doesn't exist" do 48 | it { should be false } 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/knapsack/distributors/base_distributor_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Distributors::BaseDistributor do 2 | let(:report) { { 'a_spec.rb' => 1.0 } } 3 | let(:default_args) do 4 | { 5 | report: report, 6 | test_file_pattern: 'spec/**{,/*/**}/*_spec.rb', 7 | ci_node_total: '1', 8 | ci_node_index: '0' 9 | } 10 | end 11 | let(:args) { default_args.merge(custom_args) } 12 | let(:custom_args) { {} } 13 | let(:distributor) { described_class.new(args) } 14 | 15 | describe '#report' do 16 | subject { distributor.report } 17 | 18 | context 'when report is given' do 19 | it { should eql(report) } 20 | end 21 | 22 | context 'when report is not given' do 23 | let(:custom_args) { { report: nil } } 24 | it { expect { subject }.to raise_error('Missing report') } 25 | end 26 | end 27 | 28 | describe '#ci_node_total' do 29 | subject { distributor.ci_node_total } 30 | 31 | context 'when ci_node_total is given' do 32 | it { should eql 1 } 33 | end 34 | 35 | context 'when ci_node_total is not given' do 36 | let(:custom_args) { { ci_node_total: nil } } 37 | it { expect { subject }.to raise_error('Missing ci_node_total') } 38 | end 39 | end 40 | 41 | describe '#ci_node_index' do 42 | subject { distributor.ci_node_index } 43 | 44 | context 'when ci_node_index is given' do 45 | it { should eql 0 } 46 | end 47 | 48 | context 'when ci_node_index is not given' do 49 | let(:custom_args) { { ci_node_index: nil } } 50 | it { expect { subject }.to raise_error('Missing ci_node_index') } 51 | end 52 | 53 | context 'when ci_node_index has not allowed value' do 54 | let(:expected_exception_message) do 55 | "Node indexes are 0-based. Can't be higher or equal to the total number of nodes." 56 | end 57 | 58 | context 'when ci_node_index is equal to ci_node_total' do 59 | let(:custom_args) { { ci_node_index: 1, ci_node_total: 1 } } 60 | it { expect { subject }.to raise_error(expected_exception_message) } 61 | end 62 | 63 | context 'when ci_node_index is higher than ci_node_total' do 64 | let(:custom_args) { { ci_node_index: 2, ci_node_total: 1 } } 65 | it { expect { subject }.to raise_error(expected_exception_message) } 66 | end 67 | end 68 | end 69 | 70 | describe '#tests_for_current_node' do 71 | let(:custom_args) do 72 | { 73 | ci_node_total: 3, 74 | ci_node_index: ci_node_index 75 | } 76 | end 77 | let(:ci_node_index) { 2 } 78 | let(:tests) { double } 79 | 80 | subject { distributor.tests_for_current_node } 81 | 82 | before do 83 | expect(distributor).to receive(:tests_for_node).with(ci_node_index).and_return(tests) 84 | end 85 | 86 | it { should eql tests } 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/knapsack/distributors/leftover_distributor_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Distributors::LeftoverDistributor do 2 | let(:report) do 3 | { 4 | 'a_spec.rb' => 1.0, 5 | 'b_spec.rb' => 1.5, 6 | 'c_spec.rb' => 2.0, 7 | 'd_spec.rb' => 2.5, 8 | } 9 | end 10 | let(:test_file_pattern) { 'spec/**{,/*/**}/*_spec.rb' } 11 | let(:default_args) do 12 | { 13 | report: report, 14 | test_file_pattern: test_file_pattern, 15 | ci_node_total: '1', 16 | ci_node_index: '0' 17 | } 18 | end 19 | let(:args) { default_args.merge(custom_args) } 20 | let(:custom_args) { {} } 21 | let(:distributor) { described_class.new(args) } 22 | 23 | describe '#report_tests' do 24 | subject { distributor.report_tests } 25 | it { should eql ['a_spec.rb', 'b_spec.rb', 'c_spec.rb', 'd_spec.rb'] } 26 | end 27 | 28 | describe '#all_tests' do 29 | subject { distributor.all_tests } 30 | 31 | context 'when given test_file_pattern' do 32 | context 'spec/**{,/*/**}/*_spec.rb' do 33 | it { should_not be_empty } 34 | it { should include 'spec/knapsack/tracker_spec.rb' } 35 | it { should include 'spec/knapsack/adapters/rspec_adapter_spec.rb' } 36 | 37 | it 'has no duplicated test file paths' do 38 | expect(subject.size).to eq subject.uniq.size 39 | end 40 | end 41 | 42 | context 'spec_examples/**{,/*/**}/*_spec.rb' do 43 | let(:test_file_pattern) { 'spec_examples/**{,/*/**}/*_spec.rb' } 44 | 45 | it { should_not be_empty } 46 | it { should include 'spec_examples/fast/1_spec.rb' } 47 | it { should include 'spec_examples/leftover/a_spec.rb' } 48 | 49 | it 'has no duplicated test file paths' do 50 | expect(subject.size).to eq subject.uniq.size 51 | end 52 | end 53 | end 54 | 55 | context 'when fake test_file_pattern' do 56 | let(:test_file_pattern) { 'fake_pattern' } 57 | it { should be_empty } 58 | end 59 | 60 | context 'when missing test_file_pattern' do 61 | let(:test_file_pattern) { nil } 62 | it { expect { subject }.to raise_error('Missing test_file_pattern') } 63 | end 64 | end 65 | 66 | describe '#leftover_tests' do 67 | subject { distributor.leftover_tests } 68 | 69 | before do 70 | expect(distributor).to receive(:all_tests).and_return([ 71 | 'a_spec.rb', 72 | 'b_spec.rb', 73 | 'c_spec.rb', 74 | 'd_spec.rb', 75 | 'e_spec.rb', 76 | 'f_spec.rb', 77 | ]) 78 | end 79 | 80 | it { should eql ['e_spec.rb', 'f_spec.rb'] } 81 | end 82 | 83 | context do 84 | let(:custom_args) { { ci_node_total: 3 } } 85 | let(:leftover_tests) {[ 86 | 'a_spec.rb', 87 | 'b_spec.rb', 88 | 'c_spec.rb', 89 | 'd_spec.rb', 90 | 'e_spec.rb', 91 | 'f_spec.rb', 92 | 'g_spec.rb', 93 | ]} 94 | 95 | before do 96 | expect(distributor).to receive(:leftover_tests).and_return(leftover_tests) 97 | end 98 | 99 | describe '#assign_test_files_to_node' do 100 | before do 101 | distributor.assign_test_files_to_node 102 | end 103 | 104 | it do 105 | expect(distributor.node_tests[0]).to eql([ 106 | 'a_spec.rb', 107 | 'd_spec.rb', 108 | 'g_spec.rb', 109 | ]) 110 | end 111 | 112 | it do 113 | expect(distributor.node_tests[1]).to eql([ 114 | 'b_spec.rb', 115 | 'e_spec.rb', 116 | ]) 117 | end 118 | 119 | it do 120 | expect(distributor.node_tests[2]).to eql([ 121 | 'c_spec.rb', 122 | 'f_spec.rb', 123 | ]) 124 | end 125 | end 126 | 127 | describe '#tests_for_node' do 128 | it do 129 | expect(distributor.tests_for_node(1)).to eql([ 130 | 'b_spec.rb', 131 | 'e_spec.rb', 132 | ]) 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/knapsack/distributors/report_distributor_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Distributors::ReportDistributor do 2 | let(:report) { { 'a_spec.rb' => 1.0 } } 3 | let(:default_args) do 4 | { 5 | report: report, 6 | test_file_pattern: 'spec/**{,/*/**}/*_spec.rb', 7 | ci_node_total: '1', 8 | ci_node_index: '0' 9 | } 10 | end 11 | let(:args) { default_args.merge(custom_args) } 12 | let(:custom_args) { {} } 13 | let(:distributor) { described_class.new(args) } 14 | 15 | describe '#sorted_report' do 16 | subject { distributor.sorted_report } 17 | 18 | let(:report) do 19 | { 20 | 'e_spec.rb' => 3.0, 21 | 'f_spec.rb' => 3.5, 22 | 'c_spec.rb' => 2.0, 23 | 'd_spec.rb' => 2.5, 24 | 'a_spec.rb' => 1.0, 25 | 'b_spec.rb' => 1.5, 26 | } 27 | end 28 | 29 | it do 30 | should eql([ 31 | ['f_spec.rb', 3.5], 32 | ['e_spec.rb', 3.0], 33 | ['d_spec.rb', 2.5], 34 | ['c_spec.rb', 2.0], 35 | ['b_spec.rb', 1.5], 36 | ['a_spec.rb', 1.0], 37 | ]) 38 | end 39 | end 40 | 41 | describe '#sorted_report_with_existing_tests' do 42 | subject { distributor.sorted_report_with_existing_tests } 43 | 44 | let(:report) do 45 | { 46 | 'e_spec.rb' => 3.0, 47 | 'f_spec.rb' => 3.5, 48 | 'c_spec.rb' => 2.0, 49 | 'd_spec.rb' => 2.5, 50 | 'a_spec.rb' => 1.0, 51 | 'b_spec.rb' => 1.5, 52 | } 53 | end 54 | 55 | before do 56 | expect(distributor).to receive(:all_tests).exactly(6).times.and_return([ 57 | 'b_spec.rb', 58 | 'd_spec.rb', 59 | 'f_spec.rb', 60 | ]) 61 | end 62 | 63 | it do 64 | should eql([ 65 | ['f_spec.rb', 3.5], 66 | ['d_spec.rb', 2.5], 67 | ['b_spec.rb', 1.5], 68 | ]) 69 | end 70 | end 71 | 72 | context do 73 | let(:report) do 74 | { 75 | 'a_spec.rb' => 3.0, 76 | 'b_spec.rb' => 1.0, 77 | 'c_spec.rb' => 1.5, 78 | } 79 | end 80 | 81 | before do 82 | allow(distributor).to receive(:all_tests).and_return(report.keys) 83 | end 84 | 85 | describe '#total_time_execution' do 86 | subject { distributor.total_time_execution } 87 | 88 | context 'when time is float' do 89 | it { should eql 5.5 } 90 | end 91 | 92 | context 'when time is not float' do 93 | let(:report) do 94 | { 95 | 'a_spec.rb' => 3, 96 | 'b_spec.rb' => 1, 97 | } 98 | end 99 | 100 | it { should eql 4.0 } 101 | end 102 | end 103 | 104 | describe '#node_time_execution' do 105 | subject { distributor.node_time_execution } 106 | let(:custom_args) { { ci_node_total: 4 } } 107 | it { should eql 1.375 } 108 | end 109 | end 110 | 111 | context do 112 | let(:report) do 113 | { 114 | 'g_spec.rb' => 9.0, 115 | 'h_spec.rb' => 3.0, 116 | 'i_spec.rb' => 3.0, 117 | 'f_spec.rb' => 3.5, 118 | 'c_spec.rb' => 2.0, 119 | 'd_spec.rb' => 2.5, 120 | 'a_spec.rb' => 1.0, 121 | 'b_spec.rb' => 1.5 122 | } 123 | end 124 | let(:custom_args) { { ci_node_total: 3 } } 125 | 126 | before do 127 | allow(distributor).to receive(:all_tests).and_return(report.keys) 128 | end 129 | 130 | describe '#assign_test_files_to_node' do 131 | before { distributor.assign_test_files_to_node } 132 | 133 | it do 134 | expect(distributor.node_tests[0]).to eql({ 135 | :node_index => 0, 136 | :time_left => -0.5, 137 | :weight => 9.0, 138 | :test_files_with_time => [ 139 | ["g_spec.rb", 9.0] 140 | ] 141 | }) 142 | end 143 | 144 | it do 145 | expect(distributor.node_tests[1]).to eql({ 146 | :node_index => 1, 147 | :time_left => 0.5, 148 | :weight => 8.0, 149 | :test_files_with_time => [ 150 | ["f_spec.rb", 3.5], 151 | ["d_spec.rb", 2.5], 152 | ["c_spec.rb", 2.0] 153 | ] 154 | }) 155 | end 156 | 157 | it do 158 | expect(distributor.node_tests[2]).to eql({ 159 | :node_index => 2, 160 | :time_left => 0.0, 161 | :weight => 8.5, 162 | :test_files_with_time => [ 163 | ["h_spec.rb", 3.0], 164 | ["i_spec.rb", 3.0], 165 | ["b_spec.rb", 1.5], 166 | ["a_spec.rb", 1.0] 167 | ] 168 | }) 169 | end 170 | end 171 | 172 | describe '#tests_for_node' do 173 | context 'when node exists' do 174 | it do 175 | expect(distributor.tests_for_node(1)).to eql([ 176 | "f_spec.rb", 177 | "d_spec.rb", 178 | "c_spec.rb" 179 | ]) 180 | end 181 | end 182 | 183 | context "when node doesn't exist" do 184 | it { expect(distributor.tests_for_node(42)).to be_nil } 185 | end 186 | end 187 | end 188 | 189 | describe 'algorithmic efficiency' do 190 | subject(:node_weights) do 191 | distro = distributor 192 | distro.assign_test_files_to_node 193 | distro.node_tests.map { |node| node[:weight] } 194 | end 195 | 196 | before do 197 | allow(distributor).to receive(:all_tests).and_return(report.keys) 198 | end 199 | 200 | let(:custom_args) { { ci_node_total: 3 } } 201 | 202 | context 'with the most simple example' do 203 | let(:report) do 204 | { 205 | 'a_spec.rb' => 1.0, 206 | 'b_spec.rb' => 1.0, 207 | 'c_spec.rb' => 1.0, 208 | 'd_spec.rb' => 1.0, 209 | 'e_spec.rb' => 1.0, 210 | 'f_spec.rb' => 1.0, 211 | 'g_spec.rb' => 1.0, 212 | 'h_spec.rb' => 1.0, 213 | 'i_spec.rb' => 1.0 214 | } 215 | end 216 | 217 | it 'assigns all nodes equally' do 218 | expect(node_weights.uniq).to contain_exactly 3.0 219 | end 220 | end 221 | 222 | context 'with a medium difficulty example' do 223 | let(:report) do 224 | { 225 | 'a_spec.rb' => 1.0, 226 | 'b_spec.rb' => 2.0, 227 | 'c_spec.rb' => 3.0, 228 | 'd_spec.rb' => 1.0, 229 | 'e_spec.rb' => 2.0, 230 | 'f_spec.rb' => 3.0, 231 | 'g_spec.rb' => 1.0, 232 | 'h_spec.rb' => 2.0, 233 | 'i_spec.rb' => 3.0 234 | } 235 | end 236 | 237 | it 'assigns all nodes equally' do 238 | expect(node_weights.uniq).to contain_exactly 6.0 239 | end 240 | end 241 | 242 | context 'with a difficult example' do 243 | let(:report) do 244 | { 245 | 'a_spec.rb' => 2.0, 246 | 'b_spec.rb' => 2.0, 247 | 'c_spec.rb' => 3.0, 248 | 'd_spec.rb' => 1.0, 249 | 'e_spec.rb' => 1.0, 250 | 'f_spec.rb' => 1.0, 251 | 'g_spec.rb' => 9.0, 252 | 'h_spec.rb' => 1.0, 253 | 'i_spec.rb' => 10.0 254 | } 255 | end 256 | 257 | it 'assigns all nodes equally' do 258 | expect(node_weights.uniq).to contain_exactly 10.0 259 | end 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /spec/knapsack/extensions/time_spec.rb: -------------------------------------------------------------------------------- 1 | describe Time do 2 | it 'responds to :raw_now' do 3 | expect(Time.respond_to?(:raw_now)).to be true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/knapsack/logger_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Logger do 2 | let(:text) { 'Text' } 3 | 4 | describe '#debug' do 5 | before { subject.level = level } 6 | 7 | context 'when level is DEBUG' do 8 | let(:level) { described_class::DEBUG } 9 | it { expect { subject.debug(text) }.to output(/#{text}/).to_stdout } 10 | end 11 | 12 | context 'when level is INFO' do 13 | let(:level) { described_class::INFO } 14 | it { expect { subject.debug(text) }.to output('').to_stdout } 15 | end 16 | 17 | context 'when level is WARN' do 18 | let(:level) { described_class::WARN } 19 | it { expect { subject.debug(text) }.to output('').to_stdout } 20 | end 21 | end 22 | 23 | describe '#info' do 24 | before { subject.level = level } 25 | 26 | context 'when level is DEBUG' do 27 | let(:level) { described_class::DEBUG } 28 | it { expect { subject.info(text) }.to output(/#{text}/).to_stdout } 29 | end 30 | 31 | context 'when level is INFO' do 32 | let(:level) { described_class::INFO } 33 | it { expect { subject.info(text) }.to output(/#{text}/).to_stdout } 34 | end 35 | 36 | context 'when level is WARN' do 37 | let(:level) { described_class::WARN } 38 | it { expect { subject.info(text) }.to output('').to_stdout } 39 | end 40 | end 41 | 42 | describe '#warn' do 43 | before { subject.level = level } 44 | 45 | context 'when level is DEBUG' do 46 | let(:level) { described_class::DEBUG } 47 | it { expect { subject.warn(text) }.to output(/#{text}/).to_stdout } 48 | end 49 | 50 | context 'when level is INFO' do 51 | let(:level) { described_class::INFO } 52 | it { expect { subject.warn(text) }.to output(/#{text}/).to_stdout } 53 | end 54 | 55 | context 'when level is WARN' do 56 | let(:level) { described_class::WARN } 57 | it { expect { subject.warn(text) }.to output(/#{text}/).to_stdout } 58 | end 59 | end 60 | 61 | describe '#log' do 62 | let(:log_level) { Knapsack::Logger::INFO } 63 | let(:log_message) { 'log-message' } 64 | 65 | it 'delegates to the method matching the specified log level' do 66 | expect(subject).to receive(:info).with(log_message) 67 | 68 | subject.log(log_level, log_message) 69 | end 70 | 71 | context 'when the log level is unknown' do 72 | let(:log_level) { 5 } 73 | 74 | it 'raises an UnknownLogLevel error' do 75 | expect { 76 | subject.log(log_level, log_message) 77 | }.to raise_error Knapsack::Logger::UnknownLogLevel 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/knapsack/presenter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Presenter do 2 | let(:tracker) { instance_double(Knapsack::Tracker) } 3 | let(:test_files_with_time) do 4 | { 5 | 'a_spec.rb' => 1.0, 6 | 'b_spec.rb' => 0.4 7 | } 8 | end 9 | 10 | describe 'report methods' do 11 | before do 12 | expect(Knapsack).to receive(:tracker) { tracker } 13 | expect(tracker).to receive(:test_files_with_time).and_return(test_files_with_time) 14 | end 15 | 16 | describe '.report_yml' do 17 | subject { described_class.report_yml } 18 | it { should eql test_files_with_time.to_yaml } 19 | end 20 | 21 | describe '.report_json' do 22 | subject { described_class.report_json } 23 | it { should eql JSON.pretty_generate(test_files_with_time) } 24 | end 25 | end 26 | 27 | describe '.global_time' do 28 | subject { described_class.global_time } 29 | 30 | before do 31 | expect(Knapsack).to receive(:tracker) { tracker } 32 | expect(tracker).to receive(:global_time).and_return(60*62+3) 33 | end 34 | 35 | it { should eql "\nKnapsack global time execution for tests: 01h 02m 03s" } 36 | end 37 | 38 | describe '.report_details' do 39 | subject { described_class.report_details } 40 | 41 | before do 42 | expect(described_class).to receive(:report_json).and_return('{}') 43 | end 44 | 45 | it { should eql "Knapsack report was generated. Preview:\n{}" } 46 | end 47 | 48 | describe '.time_offset_log_level' do 49 | before do 50 | allow(Knapsack).to receive(:tracker) { tracker } 51 | allow(tracker).to receive(:time_exceeded?).and_return(time_exceeded) 52 | end 53 | 54 | context 'when the time offset is exceeded' do 55 | let(:time_exceeded) { true } 56 | 57 | it 'returns a WARN log level' do 58 | expect(described_class.time_offset_log_level).to eq(Knapsack::Logger::WARN) 59 | end 60 | end 61 | 62 | context 'when the time offset is not exceeded' do 63 | let(:time_exceeded) { false } 64 | 65 | it 'returns an INFO log level' do 66 | expect(described_class.time_offset_log_level).to eq(Knapsack::Logger::INFO) 67 | end 68 | end 69 | end 70 | 71 | describe '.time_offset_warning' do 72 | let(:time_offset_in_seconds) { 30 } 73 | let(:max_node_time_execution) { 60 } 74 | let(:exceeded_time) { 3 } 75 | 76 | subject { described_class.time_offset_warning } 77 | 78 | before do 79 | allow(Knapsack).to receive(:tracker) { tracker } 80 | expect(tracker).to receive(:config).and_return({time_offset_in_seconds: time_offset_in_seconds}) 81 | expect(tracker).to receive(:max_node_time_execution).and_return(max_node_time_execution) 82 | expect(tracker).to receive(:exceeded_time).and_return(exceeded_time) 83 | expect(tracker).to receive(:time_exceeded?).and_return(time_exceeded?) 84 | end 85 | 86 | shared_examples 'knapsack time offset warning' do 87 | it { should include 'Time offset: 30s' } 88 | it { should include 'Max allowed node time execution: 01m' } 89 | it { should include 'Exceeded time: 03s' } 90 | end 91 | 92 | context 'when time exceeded' do 93 | let(:time_exceeded?) { true } 94 | 95 | it_behaves_like 'knapsack time offset warning' 96 | it { should include 'Please regenerate your knapsack report.' } 97 | end 98 | 99 | context "when time did not exceed" do 100 | let(:time_exceeded?) { false } 101 | 102 | it_behaves_like 'knapsack time offset warning' 103 | it { should include 'Global time execution for this CI node is fine.' } 104 | end 105 | end 106 | 107 | describe '.pretty_seconds' do 108 | subject { described_class.pretty_seconds(seconds) } 109 | 110 | context 'when less then one second' do 111 | let(:seconds) { 0.987 } 112 | it { should eql '0.987s' } 113 | end 114 | 115 | context 'when one second' do 116 | let(:seconds) { 1 } 117 | it { should eql '01s' } 118 | end 119 | 120 | context 'when only seconds' do 121 | let(:seconds) { 5 } 122 | it { should eql '05s' } 123 | end 124 | 125 | context 'when only minutes' do 126 | let(:seconds) { 120 } 127 | it { should eql '02m' } 128 | end 129 | 130 | context 'when only hours' do 131 | let(:seconds) { 60*60*3 } 132 | it { should eql '03h' } 133 | end 134 | 135 | context 'when minutes and seconds' do 136 | let(:seconds) { 180+9 } 137 | it { should eql '03m 09s' } 138 | end 139 | 140 | context 'when all' do 141 | let(:seconds) { 60*60*4+120+7 } 142 | it { should eql '04h 02m 07s' } 143 | end 144 | 145 | context 'when negative seconds' do 146 | let(:seconds) { -67 } 147 | it { should eql '-01m 07s' } 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/knapsack/report_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::Report do 2 | let(:report) { described_class.send(:new) } 3 | let(:report_path) { 'tmp/fake_report.json' } 4 | let(:report_json) do 5 | %Q[{"a_spec.rb": #{rand(Math::E..Math::PI)}}] 6 | end 7 | 8 | describe '#config' do 9 | context 'when passed options' do 10 | let(:args) do 11 | { 12 | report_path: 'knapsack_new_report.json', 13 | fake: true 14 | } 15 | end 16 | 17 | it do 18 | expect(report.config(args)).to eql({ 19 | report_path: 'knapsack_new_report.json', 20 | fake: true 21 | }) 22 | end 23 | end 24 | 25 | context "when didn't pass options" do 26 | it { expect(report.config).to eql({}) } 27 | end 28 | end 29 | 30 | describe '#save', :clear_tmp do 31 | before do 32 | expect(report).to receive(:report_json).and_return(report_json) 33 | report.config({ 34 | report_path: report_path 35 | }) 36 | report.save 37 | end 38 | 39 | it { expect(File.read(report_path)).to eql report_json } 40 | end 41 | 42 | describe '.open' do 43 | let(:subject) { report.open } 44 | 45 | before do 46 | report.config({ 47 | report_path: report_path 48 | }) 49 | end 50 | 51 | context 'when report file exists' do 52 | before do 53 | expect(File).to receive(:read).with(report_path).and_return(report_json) 54 | end 55 | 56 | it { should eql(JSON.parse(report_json)) } 57 | end 58 | 59 | context "when report file doesn't exist" do 60 | let(:report_path) { 'tmp/non_existing_report.json' } 61 | 62 | it do 63 | expect { 64 | subject 65 | }.to raise_error("Knapsack report file #{report_path} doesn't exist. Please generate report first!") 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/knapsack/task_loader_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack::TaskLoader do 2 | describe '#load_tasks' do 3 | let(:rspec_rake_task_path) { "#{Knapsack.root}/lib/tasks/knapsack_rspec.rake" } 4 | let(:cucumber_rake_task_path) { "#{Knapsack.root}/lib/tasks/knapsack_cucumber.rake" } 5 | let(:spinach_rake_task_path) { "#{Knapsack.root}/lib/tasks/knapsack_spinach.rake" } 6 | let(:minitest_rake_task_path) { "#{Knapsack.root}/lib/tasks/knapsack_minitest.rake" } 7 | 8 | it do 9 | expect(subject).to receive(:import).with(rspec_rake_task_path) 10 | expect(subject).to receive(:import).with(cucumber_rake_task_path) 11 | expect(subject).to receive(:import).with(spinach_rake_task_path) 12 | expect(subject).to receive(:import).with(minitest_rake_task_path) 13 | subject.load_tasks 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/knapsack/tracker_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'default trakcer attributes' do 2 | it { expect(tracker.global_time).to eql 0 } 3 | it { expect(tracker.test_files_with_time).to eql({}) } 4 | end 5 | 6 | describe Knapsack::Tracker do 7 | let(:tracker) { described_class.send(:new) } 8 | 9 | it_behaves_like 'default trakcer attributes' 10 | 11 | describe '#config' do 12 | context 'when passed options' do 13 | let(:generate_report) { 'true' } 14 | let(:opts) do 15 | { 16 | enable_time_offset_warning: false, 17 | fake: true 18 | } 19 | end 20 | 21 | it do 22 | with_env 'KNAPSACK_GENERATE_REPORT' => generate_report do 23 | expect(tracker.config(opts)).to eql({ 24 | enable_time_offset_warning: false, 25 | time_offset_in_seconds: 30, 26 | generate_report: true, 27 | fake: true 28 | }) 29 | end 30 | end 31 | end 32 | 33 | context "when didn't pass options" do 34 | let(:generate_report) { nil } 35 | 36 | it do 37 | expect(tracker.config).to eql({ 38 | enable_time_offset_warning: true, 39 | time_offset_in_seconds: 30, 40 | generate_report: false 41 | }) 42 | end 43 | end 44 | end 45 | 46 | describe '#test_path' do 47 | subject { tracker.test_path } 48 | 49 | context 'when test_path not set' do 50 | it do 51 | expect(subject).to be_nil 52 | end 53 | end 54 | 55 | context 'when test_path set' do 56 | context 'when test_path has prefix ./' do 57 | before { tracker.test_path = './spec/models/user_spec.rb' } 58 | it { should eql 'spec/models/user_spec.rb' } 59 | end 60 | 61 | context 'when test_path has not prefix ./' do 62 | before { tracker.test_path = 'spec/models/user_spec.rb' } 63 | it { should eql 'spec/models/user_spec.rb' } 64 | end 65 | end 66 | end 67 | 68 | describe '#time_exceeded?' do 69 | subject { tracker.time_exceeded? } 70 | 71 | before do 72 | expect(tracker).to receive(:global_time).and_return(global_time) 73 | expect(tracker).to receive(:max_node_time_execution).and_return(max_node_time_execution) 74 | end 75 | 76 | context 'when true' do 77 | let(:global_time) { 2 } 78 | let(:max_node_time_execution) { 1 } 79 | it { should be true } 80 | end 81 | 82 | context 'when false' do 83 | let(:global_time) { 1 } 84 | let(:max_node_time_execution) { 1 } 85 | it { should be false } 86 | end 87 | end 88 | 89 | describe '#max_node_time_execution' do 90 | let(:report_distributor) { instance_double(Knapsack::Distributors::ReportDistributor) } 91 | let(:node_time_execution) { 3.5 } 92 | let(:max_node_time_execution) { node_time_execution + tracker.config[:time_offset_in_seconds] } 93 | 94 | subject { tracker.max_node_time_execution } 95 | 96 | before do 97 | expect(tracker).to receive(:report_distributor).and_return(report_distributor) 98 | expect(report_distributor).to receive(:node_time_execution).and_return(node_time_execution) 99 | end 100 | 101 | it { should eql max_node_time_execution } 102 | end 103 | 104 | describe '#exceeded_time' do 105 | let(:global_time) { 5 } 106 | let(:max_node_time_execution) { 2 } 107 | 108 | subject { tracker.exceeded_time } 109 | 110 | before do 111 | expect(tracker).to receive(:global_time).and_return(global_time) 112 | expect(tracker).to receive(:max_node_time_execution).and_return(max_node_time_execution) 113 | end 114 | 115 | it { should eql 3 } 116 | end 117 | 118 | describe 'track time execution' do 119 | let(:test_paths) { ['a_spec.rb', 'b_spec.rb'] } 120 | let(:delta) { 0.02 } 121 | 122 | context 'without Timecop' do 123 | before do 124 | test_paths.each_with_index do |test_path, index| 125 | tracker.test_path = test_path 126 | tracker.start_timer 127 | sleep index.to_f / 10 + 0.1 128 | tracker.stop_timer 129 | end 130 | end 131 | 132 | it { expect(tracker.global_time).to be_within(delta).of(0.3) } 133 | it { expect(tracker.test_files_with_time.keys.size).to eql 2 } 134 | it { expect(tracker.test_files_with_time['a_spec.rb']).to be_within(delta).of(0.1) } 135 | it { expect(tracker.test_files_with_time['b_spec.rb']).to be_within(delta).of(0.2) } 136 | it 'resets test_path after time is measured' do 137 | expect(tracker.test_path).to be_nil 138 | end 139 | end 140 | 141 | context "with Timecop - Timecop shouldn't have impact on measured test time" do 142 | let(:now) { Time.now } 143 | 144 | before do 145 | test_paths.each_with_index do |test_path, index| 146 | Timecop.freeze(now) do 147 | tracker.test_path = test_path 148 | tracker.start_timer 149 | end 150 | 151 | delay = index + 1 152 | Timecop.freeze(now+delay) do 153 | tracker.stop_timer 154 | end 155 | end 156 | end 157 | 158 | it { expect(tracker.global_time).to be > 0 } 159 | it { expect(tracker.global_time).to be_within(delta).of(0) } 160 | it { expect(tracker.test_files_with_time.keys.size).to eql 2 } 161 | it { expect(tracker.test_files_with_time['a_spec.rb']).to be_within(delta).of(0) } 162 | it { expect(tracker.test_files_with_time['b_spec.rb']).to be_within(delta).of(0) } 163 | it 'resets test_path after time is measured' do 164 | expect(tracker.test_path).to be_nil 165 | end 166 | end 167 | end 168 | 169 | describe '#reset!' do 170 | before do 171 | tracker.test_path = 'a_spec.rb' 172 | tracker.start_timer 173 | sleep 0.1 174 | tracker.stop_timer 175 | expect(tracker.global_time).not_to eql 0 176 | tracker.reset! 177 | end 178 | 179 | it_behaves_like 'default trakcer attributes' 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/knapsack_spec.rb: -------------------------------------------------------------------------------- 1 | describe Knapsack do 2 | describe '.tracker' do 3 | subject { described_class.tracker } 4 | 5 | it { should be_a Knapsack::Tracker } 6 | it { expect(subject.object_id).to eql described_class.tracker.object_id } 7 | end 8 | 9 | describe '.report' do 10 | subject { described_class.report } 11 | 12 | it { should be_a Knapsack::Report } 13 | it { expect(subject.object_id).to eql described_class.report.object_id } 14 | end 15 | 16 | describe '.root' do 17 | subject { described_class.root } 18 | 19 | it { expect(subject).to match 'knapsack' } 20 | end 21 | 22 | describe '.load_tasks' do 23 | let(:task_loader) { instance_double(Knapsack::TaskLoader) } 24 | 25 | it do 26 | expect(Knapsack::TaskLoader).to receive(:new).and_return(task_loader) 27 | expect(task_loader).to receive(:load_tasks) 28 | described_class.load_tasks 29 | end 30 | end 31 | 32 | describe '.logger' do 33 | subject { described_class.logger } 34 | 35 | before { described_class.logger = nil } 36 | after { described_class.logger = nil } 37 | 38 | context 'when default logger' do 39 | let(:logger) { instance_double(Knapsack::Logger) } 40 | 41 | before do 42 | expect(Knapsack::Logger).to receive(:new).and_return(logger) 43 | expect(logger).to receive(:level=).with(Knapsack::Logger::INFO) 44 | end 45 | 46 | it { should eql logger } 47 | end 48 | 49 | context 'when custom logger' do 50 | let(:logger) { double('custom logger') } 51 | 52 | before do 53 | described_class.logger = logger 54 | end 55 | 56 | it { should eql logger } 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/its' 2 | require 'spinach' 3 | 4 | require 'timecop' 5 | Timecop.safe_mode = true 6 | 7 | require 'knapsack' 8 | 9 | Dir["#{Knapsack.root}/spec/support/**/*.rb"].each { |f| require f } 10 | 11 | RSpec.configure do |config| 12 | config.order = :random 13 | config.mock_with :rspec do |mocks| 14 | mocks.syntax = :expect 15 | mocks.verify_partial_doubles = true 16 | end 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | 22 | config.before(:each) do 23 | if RSpec.current_example.metadata[:clear_tmp] 24 | FileUtils.mkdir_p(File.join(Knapsack.root, 'tmp')) 25 | end 26 | end 27 | 28 | config.after(:each) do 29 | if RSpec.current_example.metadata[:clear_tmp] 30 | FileUtils.rm_r(File.join(Knapsack.root, 'tmp')) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/env_helper.rb: -------------------------------------------------------------------------------- 1 | module EnvHelper 2 | def with_env(vars) 3 | original = ENV.to_hash 4 | vars.each { |k, v| ENV[k] = v } 5 | 6 | begin 7 | yield 8 | ensure 9 | ENV.replace(original) 10 | end 11 | end 12 | end 13 | RSpec.configuration.include EnvHelper 14 | -------------------------------------------------------------------------------- /spec/support/fakes/cucumber.rb: -------------------------------------------------------------------------------- 1 | module Cucumber 2 | # Cucumber 1 and 2 3 | # https://github.com/cucumber/cucumber-ruby/blob/v2.99.0/lib/cucumber/rb_support/rb_dsl.rb 4 | module RbSupport 5 | class RbDsl 6 | class << self 7 | def register_rb_hook(phase, tag_names, proc) 8 | proc.call 9 | end 10 | end 11 | end 12 | end 13 | 14 | # Cucumber 3 15 | # https://github.com/cucumber/cucumber-ruby/blob/v3.0.0/lib/cucumber/glue/dsl.rb 16 | module Glue 17 | class Dsl 18 | class << self 19 | def register_rb_hook(phase, tag_names, proc) 20 | proc.call 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/fakes/minitest.rb: -------------------------------------------------------------------------------- 1 | # https://github.com/seattlerb/minitest/blob/master/lib/minitest/test.rb 2 | module Minitest 3 | class Test 4 | def before_setup; end 5 | def after_teardown; end 6 | end 7 | 8 | # https://github.com/seattlerb/minitest/blob/master/lib/minitest.rb 9 | def self.after_run(&block) 10 | block.call 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/shared_examples/adapter.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'adapter' do 2 | describe '#bind_time_tracker' do 3 | it do 4 | expect { 5 | subject.bind_time_tracker 6 | }.not_to raise_error 7 | end 8 | end 9 | 10 | describe '#bind_report_generator' do 11 | it do 12 | expect { 13 | subject.bind_report_generator 14 | }.not_to raise_error 15 | end 16 | end 17 | 18 | describe '#bind_time_offset_warning' do 19 | it do 20 | expect { 21 | subject.bind_time_offset_warning 22 | }.not_to raise_error 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec_engine_examples/1_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Engine 1' do 2 | it {} 3 | end 4 | -------------------------------------------------------------------------------- /spec_examples/fast/1_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 1', :focus, :custom_focus do 2 | it {} 3 | end 4 | -------------------------------------------------------------------------------- /spec_examples/fast/2_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 2', :focus do 2 | it {} 3 | it {} 4 | end 5 | -------------------------------------------------------------------------------- /spec_examples/fast/3_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 3', :focus do 2 | it {} 3 | it {} 4 | it {} 5 | end 6 | -------------------------------------------------------------------------------- /spec_examples/fast/4_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 4', :focus do 2 | it {} 3 | it {} 4 | it {} 5 | it {} 6 | end 7 | -------------------------------------------------------------------------------- /spec_examples/fast/5_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 5', :focus do 2 | it {} 3 | it {} 4 | it {} 5 | it {} 6 | it {} 7 | end 8 | -------------------------------------------------------------------------------- /spec_examples/fast/6_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Fast 6', :focus do 2 | it {} 3 | it {} 4 | it {} 5 | it {} 6 | it {} 7 | it {} 8 | end 9 | -------------------------------------------------------------------------------- /spec_examples/fast/use_shared_example_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Use Shared Example', :focus do 2 | it_behaves_like 'common exmaple' 3 | end 4 | -------------------------------------------------------------------------------- /spec_examples/leftover/1_spec.rb: -------------------------------------------------------------------------------- 1 | # this file should not be included in knapsack_rspec_report.json 2 | describe 'Leftover Fast 1', :custom_focus do 3 | it {} 4 | end 5 | -------------------------------------------------------------------------------- /spec_examples/leftover/a_spec.rb: -------------------------------------------------------------------------------- 1 | # this file should not be included in knapsack_rspec_report.json 2 | describe 'Leftover Slow A' do 3 | it { sleep 1 } 4 | it { sleep 0.1 } 5 | it { sleep 0.5 } 6 | it { sleep 3 } 7 | it { sleep 1 } 8 | end 9 | -------------------------------------------------------------------------------- /spec_examples/slow/a_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Slow A', :focus do 2 | it { sleep 1 } 3 | it { sleep 0.1 } 4 | it { sleep 0.5 } 5 | end 6 | -------------------------------------------------------------------------------- /spec_examples/slow/b_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Slow B', :focus do 2 | it { sleep 0.3 } 3 | it { sleep 0.2 } 4 | it { sleep 0.4 } 5 | end 6 | -------------------------------------------------------------------------------- /spec_examples/slow/c_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Slow C', :focus do 2 | it { sleep 0.8 } 3 | it { sleep 0.1 } 4 | it { sleep 0.1 } 5 | end 6 | -------------------------------------------------------------------------------- /spec_examples/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'knapsack' 2 | require 'support/shared_examples/common_example' 3 | 4 | Knapsack.tracker.config({ 5 | enable_time_offset_warning: true, 6 | time_offset_in_seconds: 3 7 | }) 8 | Knapsack.report.config({ 9 | report_path: 'knapsack_rspec_report.json' 10 | }) 11 | 12 | if ENV['CUSTOM_LOGGER'] 13 | require 'logger' 14 | Knapsack.logger = Logger.new(STDOUT) 15 | Knapsack.logger.level = Logger::INFO 16 | end 17 | 18 | Knapsack::Adapters::RSpecAdapter.bind 19 | 20 | RSpec.configure do |config| 21 | config.order = :random 22 | config.mock_with :rspec do |mocks| 23 | mocks.syntax = :expect 24 | mocks.verify_partial_doubles = true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec_examples/support/shared_examples/common_example.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'common exmaple' do 2 | it { sleep 0.1 } 3 | end 4 | -------------------------------------------------------------------------------- /spinach_examples/scenario1.feature: -------------------------------------------------------------------------------- 1 | Feature: Test how spinach works for first test 2 | Scenario: Format greeting 3 | Given I have an empty array 4 | And I append my first name and my last name to it 5 | When I pass it to my super-duper method 6 | Then the output should contain a formal greeting 7 | -------------------------------------------------------------------------------- /spinach_examples/scenario2.feature: -------------------------------------------------------------------------------- 1 | Feature: Test how spinach works for second test 2 | Scenario: Informal greeting 3 | Given I have an empty array 4 | And I append only my first name to it 5 | When I pass it to my super-duper method 6 | Then the output should contain a casual greeting 7 | -------------------------------------------------------------------------------- /spinach_examples/steps/test_how_spinach_works_for_first_test.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::TestHowSpinachWorksForFirstTest < Spinach::FeatureSteps 2 | step 'I have an empty array' do 3 | end 4 | 5 | step 'I append my first name and my last name to it' do 6 | end 7 | 8 | step 'I pass it to my super-duper method' do 9 | end 10 | 11 | step 'the output should contain a formal greeting' do 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spinach_examples/steps/test_how_spinach_works_for_second_test.rb: -------------------------------------------------------------------------------- 1 | class Spinach::Features::TestHowSpinachWorksForSecondTest < Spinach::FeatureSteps 2 | step 'I have an empty array' do 3 | end 4 | 5 | step 'I append only my first name to it' do 6 | end 7 | 8 | step 'I pass it to my super-duper method' do 9 | end 10 | 11 | step 'the output should contain a casual greeting' do 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spinach_examples/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'knapsack' 3 | 4 | Knapsack::Adapters::SpinachAdapter.bind 5 | -------------------------------------------------------------------------------- /test_examples/fast/shared_examples_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Minitest::SharedExamples < Module 4 | include Minitest::Spec::DSL 5 | end 6 | 7 | SharedExampleSpec = Minitest::SharedExamples.new do 8 | def setup 9 | sleep 0.1 10 | end 11 | 12 | def test_mal 13 | sleep 0.1 14 | assert_equal 4, 2 * 2 15 | end 16 | 17 | def test_no_way 18 | sleep 0.2 19 | refute_match(/^no/i, 'yes') 20 | end 21 | 22 | def test_that_will_be_skipped 23 | skip 'test this later' 24 | end 25 | end 26 | 27 | describe "test that use SharedExamples" do 28 | include SharedExampleSpec 29 | end 30 | -------------------------------------------------------------------------------- /test_examples/fast/spec_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FakeCalculator 4 | def add(x, y) 5 | x + y 6 | end 7 | 8 | def mal(x, y) 9 | x * y 10 | end 11 | end 12 | 13 | describe FakeCalculator do 14 | before do 15 | @calc = FakeCalculator.new 16 | end 17 | 18 | it '#add' do 19 | result = @calc.add(2, 3) 20 | 21 | if self.respond_to?(:_) 22 | _(result).must_equal 5 23 | else 24 | result.must_equal 5 25 | end 26 | end 27 | 28 | it '#mal' do 29 | result = @calc.mal(2, 3) 30 | 31 | if self.respond_to?(:_) 32 | _(result).must_equal 6 33 | else 34 | result.must_equal 6 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test_examples/fast/unit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UnitTest < Minitest::Test 4 | def setup 5 | sleep 0.1 6 | end 7 | 8 | def test_mal 9 | sleep 0.1 10 | assert_equal 4, 2 * 2 11 | end 12 | 13 | def test_no_way 14 | sleep 0.2 15 | refute_match(/^no/i, 'yes') 16 | end 17 | 18 | def test_that_will_be_skipped 19 | sleep 1 20 | skip 'test this later' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test_examples/slow/slow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SlowTest < Minitest::Test 4 | def setup 5 | sleep 0.5 6 | end 7 | 8 | def test_a 9 | sleep 0.5 10 | end 11 | 12 | def test_b 13 | sleep 1.0 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test_examples/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | require 'knapsack' 4 | 5 | Knapsack.tracker.config({ 6 | enable_time_offset_warning: true, 7 | time_offset_in_seconds: 3 8 | }) 9 | Knapsack.report.config({ 10 | report_path: 'knapsack_minitest_report.json' 11 | }) 12 | 13 | if ENV['CUSTOM_LOGGER'] 14 | require 'logger' 15 | Knapsack.logger = Logger.new(STDOUT) 16 | Knapsack.logger.level = Logger::INFO 17 | end 18 | 19 | knapsack_adapter = Knapsack::Adapters::MinitestAdapter.bind 20 | knapsack_adapter.set_test_helper_path(__FILE__) 21 | --------------------------------------------------------------------------------