knapsack_pro 1.20.0 → 1.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +42 -17
  4. data/lib/knapsack_pro.rb +6 -0
  5. data/lib/knapsack_pro/adapters/base_adapter.rb +16 -0
  6. data/lib/knapsack_pro/adapters/rspec_adapter.rb +11 -9
  7. data/lib/knapsack_pro/allocator.rb +7 -5
  8. data/lib/knapsack_pro/allocator_builder.rb +2 -1
  9. data/lib/knapsack_pro/base_allocator_builder.rb +41 -10
  10. data/lib/knapsack_pro/build_distribution_fetcher.rb +57 -0
  11. data/lib/knapsack_pro/client/api/v1/build_distributions.rb +13 -0
  12. data/lib/knapsack_pro/client/connection.rb +33 -15
  13. data/lib/knapsack_pro/config/env.rb +4 -0
  14. data/lib/knapsack_pro/formatters/rspec_json_formatter.rb +1 -1
  15. data/lib/knapsack_pro/queue_allocator.rb +7 -5
  16. data/lib/knapsack_pro/queue_allocator_builder.rb +2 -1
  17. data/lib/knapsack_pro/report.rb +1 -1
  18. data/lib/knapsack_pro/runners/queue/cucumber_runner.rb +10 -1
  19. data/lib/knapsack_pro/runners/queue/rspec_runner.rb +7 -0
  20. data/lib/knapsack_pro/runners/rspec_runner.rb +5 -2
  21. data/lib/knapsack_pro/slow_test_file_determiner.rb +33 -0
  22. data/lib/knapsack_pro/slow_test_file_finder.rb +27 -0
  23. data/lib/knapsack_pro/test_case_detectors/rspec_test_example_detector.rb +26 -7
  24. data/lib/knapsack_pro/test_case_mergers/base_merger.rb +29 -0
  25. data/lib/knapsack_pro/test_case_mergers/rspec_merger.rb +34 -0
  26. data/lib/knapsack_pro/test_file_finder.rb +43 -5
  27. data/lib/knapsack_pro/test_files_with_test_cases_composer.rb +22 -0
  28. data/lib/knapsack_pro/version.rb +1 -1
  29. data/spec/knapsack_pro/adapters/base_adapter_spec.rb +55 -0
  30. data/spec/knapsack_pro/adapters/rspec_adapter_spec.rb +61 -25
  31. data/spec/knapsack_pro/allocator_builder_spec.rb +7 -3
  32. data/spec/knapsack_pro/allocator_spec.rb +7 -5
  33. data/spec/knapsack_pro/base_allocator_builder_spec.rb +79 -27
  34. data/spec/knapsack_pro/build_distribution_fetcher_spec.rb +89 -0
  35. data/spec/knapsack_pro/client/api/v1/build_distributions_spec.rb +31 -0
  36. data/spec/knapsack_pro/client/connection_spec.rb +202 -104
  37. data/spec/knapsack_pro/config/env_spec.rb +14 -0
  38. data/spec/knapsack_pro/queue_allocator_builder_spec.rb +7 -3
  39. data/spec/knapsack_pro/queue_allocator_spec.rb +7 -5
  40. data/spec/knapsack_pro/report_spec.rb +1 -1
  41. data/spec/knapsack_pro/runners/queue/cucumber_runner_spec.rb +38 -25
  42. data/spec/knapsack_pro/runners/rspec_runner_spec.rb +4 -4
  43. data/spec/knapsack_pro/slow_test_file_determiner_spec.rb +74 -0
  44. data/spec/knapsack_pro/slow_test_file_finder_spec.rb +43 -0
  45. data/spec/knapsack_pro/test_case_detectors/rspec_test_example_detector_spec.rb +83 -37
  46. data/spec/knapsack_pro/test_case_mergers/base_merger_spec.rb +27 -0
  47. data/spec/knapsack_pro/test_case_mergers/rspec_merger_spec.rb +59 -0
  48. data/spec/knapsack_pro/test_file_finder_spec.rb +105 -29
  49. data/spec/knapsack_pro/test_files_with_test_cases_composer_spec.rb +41 -0
  50. metadata +20 -2
@@ -38,4 +38,35 @@ describe KnapsackPro::Client::API::V1::BuildDistributions do
38
38
  expect(subject).to eq action
39
39
  end
40
40
  end
41
+
42
+ describe '.last' do
43
+ let(:commit_hash) { double }
44
+ let(:branch) { double }
45
+ let(:node_total) { double }
46
+ let(:node_index) { double }
47
+
48
+ subject do
49
+ described_class.last(
50
+ commit_hash: commit_hash,
51
+ branch: branch,
52
+ node_total: node_total,
53
+ node_index: node_index,
54
+ )
55
+ end
56
+
57
+ it do
58
+ action = double
59
+ expect(KnapsackPro::Client::API::Action).to receive(:new).with({
60
+ endpoint_path: '/v1/build_distributions/last',
61
+ http_method: :get,
62
+ request_hash: {
63
+ commit_hash: commit_hash,
64
+ branch: branch,
65
+ node_total: node_total,
66
+ node_index: node_index,
67
+ }
68
+ }).and_return(action)
69
+ expect(subject).to eq action
70
+ end
71
+ end
41
72
  end
@@ -1,167 +1,265 @@
1
+ shared_examples 'when request got response from API' do
2
+ context 'when body response is JSON and API response code is 400' do
3
+ let(:body) { '{"errors": "value"}' }
4
+ let(:code) { '400' } # it must be string code
5
+
6
+ before do
7
+ expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
8
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
9
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
10
+ expect(logger).to receive(:debug).with('API response:')
11
+ end
12
+
13
+ it do
14
+ parsed_response = { 'errors' => 'value' }
15
+
16
+ expect(logger).to receive(:error).with(parsed_response)
17
+
18
+ expect(subject).to eq(parsed_response)
19
+ expect(connection.success?).to be true
20
+ expect(connection.errors?).to be true
21
+ end
22
+ end
23
+
24
+ context 'when body response is JSON with build_distribution_id' do
25
+ let(:body) { '{"build_distribution_id": "seed-uuid"}' }
26
+ let(:code) { '200' } # it must be string code
27
+
28
+ before do
29
+ expect(KnapsackPro).to receive(:logger).exactly(5).and_return(logger)
30
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
31
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
32
+ expect(logger).to receive(:debug).with("Test suite split seed: seed-uuid")
33
+ expect(logger).to receive(:debug).with('API response:')
34
+ end
35
+
36
+ it do
37
+ parsed_response = { 'build_distribution_id' => 'seed-uuid' }
38
+
39
+ expect(logger).to receive(:debug).with(parsed_response)
40
+
41
+ expect(subject).to eq(parsed_response)
42
+ expect(connection.success?).to be true
43
+ expect(connection.errors?).to be false
44
+ end
45
+ end
46
+
47
+ context 'when body response is empty' do
48
+ let(:body) { '' }
49
+ let(:code) { '200' } # it must be string code
50
+
51
+ before do
52
+ expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
53
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
54
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
55
+ expect(logger).to receive(:debug).with('API response:')
56
+ end
57
+
58
+ it do
59
+ expect(logger).to receive(:debug).with('')
60
+
61
+ expect(subject).to eq('')
62
+ expect(connection.success?).to be true
63
+ expect(connection.errors?).to be false
64
+ end
65
+ end
66
+ end
67
+
68
+ shared_examples 'when retry request' do
69
+ context 'when body response is JSON and API response code is 500' do
70
+ let(:body) { '{"error": "Internal Server Error"}' }
71
+ let(:code) { '500' } # it must be string code
72
+
73
+ before do
74
+ expect(KnapsackPro).to receive(:logger).at_least(1).and_return(logger)
75
+ end
76
+
77
+ it do
78
+ expect(logger).to receive(:debug).exactly(3).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
79
+ expect(logger).to receive(:debug).exactly(3).with('API request UUID: fake-uuid')
80
+ expect(logger).to receive(:debug).exactly(3).with('API response:')
81
+
82
+ parsed_response = { 'error' => 'Internal Server Error' }
83
+
84
+ expect(logger).to receive(:error).exactly(3).with(parsed_response)
85
+
86
+ server_error = described_class::ServerError.new(parsed_response)
87
+ expect(logger).to receive(:warn).exactly(3).with(server_error.inspect)
88
+
89
+ expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
90
+ expect(logger).to receive(:warn).with("Wait 16s and retry request to Knapsack Pro API.")
91
+ expect(Kernel).to receive(:sleep).with(8)
92
+ expect(Kernel).to receive(:sleep).with(16)
93
+
94
+ expect(subject).to eq(parsed_response)
95
+
96
+ expect(connection.success?).to be false
97
+ expect(connection.errors?).to be true
98
+ end
99
+
100
+ context 'when Fallback Mode is disabled' do
101
+ before do
102
+ expect(KnapsackPro::Config::Env).to receive(:fallback_mode_enabled?).at_least(1).and_return(false)
103
+ end
104
+
105
+ it do
106
+ expect(logger).to receive(:debug).exactly(6).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
107
+ expect(logger).to receive(:debug).exactly(6).with('API request UUID: fake-uuid')
108
+ expect(logger).to receive(:debug).exactly(6).with('API response:')
109
+
110
+ parsed_response = { 'error' => 'Internal Server Error' }
111
+
112
+ expect(logger).to receive(:error).exactly(6).with(parsed_response)
113
+
114
+ server_error = described_class::ServerError.new(parsed_response)
115
+ expect(logger).to receive(:warn).exactly(6).with(server_error.inspect)
116
+
117
+ expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
118
+ expect(logger).to receive(:warn).with("Wait 16s and retry request to Knapsack Pro API.")
119
+ expect(logger).to receive(:warn).with("Wait 24s and retry request to Knapsack Pro API.")
120
+ expect(logger).to receive(:warn).with("Wait 32s and retry request to Knapsack Pro API.")
121
+ expect(logger).to receive(:warn).with("Wait 40s and retry request to Knapsack Pro API.")
122
+ expect(Kernel).to receive(:sleep).with(8)
123
+ expect(Kernel).to receive(:sleep).with(16)
124
+ expect(Kernel).to receive(:sleep).with(24)
125
+ expect(Kernel).to receive(:sleep).with(32)
126
+ expect(Kernel).to receive(:sleep).with(40)
127
+
128
+ expect(subject).to eq(parsed_response)
129
+
130
+ expect(connection.success?).to be false
131
+ expect(connection.errors?).to be true
132
+ end
133
+ end
134
+ end
135
+ end
136
+
1
137
  describe KnapsackPro::Client::Connection do
2
138
  let(:endpoint_path) { '/v1/fake_endpoint' }
3
- let(:http_method) { :post }
4
139
  let(:request_hash) { { fake: 'hash' } }
140
+ let(:http_method) { :post }
5
141
  let(:action) do
6
142
  instance_double(KnapsackPro::Client::API::Action,
7
143
  endpoint_path: endpoint_path,
8
144
  http_method: http_method,
9
145
  request_hash: request_hash)
10
146
  end
147
+ let(:test_suite_token) { '3fa64859337f6e56409d49f865d13fd7' }
11
148
 
12
149
  let(:connection) { described_class.new(action) }
13
150
 
14
151
  before do
15
152
  stub_const('ENV', {
16
153
  'KNAPSACK_PRO_ENDPOINT' => 'http://api.knapsackpro.test:3000',
17
- 'KNAPSACK_PRO_TEST_SUITE_TOKEN' => '3fa64859337f6e56409d49f865d13fd7',
154
+ 'KNAPSACK_PRO_TEST_SUITE_TOKEN' => test_suite_token,
18
155
  })
19
156
  end
20
157
 
21
158
  describe '#call' do
22
159
  let(:logger) { instance_double(Logger) }
160
+ let(:http) { instance_double(Net::HTTP) }
161
+ let(:http_response) do
162
+ header = { 'X-Request-Id' => 'fake-uuid' }
163
+ instance_double(Net::HTTPOK, body: body, header: header, code: code)
164
+ end
23
165
 
24
166
  subject { connection.call }
25
167
 
26
- context 'when http method is POST' do
27
- before do
28
- http = instance_double(Net::HTTP)
168
+ before do
169
+ expect(Net::HTTP).to receive(:new).with('api.knapsackpro.test', 3000).and_return(http)
29
170
 
30
- expect(Net::HTTP).to receive(:new).with('api.knapsackpro.test', 3000).and_return(http)
171
+ expect(http).to receive(:use_ssl=).with(false)
172
+ expect(http).to receive(:open_timeout=).with(15)
173
+ expect(http).to receive(:read_timeout=).with(15)
174
+ end
31
175
 
32
- expect(http).to receive(:use_ssl=).with(false)
33
- expect(http).to receive(:open_timeout=).with(15)
34
- expect(http).to receive(:read_timeout=).with(15)
176
+ context 'when http method is POST' do
177
+ let(:http_method) { :post }
35
178
 
36
- header = { 'X-Request-Id' => 'fake-uuid' }
37
- http_response = instance_double(Net::HTTPOK, body: body, header: header, code: code)
179
+ before do
38
180
  expect(http).to receive(:post).with(
39
181
  endpoint_path,
40
- "{\"fake\":\"hash\",\"test_suite_token\":\"3fa64859337f6e56409d49f865d13fd7\"}",
182
+ request_hash.to_json,
41
183
  {
42
184
  'Content-Type' => 'application/json',
43
185
  'Accept' => 'application/json',
44
186
  'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
45
187
  'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
188
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
46
189
  }
47
190
  ).and_return(http_response)
48
191
  end
49
192
 
50
- context 'when body response is json and API response code is 400' do
51
- let(:body) { '{"errors": "value"}' }
52
- let(:code) { '400' } # it must be string code
53
-
54
- before do
55
- expect(KnapsackPro).to receive(:logger).exactly(3).and_return(logger)
56
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
57
- expect(logger).to receive(:debug).with('API response:')
58
- end
59
-
60
- it do
61
- parsed_response = { 'errors' => 'value' }
62
-
63
- expect(logger).to receive(:error).with(parsed_response)
64
-
65
- expect(subject).to eq(parsed_response)
66
- expect(connection.success?).to be true
67
- expect(connection.errors?).to be true
68
- end
193
+ it_behaves_like 'when request got response from API' do
194
+ let(:expected_http_method) { 'POST' }
69
195
  end
196
+ end
70
197
 
71
- context 'when body response is json with build_distribution_id' do
72
- let(:body) { '{"build_distribution_id": "seed-uuid"}' }
73
- let(:code) { '200' } # it must be string code
74
-
75
- before do
76
- expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
77
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
78
- expect(logger).to receive(:debug).with("Test suite split seed: seed-uuid")
79
- expect(logger).to receive(:debug).with('API response:')
80
- end
81
-
82
- it do
83
- parsed_response = { 'build_distribution_id' => 'seed-uuid' }
84
-
85
- expect(logger).to receive(:debug).with(parsed_response)
198
+ context 'when http method is GET' do
199
+ let(:http_method) { :get }
86
200
 
87
- expect(subject).to eq(parsed_response)
88
- expect(connection.success?).to be true
89
- expect(connection.errors?).to be false
90
- end
201
+ before do
202
+ uri = URI.parse("http://api.knapsackpro.test:3000#{endpoint_path}")
203
+ uri.query = URI.encode_www_form(request_hash)
204
+ expect(http).to receive(:get).with(
205
+ uri,
206
+ {
207
+ 'Content-Type' => 'application/json',
208
+ 'Accept' => 'application/json',
209
+ 'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
210
+ 'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
211
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
212
+ }
213
+ ).and_return(http_response)
91
214
  end
92
215
 
93
- context 'when body response is empty' do
94
- let(:body) { '' }
95
- let(:code) { '200' } # it must be string code
96
-
97
- before do
98
- expect(KnapsackPro).to receive(:logger).exactly(3).and_return(logger)
99
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
100
- expect(logger).to receive(:debug).with('API response:')
101
- end
102
-
103
- it do
104
- expect(logger).to receive(:debug).with('')
105
-
106
- expect(subject).to eq('')
107
- expect(connection.success?).to be true
108
- expect(connection.errors?).to be false
109
- end
216
+ it_behaves_like 'when request got response from API' do
217
+ let(:expected_http_method) { 'GET' }
110
218
  end
111
219
  end
112
220
 
113
221
  context 'when retry request for http method POST' do
114
- before do
115
- http = instance_double(Net::HTTP)
222
+ let(:http_method) { :post }
116
223
 
117
- expect(Net::HTTP).to receive(:new).exactly(3).with('api.knapsackpro.test', 3000).and_return(http)
118
-
119
- expect(http).to receive(:use_ssl=).exactly(3).with(false)
120
- expect(http).to receive(:open_timeout=).exactly(3).with(15)
121
- expect(http).to receive(:read_timeout=).exactly(3).with(15)
122
-
123
- header = { 'X-Request-Id' => 'fake-uuid' }
124
- http_response = instance_double(Net::HTTPOK, body: body, header: header, code: code)
125
- expect(http).to receive(:post).exactly(3).with(
224
+ before do
225
+ expect(http).to receive(:post).at_least(3).with(
126
226
  endpoint_path,
127
- "{\"fake\":\"hash\",\"test_suite_token\":\"3fa64859337f6e56409d49f865d13fd7\"}",
227
+ request_hash.to_json,
128
228
  {
129
229
  'Content-Type' => 'application/json',
130
230
  'Accept' => 'application/json',
131
231
  'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
132
232
  'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
233
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
133
234
  }
134
235
  ).and_return(http_response)
135
236
  end
136
237
 
137
- context 'when body response is json and API response code is 500' do
138
- let(:body) { '{"error": "Internal Server Error"}' }
139
- let(:code) { '500' } # it must be string code
140
-
141
- before do
142
- expect(KnapsackPro).to receive(:logger).at_least(1).and_return(logger)
143
- expect(logger).to receive(:debug).exactly(3).with('API request UUID: fake-uuid')
144
- expect(logger).to receive(:debug).exactly(3).with('API response:')
145
- end
146
-
147
- it do
148
- parsed_response = { 'error' => 'Internal Server Error' }
149
-
150
- expect(logger).to receive(:error).exactly(3).with(parsed_response)
151
-
152
- server_error = described_class::ServerError.new(parsed_response)
153
- expect(logger).to receive(:warn).exactly(3).with(server_error.inspect)
238
+ it_behaves_like 'when retry request' do
239
+ let(:expected_http_method) { 'POST' }
240
+ end
241
+ end
154
242
 
155
- expect(logger).to receive(:warn).with("Wait 4s and retry request to Knapsack Pro API.")
156
- expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
157
- expect(Kernel).to receive(:sleep).with(4)
158
- expect(Kernel).to receive(:sleep).with(8)
243
+ context 'when retry request for http method GET' do
244
+ let(:http_method) { :get }
159
245
 
160
- expect(subject).to eq(parsed_response)
246
+ before do
247
+ uri = URI.parse("http://api.knapsackpro.test:3000#{endpoint_path}")
248
+ uri.query = URI.encode_www_form(request_hash)
249
+ expect(http).to receive(:get).at_least(3).with(
250
+ uri,
251
+ {
252
+ 'Content-Type' => 'application/json',
253
+ 'Accept' => 'application/json',
254
+ 'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
255
+ 'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
256
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
257
+ }
258
+ ).and_return(http_response)
259
+ end
161
260
 
162
- expect(connection.success?).to be false
163
- expect(connection.errors?).to be true
164
- end
261
+ it_behaves_like 'when retry request' do
262
+ let(:expected_http_method) { 'GET' }
165
263
  end
166
264
  end
167
265
  end
@@ -183,6 +183,20 @@ describe KnapsackPro::Config::Env do
183
183
  end
184
184
  end
185
185
 
186
+ describe '.slow_test_file_pattern' do
187
+ subject { described_class.slow_test_file_pattern }
188
+
189
+ context 'when ENV exists' do
190
+ let(:slow_test_file_pattern) { 'spec/features/*_spec.rb' }
191
+ before { stub_const("ENV", { 'KNAPSACK_PRO_SLOW_TEST_FILE_PATTERN' => slow_test_file_pattern }) }
192
+ it { should eq slow_test_file_pattern }
193
+ end
194
+
195
+ context "when ENV doesn't exist" do
196
+ it { should be_nil }
197
+ end
198
+ end
199
+
186
200
  describe '.test_file_exclude_pattern' do
187
201
  subject { described_class.test_file_exclude_pattern }
188
202
 
@@ -8,8 +8,11 @@ describe KnapsackPro::QueueAllocatorBuilder do
8
8
  subject { allocator_builder.allocator }
9
9
 
10
10
  before do
11
- test_files = double
12
- expect(allocator_builder).to receive(:test_files).and_return(test_files)
11
+ fast_and_slow_test_files_to_run = double
12
+ expect(allocator_builder).to receive(:fast_and_slow_test_files_to_run).and_return(fast_and_slow_test_files_to_run)
13
+
14
+ fallback_mode_test_files = double
15
+ expect(allocator_builder).to receive(:fallback_mode_test_files).and_return(fallback_mode_test_files)
13
16
 
14
17
  repository_adapter = double
15
18
  expect(KnapsackPro::RepositoryAdapterInitiator).to receive(:call).and_return(repository_adapter)
@@ -22,7 +25,8 @@ describe KnapsackPro::QueueAllocatorBuilder do
22
25
  expect(KnapsackPro::Config::Env).to receive(:ci_node_build_id).and_return(ci_node_build_id)
23
26
 
24
27
  expect(KnapsackPro::QueueAllocator).to receive(:new).with(
25
- test_files: test_files,
28
+ fast_and_slow_test_files_to_run: fast_and_slow_test_files_to_run,
29
+ fallback_mode_test_files: fallback_mode_test_files,
26
30
  ci_node_total: ci_node_total,
27
31
  ci_node_index: ci_node_index,
28
32
  ci_node_build_id: ci_node_build_id,