knapsack_pro 9.1.0 → 9.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3968c8fcf00e58b3ba539fb37bd8458455c9779b76e2cb118dd09be8caa3cc5
4
- data.tar.gz: fea6294e3ec2bd6edb7c18dae2a6c42c06cd14a3c28e4cb5a543862f4bb6326b
3
+ metadata.gz: 69f4cd183de10604ce425379105354fd37da56929568c6bac466d682dc893088
4
+ data.tar.gz: 3c9c33a0aed5050e6d6582ff66550d8497b833383b1b34c09e622dd8f57bc6b8
5
5
  SHA512:
6
- metadata.gz: eb649c4dd8c047d5455a487767c7ce30d6424e340673620c4b6365f0a2cb8aedc870a83d304f1ca024fb3d3e39c2ccf003536262f986324326814915b8bdfccf
7
- data.tar.gz: 5aee2305c85f6cd6760d75ae456a12101305dc5725cdc2b2641c13d17cf1004ad5e90168256352665c884c1bc58f702289f65a808cc3c7383667b5a88a0f52c1
6
+ metadata.gz: bac25fed3f1e309e57f21bbce67391ed2d638d8484c91ce99900aa9ab22bbebde61bf0a968e9bf208f45d9d684574cb8a0b9fd549a0f5875c036b4c0e6aeec30
7
+ data.tar.gz: 35f0c05d901993faa07ceeffc211e596b693aa852671e9a04aa12ca845ef78bb39e332b8011b846a93be6cab17f803197f2e7a008fe9f7d9140ba9df75240561
@@ -15,26 +15,10 @@ module KnapsackPro
15
15
  false
16
16
  end
17
17
 
18
- def self.test_file_cases_for(slow_test_files)
18
+ def self.calculate_slow_id_paths
19
19
  raise NotImplementedError
20
20
  end
21
21
 
22
- def self.slow_test_file?(adapter_class, test_file_path)
23
- @slow_test_file_paths ||=
24
- begin
25
- slow_test_files =
26
- if KnapsackPro::Config::Env.slow_test_file_pattern
27
- KnapsackPro::TestFileFinder.slow_test_files_by_pattern(adapter_class)
28
- else
29
- # get slow test files from JSON file based on data from API
30
- KnapsackPro::SlowTestFileDeterminer.read_from_json_report
31
- end
32
- KnapsackPro::TestFilePresenter.paths(slow_test_files)
33
- end
34
- clean_path = KnapsackPro::TestFileCleaner.clean(test_file_path)
35
- @slow_test_file_paths.include?(clean_path)
36
- end
37
-
38
22
  def self.bind
39
23
  adapter = new
40
24
  adapter.bind
@@ -20,22 +20,17 @@ module KnapsackPro
20
20
  true
21
21
  end
22
22
 
23
- def self.test_file_cases_for(slow_test_files)
24
- KnapsackPro.logger.info("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual test cases). Thanks to that, a single slow test file can be split across parallel CI nodes. Analyzing #{slow_test_files.size} slow test files.")
25
-
26
- # generate the RSpec JSON report in a separate process to not pollute the RSpec state
23
+ def self.calculate_slow_id_paths
24
+ # Shell out not to pollute the RSpec state
27
25
  cmd = [
28
26
  'RACK_ENV=test',
29
27
  'RAILS_ENV=test',
30
28
  KnapsackPro::Config::Env.rspec_test_example_detector_prefix,
31
29
  'rake knapsack_pro:rspec_test_example_detector',
32
30
  ].join(' ')
33
- unless Kernel.system(cmd)
34
- raise "Could not generate JSON report for RSpec. Rake task failed when running #{cmd}"
35
- end
31
+ raise "Failed to calculate Split by Test Examples: #{cmd}" unless Kernel.system(cmd)
36
32
 
37
- # read the JSON report
38
- KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new.test_file_example_paths
33
+ KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new.slow_id_paths!
39
34
  end
40
35
 
41
36
  def self.has_format_option?(cli_args)
@@ -86,6 +81,13 @@ module KnapsackPro
86
81
  !id.nil?
87
82
  end
88
83
 
84
+ def self.concat_paths(test_files, id_paths)
85
+ paths = KnapsackPro::TestFilePresenter.paths(test_files)
86
+ file_paths = id_paths.map { |id_path| parse_file_path(id_path) }
87
+ acc = paths + id_paths - file_paths
88
+ KnapsackPro::TestFilePresenter.test_files(acc)
89
+ end
90
+
89
91
  def self.rails_helper_exists?(test_dir)
90
92
  File.exist?("#{test_dir}/rails_helper.rb")
91
93
  end
@@ -7,10 +7,6 @@ module KnapsackPro
7
7
  @response = response
8
8
  end
9
9
 
10
- def time_execution
11
- response.fetch('time_execution')
12
- end
13
-
14
10
  def test_files
15
11
  response.fetch('test_files')
16
12
  end
@@ -20,12 +16,6 @@ module KnapsackPro
20
16
  attr_reader :response
21
17
  end
22
18
 
23
- def self.call
24
- new.call
25
- end
26
-
27
- # get test files and time execution for last build distribution matching:
28
- # branch, node_total, node_index
29
19
  def call
30
20
  connection = KnapsackPro::Client::Connection.new(build_action)
31
21
  response = connection.call
@@ -33,11 +23,8 @@ module KnapsackPro
33
23
  raise ArgumentError.new(response) if connection.errors?
34
24
  BuildDistributionEntity.new(response)
35
25
  else
36
- KnapsackPro.logger.warn("Slow test files fallback behaviour started. We could not connect with Knapsack Pro API to fetch last CI build test files that are needed to determine slow test files. No test files will be split by test cases. It means all test files will be split by the whole test files as if split by test cases would be disabled #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}")
37
- BuildDistributionEntity.new({
38
- 'time_execution' => 0.0,
39
- 'test_files' => [],
40
- })
26
+ KnapsackPro.logger.warn("Failed to fetch slow test files. Split by Test Examples disabled. See: #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}")
27
+ BuildDistributionEntity.new({ 'time_execution' => 0.0, 'test_files' => [] })
41
28
  end
42
29
  end
43
30
 
@@ -48,12 +35,29 @@ module KnapsackPro
48
35
  end
49
36
 
50
37
  def build_action
51
- KnapsackPro::Client::API::V1::BuildDistributions.last(
38
+ request_hash = {
52
39
  commit_hash: repository_adapter.commit_hash,
53
40
  branch: repository_adapter.branch,
54
41
  node_total: KnapsackPro::Config::Env.ci_node_total,
55
- node_index: KnapsackPro::Config::Env.ci_node_index,
56
- )
42
+ node_index: KnapsackPro::Config::Env.ci_node_index
43
+ }.merge(additional_params)
44
+
45
+ KnapsackPro::Client::API::V1::BuildDistributions.last(request_hash)
46
+ end
47
+
48
+ def additional_params
49
+ {}
50
+ end
51
+ end
52
+
53
+ class OptimizedBuildDistributionFetcher < BuildDistributionFetcher
54
+ private
55
+
56
+ def additional_params
57
+ {
58
+ node_build_id: KnapsackPro::Config::Env.ci_node_build_id,
59
+ none_if_queue_initialized: true
60
+ }
57
61
  end
58
62
  end
59
63
  end
@@ -35,16 +35,11 @@ module KnapsackPro
35
35
  )
36
36
  end
37
37
 
38
- def last(args)
38
+ def last(request_hash)
39
39
  action_class.new(
40
40
  endpoint_path: '/v1/build_distributions/last',
41
41
  http_method: :get,
42
- request_hash: {
43
- :commit_hash => args.fetch(:commit_hash),
44
- :branch => args.fetch(:branch),
45
- :node_total => args.fetch(:node_total),
46
- :node_index => args.fetch(:node_index),
47
- }
42
+ request_hash: request_hash
48
43
  )
49
44
  end
50
45
  end
@@ -36,6 +36,33 @@ module KnapsackPro
36
36
  request_hash: request_hash
37
37
  )
38
38
  end
39
+
40
+ def initialize(paths)
41
+ git_adapter = KnapsackPro::RepositoryAdapters::GitAdapter.new
42
+ repository_adapter = KnapsackPro::RepositoryAdapterInitiator.call
43
+
44
+ request_hash = {
45
+ attempt_connect_to_queue: false,
46
+ branch: KnapsackPro::Crypto::BranchEncryptor.call(repository_adapter.branch),
47
+ build_author: git_adapter.build_author,
48
+ can_initialize_queue: true,
49
+ commit_authors: git_adapter.commit_authors,
50
+ commit_hash: repository_adapter.commit_hash,
51
+ fixed_queue_split: KnapsackPro::Config::Env.fixed_queue_split,
52
+ node_build_id: KnapsackPro::Config::Env.ci_node_build_id,
53
+ node_index: KnapsackPro::Config::Env.ci_node_index,
54
+ node_total: KnapsackPro::Config::Env.ci_node_total,
55
+ skip_pull: true,
56
+ test_files: KnapsackPro::Crypto::Encryptor.call(paths),
57
+ user_seat: KnapsackPro::Config::Env.masked_user_seat,
58
+ }
59
+
60
+ action_class.new(
61
+ endpoint_path: '/v1/queues/queue',
62
+ http_method: :post,
63
+ request_hash: request_hash
64
+ )
65
+ end
39
66
  end
40
67
  end
41
68
  end
@@ -67,6 +67,14 @@ module KnapsackPro
67
67
  ENV['KNAPSACK_PRO_SLOW_TEST_FILE_PATTERN']
68
68
  end
69
69
 
70
+ def slow_test_file_threshold
71
+ ENV.fetch('KNAPSACK_PRO_SLOW_TEST_FILE_THRESHOLD', nil)&.to_f
72
+ end
73
+
74
+ def slow_test_file_threshold?
75
+ !!slow_test_file_threshold
76
+ end
77
+
70
78
  def test_file_exclude_pattern
71
79
  ENV['KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN']
72
80
  end
@@ -188,14 +196,6 @@ module KnapsackPro
188
196
  ENV.fetch('KNAPSACK_PRO_RSPEC_TEST_EXAMPLE_DETECTOR_PREFIX', 'bundle exec')
189
197
  end
190
198
 
191
- def slow_test_file_threshold
192
- ENV.fetch('KNAPSACK_PRO_SLOW_TEST_FILE_THRESHOLD', nil)&.to_f
193
- end
194
-
195
- def slow_test_file_threshold?
196
- !!slow_test_file_threshold
197
- end
198
-
199
199
  def test_suite_token
200
200
  env_name = 'KNAPSACK_PRO_TEST_SUITE_TOKEN'
201
201
  ENV[env_name] || raise("Missing environment variable #{env_name}. You should set environment variable like #{env_name}_RSPEC (note there is suffix _RSPEC at the end). knapsack_pro gem will set #{env_name} based on #{env_name}_RSPEC value. If you use other test runner than RSpec then use proper suffix.")
@@ -141,8 +141,9 @@ module KnapsackPro
141
141
  end
142
142
  end
143
143
 
144
+ # Run file paths to guarantee at-least-once execution across CI nodes.
144
145
  def fallback_test_files(executed_test_files)
145
- test_flat_distributor = KnapsackPro::TestFlatDistributor.new(test_suite.fallback_test_files, ci_node_total)
146
+ test_flat_distributor = KnapsackPro::TestFlatDistributor.new(test_suite.all_test_files_to_run, ci_node_total)
146
147
  test_files_for_node_index = test_flat_distributor.test_files_for_node(ci_node_index)
147
148
  KnapsackPro::TestFilePresenter.paths(test_files_for_node_index) - executed_test_files
148
149
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ module RSpec
5
+ class QueueInitializer
6
+ def call(args)
7
+ slow_id_paths = KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new.calculate_slow_id_paths(args.to_s)
8
+ all_test_files_to_run = KnapsackPro::TestSuite.new(KnapsackPro::Adapters::RSpecAdapter).all_test_files_to_run
9
+ paths = KnapsackPro::Adapters::RSpecAdapter.concat_paths(all_test_files_to_run, slow_id_paths)
10
+
11
+ raise 'No paths to run' if paths.empty?
12
+ action = KnapsackPro::Client::API::V1::Queues.initialize(paths)
13
+ connection = KnapsackPro::Client::Connection.new(action)
14
+ response = connection.call
15
+ return unless response.key?('url') # Race to initialize lost to another parallel node
16
+
17
+ KnapsackPro.logger.info "Build URL: #{response.fetch('url')}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -131,8 +131,9 @@ module KnapsackPro
131
131
  end
132
132
  end
133
133
 
134
+ # Run file paths to guarantee at-least-once execution across CI nodes.
134
135
  def fallback_test_files
135
- test_flat_distributor = KnapsackPro::TestFlatDistributor.new(test_suite.fallback_test_files, ci_node_total)
136
+ test_flat_distributor = KnapsackPro::TestFlatDistributor.new(test_suite.all_test_files_to_run, ci_node_total)
136
137
  test_files_for_node_index = test_flat_distributor.test_files_for_node(ci_node_index)
137
138
  KnapsackPro::TestFilePresenter.paths(test_files_for_node_index)
138
139
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ class RSpecSlowTestFileFinder
5
+ def initialize(build_distribution_fetcher)
6
+ @build_distribution_fetcher = build_distribution_fetcher
7
+ end
8
+
9
+ def call
10
+ if KnapsackPro::Config::Env.test_files_encrypted?
11
+ raise "Split by test cases is not possible when you have enabled test file names encryption ( #{KnapsackPro::Urls::ENCRYPTION} ). You need to disable encryption with KNAPSACK_PRO_TEST_FILES_ENCRYPTED=false in order to use split by test cases #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}"
12
+ end
13
+
14
+ test_files_from_api = @build_distribution_fetcher.call.test_files
15
+ merged_test_files_from_api = KnapsackPro::TestCaseMergers::RSpecMerger.new(test_files_from_api).call
16
+ test_files_existing_on_disk = KnapsackPro::TestFileFinder.select_test_files_that_can_be_run(KnapsackPro::Adapters::RSpecAdapter, merged_test_files_from_api)
17
+ KnapsackPro::SlowTestFileDeterminer.call(test_files_existing_on_disk)
18
+ end
19
+ end
20
+ end
@@ -18,27 +18,5 @@ module KnapsackPro
18
18
  execution_time >= KnapsackPro::Config::Env.slow_test_file_threshold
19
19
  end
20
20
  end
21
-
22
- def self.save_to_json_report(test_files)
23
- KnapsackPro::Config::TempFiles.ensure_temp_directory_exists!
24
- FileUtils.mkdir_p(report_dir)
25
- File.write(report_path, test_files.to_json)
26
- end
27
-
28
- def self.read_from_json_report
29
- raise "The report with slow test files has not been generated yet. If you have enabled split by test cases #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES} and you see this error it means that your tests accidentally cleaned up the .knapsack_pro directory. Please do not remove this directory during tests runtime!" unless File.exist?(report_path)
30
- slow_test_files_json_report = File.read(report_path)
31
- JSON.parse(slow_test_files_json_report)
32
- end
33
-
34
- private
35
-
36
- def self.report_path
37
- "#{report_dir}/slow_test_files_node_#{KnapsackPro::Config::Env.ci_node_index}.json"
38
- end
39
-
40
- def self.report_dir
41
- "#{KnapsackPro::Config::TempFiles::TEMP_DIRECTORY_PATH}/slow_test_file_determiner"
42
- end
43
21
  end
44
22
  end
@@ -3,87 +3,80 @@
3
3
  module KnapsackPro
4
4
  module TestCaseDetectors
5
5
  class RSpecTestExampleDetector
6
- def generate_json_report(rspec_args)
7
- raise "The internal KNAPSACK_PRO_RSPEC_OPTIONS environment variable is unset. Ensure it is not overridden accidentally. Otherwise, please report this as a bug: #{KnapsackPro::Urls::SUPPORT}" if rspec_args.nil?
8
-
9
- require 'rspec/core'
10
-
11
- cli_format =
12
- if Gem::Version.new(::RSpec::Core::Version::STRING) < Gem::Version.new('3.6.0')
13
- require_relative '../formatters/rspec_json_formatter'
14
- ['--format', KnapsackPro::Formatters::RSpecJsonFormatter.to_s]
15
- else
16
- ['--format', 'json']
17
- end
18
-
19
- ensure_report_dir_exists
20
- remove_old_json_report
21
-
22
- test_file_entities = slow_test_files
23
-
24
- if test_file_entities.empty?
25
- no_examples_json = { examples: [] }.to_json
26
- File.write(report_path, no_examples_json)
27
- return
28
- end
6
+ def dry_run_to_file(rspec_args, slow_test_files = slow_test_files(KnapsackPro::BuildDistributionFetcher.new))
7
+ KnapsackPro::Config::TempFiles.ensure_temp_directory_exists!
8
+ FileUtils.mkdir_p(File.dirname(report_path))
9
+ File.delete(report_path) if File.exist?(report_path)
10
+ return File.write(report_path, { examples: [] }.to_json) if slow_test_files.empty?
29
11
 
12
+ KnapsackPro.logger.info("Calculating Split by Test Examples. Analyzing #{slow_test_files.size} slow test files.")
30
13
  args = (rspec_args || '').split
31
14
  cli_args_without_formatters = KnapsackPro::Adapters::RSpecAdapter.remove_formatters(args)
32
-
33
- # Apply a --format option which overrides formatters from the RSpec custom option files like `.rspec`.
34
15
  cli_args = cli_args_without_formatters + cli_format + [
35
16
  '--dry-run',
36
17
  '--out', report_path,
37
18
  '--default-path', test_dir
38
- ] + KnapsackPro::TestFilePresenter.paths(test_file_entities)
39
- exit_code = begin
40
- options = ::RSpec::Core::ConfigurationOptions.new(cli_args)
41
- ::RSpec::Core::Runner.new(options).run($stderr, $stdout)
42
- rescue SystemExit => e
43
- e.status
44
- end
45
-
19
+ ] + KnapsackPro::TestFilePresenter.paths(slow_test_files)
20
+ exit_code = dry_run(cli_args)
46
21
  return if exit_code.zero?
47
22
 
48
23
  report.fetch('messages', []).each { |message| puts message }
49
24
  command = (['bundle exec rspec'] + cli_args).join(' ')
50
- KnapsackPro.logger.error("Failed to generate the slow test files report: #{command}")
25
+ KnapsackPro.logger.error("Failed to calculate Split by Test Examples: #{command}")
51
26
  exit exit_code
52
27
  end
53
28
 
54
- def test_file_example_paths
29
+ def calculate_slow_id_paths(rspec_args)
30
+ dry_run_to_file(rspec_args, slow_test_files(KnapsackPro::OptimizedBuildDistributionFetcher.new))
31
+ slow_id_paths!
32
+ end
33
+
34
+ def slow_id_paths!
55
35
  raise "No report found at #{report_path}" unless File.exist?(report_path)
56
36
 
57
- json_report = File.read(report_path)
58
- hash_report = JSON.parse(json_report)
59
- hash_report
37
+ JSON.parse(File.read(report_path))
60
38
  .fetch('examples')
61
- .map { |e| e.fetch('id') }
62
- .map { |path_with_example_id| test_file_hash_for(path_with_example_id) }
39
+ .map { |example| TestFileCleaner.clean(example.fetch('id')) }
63
40
  end
64
41
 
65
- def slow_test_files
66
- if KnapsackPro::Config::Env.slow_test_file_pattern
67
- KnapsackPro::TestFileFinder.slow_test_files_by_pattern(adapter_class)
42
+ private
43
+
44
+ # Apply a --format option which overrides formatters from the RSpec custom option files like `.rspec`.
45
+ def cli_format
46
+ require 'rspec/core'
47
+
48
+ if Gem::Version.new(::RSpec::Core::Version::STRING) < Gem::Version.new('3.6.0')
49
+ require_relative '../formatters/rspec_json_formatter'
50
+ ['--format', KnapsackPro::Formatters::RSpecJsonFormatter.to_s]
68
51
  else
69
- # read slow test files from JSON file on disk that was generated
70
- # by lib/knapsack_pro/base_allocator_builder.rb
71
- KnapsackPro::SlowTestFileDeterminer.read_from_json_report
52
+ ['--format', 'json']
72
53
  end
73
54
  end
74
55
 
75
- private
56
+ def dry_run(cli_args)
57
+ require 'rspec/core'
76
58
 
77
- def report_dir
78
- "#{KnapsackPro::Config::TempFiles::TEMP_DIRECTORY_PATH}/test_case_detectors/rspec"
59
+ options = ::RSpec::Core::ConfigurationOptions.new(cli_args)
60
+ ::RSpec::Core::Runner.new(options).run($stderr, $stdout)
61
+ rescue SystemExit => e
62
+ e.status
79
63
  end
80
64
 
81
65
  def report_path
82
- "#{report_dir}/rspec_dry_run_json_report_node_#{KnapsackPro::Config::Env.ci_node_index}.json"
66
+ "#{KnapsackPro::Config::TempFiles::TEMP_DIRECTORY_PATH}/test_case_detectors/rspec/rspec_dry_run_json_report_node_#{KnapsackPro::Config::Env.ci_node_index}.json"
67
+ end
68
+
69
+ def slow_test_files(build_distribution_fetcher)
70
+ if KnapsackPro::Config::Env.slow_test_file_pattern
71
+ KnapsackPro::TestFileFinder.slow_test_files_by_pattern(adapter_class)
72
+ else
73
+ KnapsackPro::RSpecSlowTestFileFinder.new(build_distribution_fetcher).call
74
+ end
83
75
  end
84
76
 
85
77
  def report
86
78
  return {} unless File.exist?(report_path)
79
+
87
80
  JSON.parse(File.read(report_path))
88
81
  end
89
82
 
@@ -94,25 +87,6 @@ module KnapsackPro
94
87
  def test_dir
95
88
  KnapsackPro::Config::Env.test_dir || KnapsackPro::TestFilePattern.test_dir(adapter_class)
96
89
  end
97
-
98
- def test_file_pattern
99
- KnapsackPro::TestFilePattern.call(adapter_class)
100
- end
101
-
102
- def ensure_report_dir_exists
103
- KnapsackPro::Config::TempFiles.ensure_temp_directory_exists!
104
- FileUtils.mkdir_p(report_dir)
105
- end
106
-
107
- def remove_old_json_report
108
- File.delete(report_path) if File.exist?(report_path)
109
- end
110
-
111
- def test_file_hash_for(test_file_path)
112
- {
113
- 'path' => TestFileCleaner.clean(test_file_path)
114
- }
115
- end
116
90
  end
117
91
  end
118
92
  end
@@ -2,44 +2,32 @@
2
2
 
3
3
  module KnapsackPro
4
4
  module TestCaseMergers
5
- class RSpecMerger < BaseMerger
5
+ class RSpecMerger
6
+ def initialize(test_files)
7
+ @test_files = test_files
8
+ end
9
+
6
10
  def call
7
- all_test_files_hash = {}
8
- merged_test_file_examples_hash = {}
11
+ file_paths = {}
12
+ id_paths = {}
9
13
 
10
- test_files.each do |test_file|
11
- path = test_file.fetch('path')
12
- test_file_path = extract_test_file_path(path)
14
+ @test_files.each do |test_file|
15
+ raw_path = test_file.fetch('path')
16
+ file_path = KnapsackPro::Adapters::RSpecAdapter.parse_file_path(raw_path)
13
17
 
14
- if KnapsackPro::Adapters::RSpecAdapter.id_path?(path)
15
- merged_test_file_examples_hash[test_file_path] ||= 0.0
16
- merged_test_file_examples_hash[test_file_path] += test_file.fetch('time_execution')
18
+ if KnapsackPro::Adapters::RSpecAdapter.id_path?(raw_path)
19
+ id_paths[file_path] ||= 0.0
20
+ id_paths[file_path] += test_file.fetch('time_execution')
17
21
  else
18
- all_test_files_hash[test_file_path] = test_file.fetch('time_execution')
22
+ file_paths[file_path] = test_file.fetch('time_execution') # may be nil
19
23
  end
20
24
  end
21
25
 
22
- merged_test_file_examples_hash.each do |path, time_execution|
23
- all_test_files_hash[path] = [time_execution, all_test_files_hash[path]].compact.max
24
- end
25
-
26
- merged_test_files = []
27
- all_test_files_hash.each do |path, time_execution|
28
- merged_test_files << {
29
- 'path' => path,
30
- 'time_execution' => time_execution
31
- }
32
- end
33
- merged_test_files
34
- end
35
-
36
- private
37
-
38
- # path - can be:
39
- # test file path: spec/a_spec.rb
40
- # or test example path: spec/a_spec.rb[1:1]
41
- def extract_test_file_path(path)
42
- path.gsub(/\.rb\[.+\]$/, '.rb')
26
+ file_paths
27
+ .merge(id_paths) { |_, v1, v2| [v1, v2].compact.max }
28
+ .map do |file_path, time_execution|
29
+ { 'path' => file_path, 'time_execution' => time_execution }
30
+ end
43
31
  end
44
32
  end
45
33
  end
@@ -6,41 +6,22 @@ module KnapsackPro
6
6
  new(test_file_pattern, test_file_list_enabled).call
7
7
  end
8
8
 
9
- # finds slow test files on disk based on ENV patterns
10
- # returns example: [{ 'path' => 'a_spec.rb' }]
11
9
  def self.slow_test_files_by_pattern(adapter_class)
12
10
  raise 'KNAPSACK_PRO_SLOW_TEST_FILE_PATTERN is not defined' unless KnapsackPro::Config::Env.slow_test_file_pattern
13
11
 
14
12
  test_file_pattern = KnapsackPro::TestFilePattern.call(adapter_class)
15
- test_file_entities = call(test_file_pattern)
16
-
17
- slow_test_file_entities = call(KnapsackPro::Config::Env.slow_test_file_pattern, test_file_list_enabled: false)
18
-
19
- # slow test files (KNAPSACK_PRO_SLOW_TEST_FILE_PATTERN)
20
- # should be subset of test file pattern (KNAPSACK_PRO_TEST_FILE_PATTERN)
21
- slow_test_file_entities & test_file_entities
13
+ scheduled_test_files = call(test_file_pattern)
14
+ slow_test_files = call(KnapsackPro::Config::Env.slow_test_file_pattern, test_file_list_enabled: false)
15
+ scheduled_test_files & slow_test_files
22
16
  end
23
17
 
24
- # Args:
25
- # test_file_entities_to_run - it can be list of slow test files that you want to run
26
- # Return:
27
- # subset of test_file_entities_to_run that are present on disk and it is subset of tests matching pattern KNAPSACK_PRO_TEST_FILE_PATTERN
28
- # Thanks to that we can select only slow test files that are within list of test file pattern we want to run tests for
29
- def self.select_test_files_that_can_be_run(adapter_class, test_file_entities_to_run)
18
+ def self.select_test_files_that_can_be_run(adapter_class, candidate_test_files)
30
19
  test_file_pattern = KnapsackPro::TestFilePattern.call(adapter_class)
31
- test_file_entities = call(test_file_pattern)
32
-
33
- test_file_paths_existing_on_disk = KnapsackPro::TestFilePresenter.paths(test_file_entities)
34
-
35
- selected_test_files = []
36
-
37
- test_file_entities_to_run.each do |test_file_entity|
38
- if test_file_paths_existing_on_disk.include?(test_file_entity.fetch('path'))
39
- selected_test_files << test_file_entity
40
- end
41
- end
42
-
43
- selected_test_files
20
+ scheduled_test_files = call(test_file_pattern)
21
+ scheduled_paths = KnapsackPro::TestFilePresenter.paths(scheduled_test_files)
22
+ candidate_paths = KnapsackPro::TestFilePresenter.paths(candidate_test_files)
23
+ paths = scheduled_paths & candidate_paths
24
+ candidate_test_files.filter { |test_file| paths.include? test_file.fetch('path') }
44
25
  end
45
26
 
46
27
  def initialize(test_file_pattern, test_file_list_enabled)
@@ -49,18 +30,16 @@ module KnapsackPro
49
30
  end
50
31
 
51
32
  def call
52
- test_file_hashes = []
53
- test_files.each do |test_file_path|
54
- test_file_hashes << test_file_hash_for(test_file_path)
33
+ file_paths.map do |file_path|
34
+ { 'path' => TestFileCleaner.clean(file_path) }
55
35
  end
56
- test_file_hashes
57
36
  end
58
37
 
59
38
  private
60
39
 
61
40
  attr_reader :test_file_pattern, :test_file_list_enabled
62
41
 
63
- def test_files
42
+ def file_paths
64
43
  if test_file_list_enabled && KnapsackPro::Config::Env.test_file_list
65
44
  return KnapsackPro::Config::Env.test_file_list.split(',').map(&:strip)
66
45
  end
@@ -69,22 +48,16 @@ module KnapsackPro
69
48
  return File.read(KnapsackPro::Config::Env.test_file_list_source_file).split(/\n/)
70
49
  end
71
50
 
72
- test_file_paths = Dir.glob(test_file_pattern).uniq
51
+ included_paths = Dir.glob(test_file_pattern).uniq
73
52
 
74
- excluded_test_file_paths =
53
+ excluded_paths =
75
54
  if KnapsackPro::Config::Env.test_file_exclude_pattern
76
55
  Dir.glob(KnapsackPro::Config::Env.test_file_exclude_pattern).uniq
77
56
  else
78
57
  []
79
58
  end
80
59
 
81
- (test_file_paths - excluded_test_file_paths).sort
82
- end
83
-
84
- def test_file_hash_for(test_file_path)
85
- {
86
- 'path' => TestFileCleaner.clean(test_file_path)
87
- }
60
+ (included_paths - excluded_paths).sort
88
61
  end
89
62
  end
90
63
  end
@@ -12,5 +12,9 @@ module KnapsackPro
12
12
  def self.paths(test_files)
13
13
  test_files.map { |t| t['path'] }
14
14
  end
15
+
16
+ def self.test_files(paths)
17
+ paths.map { |path| { 'path' => path } }
18
+ end
15
19
  end
16
20
  end
@@ -8,8 +8,6 @@ module KnapsackPro
8
8
  @adapter_class = adapter_class
9
9
  end
10
10
 
11
- # Detect test files present on the disk that should be run.
12
- # This may include fast test files + slow test files split by test cases.
13
11
  def calculate_test_files
14
12
  return @result if defined?(@result)
15
13
 
@@ -17,47 +15,22 @@ module KnapsackPro
17
15
  return @result = Result.new(all_test_files_to_run, true)
18
16
  end
19
17
 
20
- slow_test_files, quick =
21
- if slow_test_file_pattern
22
- [KnapsackPro::TestFileFinder.slow_test_files_by_pattern(adapter_class), true]
23
- else
24
- [KnapsackPro::SlowTestFileFinder.call(adapter_class), false]
25
- end
26
-
27
- KnapsackPro.logger.debug("Detected #{slow_test_files.size} slow test files: #{slow_test_files.inspect}")
28
-
29
- if slow_test_files.empty?
30
- return @result = Result.new(all_test_files_to_run, quick)
18
+ if KnapsackPro::Config::Env.slow_test_file_pattern
19
+ slow_test_files = KnapsackPro::TestFileFinder.slow_test_files_by_pattern(adapter_class)
20
+ return @result = Result.new(all_test_files_to_run, true) if slow_test_files.empty?
31
21
  end
32
22
 
33
- test_file_cases = adapter_class.test_file_cases_for(slow_test_files)
34
-
35
- fast_files_and_cases_for_slow_tests = KnapsackPro::TestFilesWithTestCasesComposer.call(all_test_files_to_run, slow_test_files, test_file_cases)
36
-
37
- @result = Result.new(fast_files_and_cases_for_slow_tests, false)
23
+ slow_id_paths = adapter_class.calculate_slow_id_paths
24
+ test_files = adapter_class.concat_paths(all_test_files_to_run, slow_id_paths)
25
+ @result = Result.new(test_files, false)
38
26
  end
39
27
 
40
- # In Fallback Mode, we always want to run whole test files (not split by
41
- # test cases) to guarantee that each test will be executed at least once
42
- # across parallel CI nodes.
43
- def fallback_test_files
44
- all_test_files_to_run
28
+ def all_test_files_to_run
29
+ @all_test_files_to_run ||= KnapsackPro::TestFileFinder.call(TestFilePattern.call(adapter_class))
45
30
  end
46
31
 
47
32
  private
48
33
 
49
34
  attr_reader :adapter_class
50
-
51
- def all_test_files_to_run
52
- @all_test_files_to_run ||= KnapsackPro::TestFileFinder.call(test_file_pattern)
53
- end
54
-
55
- def test_file_pattern
56
- TestFilePattern.call(adapter_class)
57
- end
58
-
59
- def slow_test_file_pattern
60
- KnapsackPro::Config::Env.slow_test_file_pattern
61
- end
62
35
  end
63
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KnapsackPro
4
- VERSION = '9.1.0'
4
+ VERSION = '9.2.0'
5
5
  end
data/lib/knapsack_pro.rb CHANGED
@@ -58,12 +58,10 @@ require_relative 'knapsack_pro/regular_allocator'
58
58
  require_relative 'knapsack_pro/queue_allocator'
59
59
  require_relative 'knapsack_pro/mask_string'
60
60
  require_relative 'knapsack_pro/test_suite'
61
- require_relative 'knapsack_pro/test_case_mergers/base_merger'
62
61
  require_relative 'knapsack_pro/test_case_mergers/rspec_merger'
63
62
  require_relative 'knapsack_pro/build_distribution_fetcher'
64
63
  require_relative 'knapsack_pro/slow_test_file_determiner'
65
- require_relative 'knapsack_pro/slow_test_file_finder'
66
- require_relative 'knapsack_pro/test_files_with_test_cases_composer'
64
+ require_relative 'knapsack_pro/rspec_slow_test_file_finder'
67
65
  require_relative 'knapsack_pro/base_allocator_builder'
68
66
  require_relative 'knapsack_pro/regular_allocator_builder'
69
67
  require_relative 'knapsack_pro/queue_allocator_builder'
@@ -12,5 +12,17 @@ namespace :knapsack_pro do
12
12
  Rake::Task.clear
13
13
  KnapsackPro::Runners::Queue::RSpecRunner.run(args[:rspec_args])
14
14
  end
15
+
16
+ namespace :rspec do
17
+ desc 'Initialize the test queue to be consumed later.'
18
+ task :initialize, [:rspec_args] do |_, args|
19
+ require_relative '../../knapsack_pro/queue_initializer'
20
+
21
+ ENV.delete('SPEC_OPTS') # Ignore `SPEC_OPTS` to not affect the RSpec execution within this rake task
22
+ ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_rspec
23
+
24
+ KnapsackPro::RSpec::QueueInitializer.new.call(args[:rspec_args].to_s)
25
+ end
26
+ end
15
27
  end
16
28
  end
data/lib/tasks/rspec.rake CHANGED
@@ -7,12 +7,16 @@ namespace :knapsack_pro do
7
7
  KnapsackPro::Runners::RSpecRunner.run(args[:rspec_args])
8
8
  end
9
9
 
10
- desc "Generate JSON report for test suite based on default test pattern or based on defined pattern with ENV vars"
10
+ # private
11
11
  task :rspec_test_example_detector do
12
- # ignore the `SPEC_OPTS` options to not affect RSpec execution within this rake task
12
+ key = 'KNAPSACK_PRO_RSPEC_OPTIONS'
13
+ raise "The internal #{key} environment variable is unset. Ensure it is not overridden accidentally. Otherwise, please report this as a bug: #{KnapsackPro::Urls::SUPPORT}" if ENV[key].nil?
14
+
15
+ # Ignore `SPEC_OPTS` to not affect the RSpec execution within this rake task
13
16
  ENV.delete('SPEC_OPTS')
14
17
 
15
- detector = KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new
16
- detector.generate_json_report(ENV['KNAPSACK_PRO_RSPEC_OPTIONS'])
18
+ KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector
19
+ .new
20
+ .dry_run_to_file(ENV[key])
17
21
  end
18
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knapsack_pro
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.1.0
4
+ version: 9.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ArturT
@@ -111,16 +111,16 @@ dependencies:
111
111
  name: rspec-its
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
- - - "~>"
114
+ - - ">="
115
115
  - !ruby/object:Gem::Version
116
- version: '1.3'
116
+ version: '0'
117
117
  type: :development
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
- - - "~>"
121
+ - - ">="
122
122
  - !ruby/object:Gem::Version
123
- version: '1.3'
123
+ version: '0'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: spinach
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -250,6 +250,7 @@ files:
250
250
  - lib/knapsack_pro/queue.rb
251
251
  - lib/knapsack_pro/queue_allocator.rb
252
252
  - lib/knapsack_pro/queue_allocator_builder.rb
253
+ - lib/knapsack_pro/queue_initializer.rb
253
254
  - lib/knapsack_pro/railtie.rb
254
255
  - lib/knapsack_pro/regular_allocator.rb
255
256
  - lib/knapsack_pro/regular_allocator_builder.rb
@@ -258,6 +259,7 @@ files:
258
259
  - lib/knapsack_pro/repository_adapters/base_adapter.rb
259
260
  - lib/knapsack_pro/repository_adapters/env_adapter.rb
260
261
  - lib/knapsack_pro/repository_adapters/git_adapter.rb
262
+ - lib/knapsack_pro/rspec_slow_test_file_finder.rb
261
263
  - lib/knapsack_pro/runners/base_runner.rb
262
264
  - lib/knapsack_pro/runners/cucumber_runner.rb
263
265
  - lib/knapsack_pro/runners/minitest_runner.rb
@@ -269,16 +271,13 @@ files:
269
271
  - lib/knapsack_pro/runners/spinach_runner.rb
270
272
  - lib/knapsack_pro/runners/test_unit_runner.rb
271
273
  - lib/knapsack_pro/slow_test_file_determiner.rb
272
- - lib/knapsack_pro/slow_test_file_finder.rb
273
274
  - lib/knapsack_pro/task_loader.rb
274
275
  - lib/knapsack_pro/test_case_detectors/rspec_test_example_detector.rb
275
- - lib/knapsack_pro/test_case_mergers/base_merger.rb
276
276
  - lib/knapsack_pro/test_case_mergers/rspec_merger.rb
277
277
  - lib/knapsack_pro/test_file_cleaner.rb
278
278
  - lib/knapsack_pro/test_file_finder.rb
279
279
  - lib/knapsack_pro/test_file_pattern.rb
280
280
  - lib/knapsack_pro/test_file_presenter.rb
281
- - lib/knapsack_pro/test_files_with_test_cases_composer.rb
282
281
  - lib/knapsack_pro/test_flat_distributor.rb
283
282
  - lib/knapsack_pro/test_suite.rb
284
283
  - lib/knapsack_pro/tracker.rb
@@ -319,7 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
319
318
  - !ruby/object:Gem::Version
320
319
  version: '0'
321
320
  requirements: []
322
- rubygems_version: 3.6.9
321
+ rubygems_version: 4.0.3
323
322
  specification_version: 4
324
323
  summary: Knapsack Pro splits tests across parallel CI nodes and ensures each parallel
325
324
  job finish work at a similar time.
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module KnapsackPro
4
- class SlowTestFileFinder
5
- # Get recorded test files from API.
6
- # Find slow tests among them that are still present on the disk.
7
- # Save slow test files in json file on the disk.
8
- # Returns slow test files.
9
- def self.call(adapter_class)
10
- if KnapsackPro::Config::Env.test_files_encrypted?
11
- raise "Split by test cases is not possible when you have enabled test file names encryption ( #{KnapsackPro::Urls::ENCRYPTION} ). You need to disable encryption with KNAPSACK_PRO_TEST_FILES_ENCRYPTED=false in order to use split by test cases #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}"
12
- end
13
-
14
- # get list of recorded test files for last CI Build
15
- build_distribution_entity = KnapsackPro::BuildDistributionFetcher.call
16
- test_files_from_api = build_distribution_entity.test_files
17
-
18
- merged_test_files_from_api = KnapsackPro::TestCaseMergers::BaseMerger.call(adapter_class, test_files_from_api)
19
-
20
- test_files_existing_on_disk = KnapsackPro::TestFileFinder.select_test_files_that_can_be_run(adapter_class, merged_test_files_from_api)
21
-
22
- slow_test_files = KnapsackPro::SlowTestFileDeterminer.call(test_files_existing_on_disk)
23
-
24
- KnapsackPro::SlowTestFileDeterminer.save_to_json_report(slow_test_files)
25
-
26
- slow_test_files
27
- end
28
- end
29
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module KnapsackPro
4
- module TestCaseMergers
5
- class BaseMerger
6
- # values must be string to avoid circular dependency problem during loading files
7
- ADAPTER_TO_MERGER_MAP = {
8
- KnapsackPro::Adapters::RSpecAdapter => 'KnapsackPro::TestCaseMergers::RSpecMerger',
9
- }
10
-
11
- def self.call(adapter_class, test_files)
12
- merger_class =
13
- ADAPTER_TO_MERGER_MAP[adapter_class] ||
14
- raise("Test case merger does not exist for adapter_class: #{adapter_class}")
15
- Kernel.const_get(merger_class).new(test_files).call
16
- end
17
-
18
- def initialize(test_files)
19
- @test_files = test_files
20
- end
21
-
22
- def call
23
- raise NotImplementedError
24
- end
25
-
26
- private
27
-
28
- attr_reader :test_files
29
- end
30
- end
31
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module KnapsackPro
4
- class TestFilesWithTestCasesComposer
5
- # Args:
6
- # All 3 arguments have structure: [{ 'path' => 'spec/a_spec.rb', 'time_execution' => 0.0 }]
7
- # time_execution is not always present (but it's not relevant here)
8
- #
9
- # test_files - list of test files that you want to run tests for
10
- # slow_test_files - list of slow test files that should be split by test cases
11
- # test_file_cases - list of paths to test cases (test examples) inside of all slow test files (slow_test_files)
12
- # Return:
13
- # Test files and test cases paths (it excludes test files that should be split by test cases (test examples))
14
- def self.call(test_files, slow_test_files, test_file_cases)
15
- slow_test_file_paths = KnapsackPro::TestFilePresenter.paths(slow_test_files)
16
-
17
- test_files_without_slow_test_files = test_files.reject do |test_file|
18
- slow_test_file_paths.include?(test_file.fetch('path'))
19
- end
20
-
21
- test_files_without_slow_test_files + test_file_cases
22
- end
23
- end
24
- end