knapsack_pro 10.0.0 → 11.0.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/knapsack_pro/adapters/base_adapter.rb +6 -6
  3. data/lib/knapsack_pro/adapters/minitest_adapter.rb +3 -3
  4. data/lib/knapsack_pro/adapters/rspec_adapter.rb +3 -3
  5. data/lib/knapsack_pro/client/api/v1/build_distributions.rb +0 -29
  6. data/lib/knapsack_pro/client/api/v2/queues.rb +2 -4
  7. data/lib/knapsack_pro/client/connection.rb +25 -12
  8. data/lib/knapsack_pro/commands.rb +8 -8
  9. data/lib/knapsack_pro/config/ci/app_veyor.rb +0 -8
  10. data/lib/knapsack_pro/config/ci/base.rb +0 -7
  11. data/lib/knapsack_pro/config/ci/buildkite.rb +1 -9
  12. data/lib/knapsack_pro/config/ci/circle.rb +0 -8
  13. data/lib/knapsack_pro/config/ci/cirrus_ci.rb +0 -8
  14. data/lib/knapsack_pro/config/ci/codefresh.rb +0 -8
  15. data/lib/knapsack_pro/config/ci/codeship.rb +0 -8
  16. data/lib/knapsack_pro/config/ci/github_actions.rb +2 -10
  17. data/lib/knapsack_pro/config/ci/gitlab_ci.rb +0 -9
  18. data/lib/knapsack_pro/config/ci/heroku.rb +1 -9
  19. data/lib/knapsack_pro/config/ci/semaphore.rb +0 -8
  20. data/lib/knapsack_pro/config/ci/semaphore2.rb +0 -8
  21. data/lib/knapsack_pro/config/ci/travis.rb +0 -8
  22. data/lib/knapsack_pro/config/env.rb +12 -47
  23. data/lib/knapsack_pro/crypto/branch_encryptor.rb +4 -5
  24. data/lib/knapsack_pro/extensions/rspec_extension.rb +1 -1
  25. data/lib/knapsack_pro/formatters/time_tracker.rb +3 -1
  26. data/lib/knapsack_pro/queue_allocator.rb +40 -93
  27. data/lib/knapsack_pro/regular_allocator.rb +41 -20
  28. data/lib/knapsack_pro/report.rb +37 -8
  29. data/lib/knapsack_pro/repository_adapters/git_adapter.rb +10 -2
  30. data/lib/knapsack_pro/runners/cucumber_runner.rb +2 -1
  31. data/lib/knapsack_pro/runners/minitest_runner.rb +2 -1
  32. data/lib/knapsack_pro/runners/queue/base_runner.rb +17 -8
  33. data/lib/knapsack_pro/runners/queue/cucumber_runner.rb +1 -1
  34. data/lib/knapsack_pro/runners/queue/minitest_runner.rb +1 -1
  35. data/lib/knapsack_pro/runners/queue/rspec_runner.rb +3 -4
  36. data/lib/knapsack_pro/runners/rspec_runner.rb +2 -1
  37. data/lib/knapsack_pro/runners/spinach_runner.rb +2 -1
  38. data/lib/knapsack_pro/runners/test_unit_runner.rb +2 -1
  39. data/lib/knapsack_pro/tracker.rb +2 -4
  40. data/lib/knapsack_pro/urls.rb +4 -10
  41. data/lib/knapsack_pro/version.rb +1 -1
  42. data/lib/tasks/queue/rspec.rake +5 -1
  43. metadata +2 -3
  44. data/lib/knapsack_pro/client/api/v1/queues.rb +0 -44
@@ -20,12 +20,6 @@ module KnapsackPro
20
20
  @ci_node_index ||= (env_for('KNAPSACK_PRO_CI_NODE_INDEX', :node_index) || 0).to_i
21
21
  end
22
22
 
23
- def ci_node_build_id
24
- env_name = 'KNAPSACK_PRO_CI_NODE_BUILD_ID'
25
- env_for(env_name, :node_build_id) ||
26
- raise("Missing environment variable #{env_name}. Read more at #{KnapsackPro::Urls::KNAPSACK_PRO_CI_NODE_BUILD_ID}")
27
- end
28
-
29
23
  def ci_node_retry_count
30
24
  (env_for('KNAPSACK_PRO_CI_NODE_RETRY_COUNT', :node_retry_count) || 0).to_i
31
25
  end
@@ -95,12 +89,12 @@ module KnapsackPro
95
89
  ENV['KNAPSACK_PRO_REPOSITORY_ADAPTER']
96
90
  end
97
91
 
98
- def regular_mode?
99
- ENV['KNAPSACK_PRO_REGULAR_MODE_ENABLED'] == 'true'
92
+ def single_batch_mode?
93
+ ENV['KNAPSACK_PRO_SINGLE_BATCH_MODE_ENABLED'] == 'true'
100
94
  end
101
95
 
102
- def queue_mode?
103
- ENV['KNAPSACK_PRO_QUEUE_MODE_ENABLED'] == 'true'
96
+ def normal_mode?
97
+ ENV['KNAPSACK_PRO_NORMAL_MODE_ENABLED'] == 'true'
104
98
  end
105
99
 
106
100
  def queue_id
@@ -143,35 +137,8 @@ module KnapsackPro
143
137
  ENV.fetch('KNAPSACK_PRO_ENDPOINT', 'https://api.knapsackpro.com')
144
138
  end
145
139
 
146
- def fixed_test_suite_split
147
- ENV.fetch('KNAPSACK_PRO_FIXED_TEST_SUITE_SPLIT', true)
148
- end
149
-
150
- def fixed_test_suite_split?
151
- fixed_test_suite_split.to_s == 'true'
152
- end
153
-
154
- def fixed_queue_split
155
- @fixed_queue_split ||= begin
156
- env_name = 'KNAPSACK_PRO_FIXED_QUEUE_SPLIT'
157
- computed = env_for(env_name, :fixed_queue_split).to_s
158
-
159
- if !ENV.key?(env_name)
160
- KnapsackPro.logger.info("#{env_name} is not set. Using default value: #{computed}. Learn more at #{KnapsackPro::Urls::FIXED_QUEUE_SPLIT}")
161
- end
162
-
163
- computed
164
- end
165
- end
166
-
167
- def fixed_queue_split_?
168
- return @fixed_queue_split_ if defined?(@fixed_queue_split_)
169
-
170
- @fixed_queue_split_ = env_for('KNAPSACK_PRO_FIXED_QUEUE_SPLIT', :fixed_queue_split).to_s == "true"
171
- end
172
-
173
- def fixed_queue_split?
174
- fixed_queue_split.to_s == 'true'
140
+ def single_batch?
141
+ ENV.fetch('KNAPSACK_PRO_SINGLE_BATCH', 'false') == 'true'
175
142
  end
176
143
 
177
144
  def cucumber_queue_prefix
@@ -269,8 +236,11 @@ module KnapsackPro
269
236
  def test_queue_id
270
237
  knapsack_env_name = 'KNAPSACK_PRO_TEST_QUEUE_ID'
271
238
  knapsack_env_value = ENV[knapsack_env_name]
239
+ if knapsack_env_value == '' || knapsack_env_value&.match?(/[^a-z0-9\-_]+/i)
240
+ raise "Invalid test_queue_id `#{knapsack_env_value}`: Only ASCII letters, numbers, hyphens (-), and underscores (_) are allowed."
241
+ end
272
242
 
273
- ci_env_value = detected_ci.new.test_queue_id
243
+ ci_env_value = detected_ci.new.test_queue_id&.gsub(/[^a-z0-9\-_]+/i, "-")
274
244
 
275
245
  if !knapsack_env_value.nil? && !ci_env_value.nil? && knapsack_env_value != ci_env_value.to_s
276
246
  warn("You have set the environment variable #{knapsack_env_name} to #{knapsack_env_value} which could be automatically determined from the CI environment as #{ci_env_value}.")
@@ -279,17 +249,12 @@ module KnapsackPro
279
249
  id = knapsack_env_value != nil ? knapsack_env_value : ci_env_value
280
250
  return id unless id.nil?
281
251
 
282
- triplet = [ci_node_total, branch, commit_hash]
283
- return triplet.join('-') if triplet.all?
252
+ triplet = [ci_node_total, KnapsackPro::Crypto::BranchEncryptor.call(branch), commit_hash]
253
+ return triplet.join('_').gsub(/[^a-z0-9\-_]+/i, "-") if triplet.all?
284
254
 
285
255
  raise("Missing test_queue_id. See: #{KnapsackPro::Urls::KNAPSACK_PRO_TEST_QUEUE_ID}")
286
256
  end
287
257
 
288
- def node_uuid
289
- env_name = 'KNAPSACK_PRO_NODE_UUID'
290
- ENV[env_name] || raise("Missing environment variable #{env_name}. Please report this as a bug: #{KnapsackPro::Urls::SUPPORT}")
291
- end
292
-
293
258
  private
294
259
 
295
260
  def required_env(env_name)
@@ -22,11 +22,10 @@ module KnapsackPro
22
22
  ]
23
23
 
24
24
  def self.call(branch)
25
- if KnapsackPro::Config::Env.branch_encrypted?
26
- new(branch).call
27
- else
28
- branch
29
- end
25
+ return branch if branch.nil?
26
+ return branch unless KnapsackPro::Config::Env.branch_encrypted?
27
+
28
+ new(branch).call
30
29
  end
31
30
 
32
31
  def initialize(branch)
@@ -77,7 +77,7 @@ module KnapsackPro
77
77
  # Based on:
78
78
  # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L113
79
79
  #
80
- # Ignore `configuration.fail_if_no_examples` in Queue Mode:
80
+ # Ignore `configuration.fail_if_no_examples`:
81
81
  # * a late CI node, started after all tests were executed by other nodes, is expected to receive an empty batch
82
82
  # * a batch could contain tests with no examples (e.g. commented out)
83
83
  #
@@ -35,10 +35,12 @@ module KnapsackPro
35
35
  paths =
36
36
  examples_by_file_path.flat_map do |file_path, examples|
37
37
  failed_id_paths = examples.filter { |example| example.execution_result.status.to_s == "failed" }.map(&:id)
38
-
38
+ next [] if failed_id_paths.none?
39
+ next file_path if KnapsackPro::Config::Env.test_files_encrypted?
39
40
  # Other nodes may have run some examples from this file, it's not safe to compact.
40
41
  next failed_id_paths if rspec_split_by_test_example?(file_path)
41
42
  next file_path if failed_id_paths.size == examples.size
43
+
42
44
  failed_id_paths
43
45
  end
44
46
 
@@ -14,7 +14,7 @@ module KnapsackPro
14
14
 
15
15
  def queue_exists?
16
16
  raise "Connection failed. Please report this as a bug: #{KnapsackPro::Urls::SUPPORT}" if connection_failed?
17
- return false if connection.api_code == KnapsackPro::Client::API::V1::Queues::CODE_ATTEMPT_CONNECT_TO_QUEUE_FAILED
17
+ return false if connection.api_code == KnapsackPro::Client::API::V2::Queues::CODE_ATTEMPT_CONNECT_TO_QUEUE_FAILED
18
18
 
19
19
  true
20
20
  end
@@ -24,11 +24,11 @@ module KnapsackPro
24
24
  end
25
25
 
26
26
  def test_files
27
- if response.key?('test_files')
28
- response.fetch('test_files')
29
- else
30
- response.fetch('paths').map { |path| { "path" => path, "time_execution" => nil } }
31
- end
27
+ paths.map { |path| { "path" => path, "time_execution" => nil } }
28
+ end
29
+
30
+ def paths
31
+ response.fetch('paths')
32
32
  end
33
33
 
34
34
  def id
@@ -47,30 +47,31 @@ module KnapsackPro
47
47
  @repository_adapter = args.fetch(:repository_adapter)
48
48
  @fallback_mode = false
49
49
  @batch_index = -1
50
- @batch_id = nil
50
+ @node_uuid = SecureRandom.uuid
51
+ @current_batch = nil
51
52
  end
52
53
 
53
- def test_file_paths(can_initialize_queue, executed_test_files, batch_uuid: SecureRandom.uuid, time_tracker: nil)
54
+ def test_file_paths(can_initialize_queue, executed_test_files, time_tracker: nil)
54
55
  @batch_index += 1
55
56
  return [] if @fallback_mode
56
57
 
57
- batch = pull_tests_from_queue(can_initialize_queue, batch_uuid, time_tracker: time_tracker)
58
+ batch = pull_tests_from_queue(can_initialize_queue, time_tracker: time_tracker)
58
59
 
59
60
  return switch_to_fallback_mode(executed_test_files: executed_test_files) if batch.connection_failed?
60
61
  return normalize_test_files(batch.test_files) if batch.queue_exists?
61
62
 
62
63
  test_files_result = test_suite.calculate_test_files
63
64
 
64
- return try_initializing_queue(test_files_result.test_files, batch_uuid, time_tracker: time_tracker) if test_files_result.quick?
65
+ return try_initializing_queue(test_files_result.test_files.map { |test| test.fetch("path") }, time_tracker: time_tracker) if test_files_result.quick?
65
66
 
66
67
  # The tests to run were found slowly. By that time, the queue could have already been initialized by another CI node.
67
68
  # Attempt to pull tests from the queue to avoid the attempt to initialize the queue unnecessarily (queue initialization is an expensive request with a big test files payload).
68
- batch = pull_tests_from_queue(can_initialize_queue, batch_uuid, time_tracker: time_tracker)
69
+ batch = pull_tests_from_queue(can_initialize_queue, time_tracker: time_tracker)
69
70
 
70
71
  return switch_to_fallback_mode(executed_test_files: executed_test_files) if batch.connection_failed?
71
72
  return normalize_test_files(batch.test_files) if batch.queue_exists?
72
73
 
73
- try_initializing_queue(test_files_result.test_files, batch_uuid, time_tracker: time_tracker)
74
+ try_initializing_queue(test_files_result.test_files.map { |test| test.fetch("path") }, time_tracker: time_tracker)
74
75
  end
75
76
 
76
77
  private
@@ -86,102 +87,51 @@ module KnapsackPro
86
87
  KnapsackPro::TestFilePresenter.paths(decrypted_test_files)
87
88
  end
88
89
 
89
- def build_action_v1(can_initialize_queue:, attempt_connect_to_queue:, batch_uuid:, test_files: nil)
90
- if can_initialize_queue && !attempt_connect_to_queue
91
- raise 'Test files are required when initializing a new queue.' if test_files.nil?
92
- test_files = KnapsackPro::Crypto::Encryptor.call(test_files)
93
- end
94
-
95
- KnapsackPro::Client::API::V1::Queues.queue(
96
- can_initialize_queue: can_initialize_queue,
97
- attempt_connect_to_queue: attempt_connect_to_queue,
98
- commit_hash: repository_adapter.commit_hash,
99
- branch: encrypted_branch,
100
- node_total: ci_node_total,
101
- node_index: ci_node_index,
102
- test_files: test_files,
103
- batch_uuid: batch_uuid
104
- )
105
- end
106
-
107
- def build_action_v2(can_initialize_queue:, attempt_connect_to_queue:, time_tracker:, paths: nil)
90
+ # time_tracker is nil for non-RSpec queue runners
91
+ def build_action(can_initialize_queue:, attempt_connect_to_queue:, time_tracker:, paths: nil)
108
92
  if can_initialize_queue && !attempt_connect_to_queue
109
93
  raise 'Test files are required when initializing a new queue.' if paths.nil?
110
94
  paths = KnapsackPro::Crypto::Encryptor.paths(paths)
111
95
  end
112
96
 
97
+ failed_paths =
98
+ if time_tracker
99
+ KnapsackPro::Crypto::Encryptor.paths(time_tracker.current_batch_failed_paths)
100
+ elsif @batch_index.positive?
101
+ KnapsackPro::Crypto::Encryptor.paths(@current_batch.paths)
102
+ end
103
+
113
104
  KnapsackPro::Client::API::V2::Queues.queue(
114
- can_initialize_queue: can_initialize_queue,
115
105
  attempt_connect_to_queue: attempt_connect_to_queue,
116
- commit_hash: repository_adapter.commit_hash,
106
+ batch_id: @current_batch&.id,
107
+ batch_index: @batch_index,
117
108
  branch: encrypted_branch,
118
- node_total: ci_node_total,
109
+ can_initialize_queue: can_initialize_queue,
110
+ commit_hash: repository_adapter.commit_hash,
111
+ failed_paths: failed_paths,
119
112
  node_index: ci_node_index,
120
- paths: paths,
121
- failed_paths: time_tracker.current_batch_failed_paths,
122
- batch_index: @batch_index,
123
- batch_id: @batch_id
113
+ node_total: ci_node_total,
114
+ node_uuid: @node_uuid,
115
+ paths: paths
124
116
  )
125
117
  end
126
118
 
127
- def pull_tests_from_queue(can_initialize_queue, batch_uuid, time_tracker: nil)
128
- if time_tracker.nil?
129
- pull_tests_from_queue_v1(can_initialize_queue, batch_uuid)
130
- else
131
- pull_tests_from_queue_v2(can_initialize_queue, time_tracker)
132
- end
133
- end
134
-
135
- def pull_tests_from_queue_v1(can_initialize_queue, batch_uuid)
136
- action = build_action_v1(can_initialize_queue: can_initialize_queue, attempt_connect_to_queue: can_initialize_queue, batch_uuid: batch_uuid)
137
- connection = KnapsackPro::Client::Connection.new(action)
138
- response = connection.call
139
- Batch.new(connection, response)
140
- end
141
-
142
- def pull_tests_from_queue_v2(can_initialize_queue, time_tracker)
143
- action = build_action_v2(can_initialize_queue: can_initialize_queue, attempt_connect_to_queue: can_initialize_queue, time_tracker: time_tracker)
144
- connection = KnapsackPro::Client::Connection.new(action)
145
- response = connection.call
146
- Batch.new(connection, response).tap do |batch|
147
- @batch_id = batch.id unless batch.connection_failed?
148
- end
149
- end
150
-
151
- def initialize_queue_v1(tests_to_run, batch_uuid)
152
- action = build_action_v1(can_initialize_queue: true, attempt_connect_to_queue: false, batch_uuid: batch_uuid, test_files: tests_to_run)
119
+ def pull_tests_from_queue(can_initialize_queue, time_tracker: nil)
120
+ action = build_action(can_initialize_queue: can_initialize_queue, attempt_connect_to_queue: can_initialize_queue, time_tracker: time_tracker)
153
121
  connection = KnapsackPro::Client::Connection.new(action)
154
122
  response = connection.call
155
- Batch.new(connection, response)
123
+ @current_batch = Batch.new(connection, response)
156
124
  end
157
125
 
158
- def initialize_queue_v2(paths, time_tracker)
159
- action = build_action_v2(can_initialize_queue: true, attempt_connect_to_queue: false, paths: paths, time_tracker: time_tracker)
126
+ def initialize_queue(paths, time_tracker: nil)
127
+ action = build_action(can_initialize_queue: true, attempt_connect_to_queue: false, paths: paths, time_tracker: time_tracker)
160
128
  connection = KnapsackPro::Client::Connection.new(action)
161
129
  response = connection.call
162
- Batch.new(connection, response).tap do |batch|
163
- @batch_id = batch.id unless batch.connection_failed?
164
- end
165
- end
166
-
167
- def try_initializing_queue(tests, batch_uuid, time_tracker: nil)
168
- if time_tracker.nil?
169
- try_initializing_queue_v1(tests, batch_uuid)
170
- else
171
- paths = tests.map { |test| test.fetch("path") }
172
- try_initializing_queue_v2(paths, time_tracker)
173
- end
130
+ @current_batch = Batch.new(connection, response)
174
131
  end
175
132
 
176
- def try_initializing_queue_v1(tests, batch_uuid)
177
- result = initialize_queue_v1(tests, batch_uuid)
178
- return switch_to_fallback_mode(executed_test_files: []) if result.connection_failed?
179
-
180
- normalize_test_files(result.test_files)
181
- end
182
-
183
- def try_initializing_queue_v2(paths, time_tracker)
184
- result = initialize_queue_v2(paths, time_tracker)
133
+ def try_initializing_queue(paths, time_tracker: nil)
134
+ result = initialize_queue(paths, time_tracker: time_tracker)
185
135
  return switch_to_fallback_mode(executed_test_files: []) if result.connection_failed?
186
136
 
187
137
  normalize_test_files(result.test_files)
@@ -191,14 +141,11 @@ module KnapsackPro
191
141
  @fallback_mode = true
192
142
 
193
143
  if !KnapsackPro::Config::Env.fallback_mode_enabled?
194
- message = "Fallback Mode was disabled with KNAPSACK_PRO_FALLBACK_MODE_ENABLED=false. Please restart this CI node to retry tests. Most likely Fallback Mode was disabled due to #{KnapsackPro::Urls::QUEUE_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_FALSE}"
144
+ message = "Fallback Mode was disabled with KNAPSACK_PRO_FALLBACK_MODE_ENABLED=false. Please restart this CI node to retry tests. Most likely Fallback Mode was disabled due to #{KnapsackPro::Urls::NORMAL_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_FALSE}"
195
145
  KnapsackPro.logger.error(message)
196
146
  raise FallbackModeError.new(message)
197
147
  elsif KnapsackPro::Config::Env.ci_node_retry_count > 0
198
- message = "knapsack_pro gem could not connect to Knapsack Pro API and the Fallback Mode cannot be used this time. Running tests in Fallback Mode are not allowed for retried parallel CI node to avoid running the wrong set of tests. Please manually retry this parallel job on your CI server then knapsack_pro gem will try to connect to Knapsack Pro API again and will run a correct set of tests for this CI node. Learn more #{KnapsackPro::Urls::QUEUE_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_TRUE_AND_POSITIVE_RETRY_COUNT}"
199
- unless KnapsackPro::Config::Env.fixed_queue_split?
200
- message += " Please ensure you have set KNAPSACK_PRO_FIXED_QUEUE_SPLIT=true to allow Knapsack Pro API remember the recorded CI node tests so when you retry failed tests on the CI node then the same set of tests will be executed. See more #{KnapsackPro::Urls::FIXED_QUEUE_SPLIT}"
201
- end
148
+ message = "knapsack_pro gem could not connect to Knapsack Pro API and the Fallback Mode cannot be used this time. Running tests in Fallback Mode are not allowed for retried parallel CI node to avoid running the wrong set of tests. Please manually retry this parallel job on your CI server then knapsack_pro gem will try to connect to Knapsack Pro API again and will run a correct set of tests for this CI node. Learn more #{KnapsackPro::Urls::NORMAL_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_TRUE_AND_POSITIVE_RETRY_COUNT}"
202
149
  KnapsackPro.logger.error(message)
203
150
  raise FallbackModeError.new(message)
204
151
  else
@@ -12,7 +12,7 @@ module KnapsackPro
12
12
 
13
13
  def exists?
14
14
  raise "Connection failed. Please report this as a bug: #{KnapsackPro::Urls::SUPPORT}" if connection_failed?
15
- return false if connection.api_code == KnapsackPro::Client::API::V1::BuildDistributions::TEST_SUITE_SPLIT_CACHE_MISS_CODE
15
+ return false if connection.api_code == KnapsackPro::Client::API::V2::Queues::CODE_ATTEMPT_CONNECT_TO_QUEUE_FAILED
16
16
 
17
17
  true
18
18
  end
@@ -22,7 +22,11 @@ module KnapsackPro
22
22
  end
23
23
 
24
24
  def test_files
25
- response.fetch('test_files')
25
+ response.fetch('paths').map { |path| { "path" => path, "time_execution" => nil } }
26
+ end
27
+
28
+ def id
29
+ response.fetch('batch_id', nil)
26
30
  end
27
31
 
28
32
  private
@@ -74,33 +78,53 @@ module KnapsackPro
74
78
  end
75
79
 
76
80
  def build_action(cache_read_attempt:, test_files: nil)
77
- unless cache_read_attempt
81
+ if cache_read_attempt # connect
82
+ KnapsackPro::Client::API::V2::Queues.queue(
83
+ attempt_connect_to_queue: true,
84
+ batch_index: 0,
85
+ branch: encrypted_branch,
86
+ can_initialize_queue: true,
87
+ commit_hash: repository_adapter.commit_hash,
88
+ node_index: ci_node_index,
89
+ node_total: ci_node_total,
90
+ node_uuid: ENV.fetch('KNAPSACK_PRO_NODE_UUID'),
91
+ single_batch: true
92
+ )
93
+ else # initialize
78
94
  raise 'Test files are required when initializing a new test suite split.' if test_files.nil?
79
- test_files = KnapsackPro::Crypto::Encryptor.call(test_files)
95
+ paths = test_files.map { |test_file| test_file.fetch("path") }
96
+
97
+ KnapsackPro::Client::API::V2::Queues.queue(
98
+ attempt_connect_to_queue: false,
99
+ batch_index: 0,
100
+ branch: encrypted_branch,
101
+ can_initialize_queue: true,
102
+ commit_hash: repository_adapter.commit_hash,
103
+ node_index: ci_node_index,
104
+ node_total: ci_node_total,
105
+ node_uuid: ENV.fetch('KNAPSACK_PRO_NODE_UUID'),
106
+ paths: KnapsackPro::Crypto::Encryptor.paths(paths),
107
+ single_batch: true
108
+ )
80
109
  end
81
-
82
- KnapsackPro::Client::API::V1::BuildDistributions.subset(
83
- cache_read_attempt: cache_read_attempt,
84
- commit_hash: repository_adapter.commit_hash,
85
- branch: encrypted_branch,
86
- node_total: ci_node_total,
87
- node_index: ci_node_index,
88
- test_files: test_files,
89
- )
90
110
  end
91
111
 
92
112
  def pull_tests
93
113
  action = build_action(cache_read_attempt: true)
94
114
  connection = KnapsackPro::Client::Connection.new(action)
95
115
  response = connection.call
96
- Split.new(connection, response)
116
+ Split.new(connection, response).tap do |batch|
117
+ ENV['KNAPSACK_PRO_BATCH_ID'] = batch.id.to_s unless batch.connection_failed?
118
+ end
97
119
  end
98
120
 
99
121
  def initialize_test_suite_split(tests_to_run)
100
122
  action = build_action(cache_read_attempt: false, test_files: tests_to_run)
101
123
  connection = KnapsackPro::Client::Connection.new(action)
102
124
  response = connection.call
103
- Split.new(connection, response)
125
+ Split.new(connection, response).tap do |batch|
126
+ ENV['KNAPSACK_PRO_BATCH_ID'] = batch.id.to_s unless batch.connection_failed?
127
+ end
104
128
  end
105
129
 
106
130
  def try_initializing_test_suite_split(tests)
@@ -113,15 +137,12 @@ module KnapsackPro
113
137
 
114
138
  def switch_to_fallback_mode
115
139
  if !KnapsackPro::Config::Env.fallback_mode_enabled?
116
- message = "Fallback Mode was disabled with KNAPSACK_PRO_FALLBACK_MODE_ENABLED=false. Please restart this CI node to retry tests. Most likely Fallback Mode was disabled due to #{KnapsackPro::Urls::REGULAR_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_FALSE}"
140
+ message = "Fallback Mode was disabled with KNAPSACK_PRO_FALLBACK_MODE_ENABLED=false. Please restart this CI node to retry tests. Most likely Fallback Mode was disabled due to #{KnapsackPro::Urls::SINGLE_BATCH_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_FALSE}"
117
141
  KnapsackPro.logger.error(message)
118
142
  exit_code = KnapsackPro::Config::Env.fallback_mode_error_exit_code
119
143
  Kernel.exit(exit_code)
120
144
  elsif KnapsackPro::Config::Env.ci_node_retry_count > 0
121
- message = "knapsack_pro gem could not connect to Knapsack Pro API and the Fallback Mode cannot be used this time. Running tests in Fallback Mode are not allowed for retried parallel CI node to avoid running the wrong set of tests. Please manually retry this parallel job on your CI server then knapsack_pro gem will try to connect to Knapsack Pro API again and will run a correct set of tests for this CI node. Learn more #{KnapsackPro::Urls::REGULAR_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_TRUE_AND_POSITIVE_RETRY_COUNT}"
122
- unless KnapsackPro::Config::Env.fixed_test_suite_split?
123
- message += " Please ensure you have set KNAPSACK_PRO_FIXED_TEST_SUITE_SPLIT=true to allow Knapsack Pro API remember the recorded CI node tests so when you retry failed tests on the CI node then the same set of tests will be executed. See more #{KnapsackPro::Urls::FIXED_TEST_SUITE_SPLIT}"
124
- end
145
+ message = "knapsack_pro gem could not connect to Knapsack Pro API and the Fallback Mode cannot be used this time. Running tests in Fallback Mode are not allowed for retried parallel CI node to avoid running the wrong set of tests. Please manually retry this parallel job on your CI server then knapsack_pro gem will try to connect to Knapsack Pro API again and will run a correct set of tests for this CI node. Learn more #{KnapsackPro::Urls::SINGLE_BATCH_MODE__CONNECTION_ERROR_WITH_FALLBACK_ENABLED_TRUE_AND_POSITIVE_RETRY_COUNT}"
125
146
  KnapsackPro.logger.error(message)
126
147
  exit_code = KnapsackPro::Config::Env.fallback_mode_error_exit_code
127
148
  Kernel.exit(exit_code)
@@ -2,17 +2,17 @@
2
2
 
3
3
  module KnapsackPro
4
4
  class Report
5
- def self.save(tests = nil)
6
- if tests.nil?
7
- tests = KnapsackPro.tracker.to_a
5
+ def self.save(test_files = nil)
6
+ if test_files.nil?
7
+ test_files = KnapsackPro.tracker.to_a
8
8
  end
9
9
 
10
- if tests.empty?
11
- KnapsackPro.logger.warn("No test files were executed on this CI node.")
12
- KnapsackPro.logger.debug("When you use knapsack_pro Regular Mode, the reason for no tests executing might be a very narrow tests list. Most likely, you run only tests with a specified tag, and there were fewer test files with the tag than parallel CI nodes.")
10
+ if test_files.empty?
11
+ KnapsackPro.logger.info("No tests were executed because the test queue is empty.")
13
12
  end
14
13
 
15
- create_build_subset(tests)
14
+ acknowledge_single_batch(test_files)
15
+ create_build_subset(test_files)
16
16
  end
17
17
 
18
18
  def self.save_subset_queue_to_file
@@ -59,6 +59,35 @@ module KnapsackPro
59
59
  create_build_subset(test_files)
60
60
  end
61
61
 
62
+ # TODO: Add support for Retry-only-Failures
63
+ def self.acknowledge_single_batch(test_files)
64
+ repository_adapter = KnapsackPro::RepositoryAdapterInitiator.call
65
+ paths = KnapsackPro::Utils.unsymbolize(test_files).map { |test_file| test_file.fetch('path') }
66
+ failed_paths = KnapsackPro::Crypto::Encryptor.paths(paths)
67
+ action = KnapsackPro::Client::API::V2::Queues.queue(
68
+ attempt_connect_to_queue: false,
69
+ batch_id: ENV['KNAPSACK_PRO_BATCH_ID'], # nil in Fallback Mode
70
+ batch_index: 1,
71
+ branch: KnapsackPro::Crypto::BranchEncryptor.call(repository_adapter.branch),
72
+ can_initialize_queue: false,
73
+ commit_hash: repository_adapter.commit_hash,
74
+ failed_paths: failed_paths,
75
+ node_total: KnapsackPro::Config::Env.ci_node_total,
76
+ node_index: KnapsackPro::Config::Env.ci_node_index,
77
+ node_uuid: ENV.fetch('KNAPSACK_PRO_NODE_UUID'),
78
+ single_batch: true
79
+ )
80
+
81
+ connection = KnapsackPro::Client::Connection.new(action)
82
+ response = connection.call
83
+ if connection.success?
84
+ raise ArgumentError.new(response) if connection.errors?
85
+ KnapsackPro.logger.debug('Acknowledged single batch.')
86
+ else
87
+ KnapsackPro.logger.warn('Single batch was not acknowledged due to connection problems.')
88
+ end
89
+ end
90
+
62
91
  def self.create_build_subset(test_files)
63
92
  repository_adapter = KnapsackPro::RepositoryAdapterInitiator.call
64
93
  test_files = KnapsackPro::Utils.unsymbolize(test_files)
@@ -77,7 +106,7 @@ module KnapsackPro
77
106
  raise ArgumentError.new(response) if connection.errors?
78
107
  KnapsackPro.logger.debug('Saved time execution report on Knapsack Pro API server.')
79
108
  else
80
- KnapsackPro.logger.warn('Time execution report was not saved on Knapsack Pro API server due to connection problem.')
109
+ KnapsackPro.logger.warn('Time execution report was not saved on Knapsack Pro API server due to connection problems.')
81
110
  end
82
111
  end
83
112
 
@@ -4,11 +4,11 @@ module KnapsackPro
4
4
  module RepositoryAdapters
5
5
  class GitAdapter < BaseAdapter
6
6
  def commit_hash
7
- `git -C "#{working_dir}" rev-parse HEAD`.strip
7
+ ENV['KNAPSACK_PRO_COMMIT_HASH'] || git_commit_hash
8
8
  end
9
9
 
10
10
  def branch
11
- `git -C "#{working_dir}" rev-parse --abbrev-ref HEAD`.strip
11
+ ENV['KNAPSACK_PRO_BRANCH'] || git_branch
12
12
  end
13
13
 
14
14
  def branches
@@ -42,6 +42,14 @@ module KnapsackPro
42
42
 
43
43
  private
44
44
 
45
+ def git_commit_hash
46
+ `git -C "#{working_dir}" rev-parse HEAD`.strip
47
+ end
48
+
49
+ def git_branch
50
+ `git -C "#{working_dir}" rev-parse --abbrev-ref HEAD`.strip
51
+ end
52
+
45
53
  def git_commit_authors
46
54
  git_unshallow if KnapsackPro::Config::Env.ci? && shallow_repository?
47
55
  `git log --since "one month ago" 2>/dev/null | git shortlog --summary --email 2>/dev/null`
@@ -5,7 +5,8 @@ module KnapsackPro
5
5
  class CucumberRunner < BaseRunner
6
6
  def self.run(args)
7
7
  ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_cucumber
8
- ENV['KNAPSACK_PRO_REGULAR_MODE_ENABLED'] = 'true'
8
+ ENV['KNAPSACK_PRO_SINGLE_BATCH_MODE_ENABLED'] = 'true'
9
+ ENV['KNAPSACK_PRO_NODE_UUID'] = SecureRandom.uuid
9
10
 
10
11
  adapter_class = KnapsackPro::Adapters::CucumberAdapter
11
12
  KnapsackPro::Config::Env.set_test_runner_adapter(adapter_class)
@@ -5,7 +5,8 @@ module KnapsackPro
5
5
  class MinitestRunner < BaseRunner
6
6
  def self.run(args)
7
7
  ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_minitest
8
- ENV['KNAPSACK_PRO_REGULAR_MODE_ENABLED'] = 'true'
8
+ ENV['KNAPSACK_PRO_SINGLE_BATCH_MODE_ENABLED'] = 'true'
9
+ ENV['KNAPSACK_PRO_NODE_UUID'] = SecureRandom.uuid
9
10
 
10
11
  adapter_class = KnapsackPro::Adapters::MinitestAdapter
11
12
  KnapsackPro::Config::Env.set_test_runner_adapter(adapter_class)
@@ -5,7 +5,6 @@ module KnapsackPro
5
5
  module Queue
6
6
  class BaseRunner
7
7
  TerminationError = Class.new(StandardError)
8
- TERMINATION_SIGNALS = %w(HUP INT TERM ABRT QUIT USR1 USR2)
9
8
 
10
9
  @@terminate_process = false
11
10
 
@@ -56,17 +55,27 @@ module KnapsackPro
56
55
  end
57
56
 
58
57
  def trap_signals
59
- TERMINATION_SIGNALS.each do |signal|
60
- Signal.trap(signal) {
61
- puts "#{signal} signal has been received. Terminating Knapsack Pro..."
58
+ Signal.trap("TERM") {
59
+ puts "SIGTERM received: Terminating Knapsack Pro..."
60
+ @@terminate_process = true
61
+ post_trap_signals(debug: true)
62
+ log_threads
63
+ }
64
+
65
+ Signal.trap("INT") {
66
+ if @@terminate_process
67
+ puts "SIGINT received: Terminated Knapsack Pro."
68
+ $stdout.flush
69
+ exit!(1)
70
+ else
71
+ puts "SIGINT received: Terminating Knapsack Pro... Interrupt again to force quit."
62
72
  @@terminate_process = true
63
73
  post_trap_signals
64
- log_threads
65
- }
66
- end
74
+ end
75
+ }
67
76
  end
68
77
 
69
- def post_trap_signals
78
+ def post_trap_signals(debug: false)
70
79
  end
71
80
 
72
81
  def log_threads
@@ -8,7 +8,7 @@ module KnapsackPro
8
8
  require 'cucumber/rake/task'
9
9
 
10
10
  ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_cucumber
11
- ENV['KNAPSACK_PRO_QUEUE_MODE_ENABLED'] = 'true'
11
+ ENV['KNAPSACK_PRO_NORMAL_MODE_ENABLED'] = 'true'
12
12
  ENV['KNAPSACK_PRO_QUEUE_ID'] = KnapsackPro::Config::EnvGenerator.set_queue_id
13
13
 
14
14
  adapter_class = KnapsackPro::Adapters::CucumberAdapter
@@ -12,7 +12,7 @@ module KnapsackPro
12
12
  ::Minitest.class_variable_set('@@installed_at_exit', true)
13
13
 
14
14
  ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_minitest
15
- ENV['KNAPSACK_PRO_QUEUE_MODE_ENABLED'] = 'true'
15
+ ENV['KNAPSACK_PRO_NORMAL_MODE_ENABLED'] = 'true'
16
16
  ENV['KNAPSACK_PRO_QUEUE_ID'] = KnapsackPro::Config::EnvGenerator.set_queue_id
17
17
 
18
18
  adapter_class = KnapsackPro::Adapters::MinitestAdapter