├── .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 |
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 |
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 | 
67 |
68 | ### With Knapsack - better test suite split
69 |
70 | 
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 |
--------------------------------------------------------------------------------