knapsack_pro 3.8.0 → 7.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +377 -23
  3. data/.github/dependabot.yml +11 -0
  4. data/.github/pull_request_template.md +22 -0
  5. data/.gitignore +4 -0
  6. data/CHANGELOG.md +325 -1
  7. data/Gemfile +9 -0
  8. data/README.md +3 -10
  9. data/bin/test +15 -0
  10. data/knapsack_pro.gemspec +7 -6
  11. data/lib/knapsack_pro/adapters/base_adapter.rb +17 -2
  12. data/lib/knapsack_pro/adapters/cucumber_adapter.rb +3 -3
  13. data/lib/knapsack_pro/adapters/minitest_adapter.rb +2 -0
  14. data/lib/knapsack_pro/adapters/rspec_adapter.rb +88 -49
  15. data/lib/knapsack_pro/adapters/spinach_adapter.rb +2 -0
  16. data/lib/knapsack_pro/adapters/test_unit_adapter.rb +2 -0
  17. data/lib/knapsack_pro/allocator.rb +2 -0
  18. data/lib/knapsack_pro/allocator_builder.rb +2 -0
  19. data/lib/knapsack_pro/base_allocator_builder.rb +8 -25
  20. data/lib/knapsack_pro/build_distribution_fetcher.rb +2 -0
  21. data/lib/knapsack_pro/client/api/action.rb +2 -0
  22. data/lib/knapsack_pro/client/api/v1/base.rb +2 -0
  23. data/lib/knapsack_pro/client/api/v1/build_distributions.rb +5 -0
  24. data/lib/knapsack_pro/client/api/v1/build_subsets.rb +2 -0
  25. data/lib/knapsack_pro/client/api/v1/queues.rb +6 -1
  26. data/lib/knapsack_pro/client/connection.rb +5 -6
  27. data/lib/knapsack_pro/config/ci/app_veyor.rb +18 -0
  28. data/lib/knapsack_pro/config/ci/base.rb +27 -0
  29. data/lib/knapsack_pro/config/ci/buildkite.rb +18 -0
  30. data/lib/knapsack_pro/config/ci/circle.rb +18 -0
  31. data/lib/knapsack_pro/config/ci/cirrus_ci.rb +18 -0
  32. data/lib/knapsack_pro/config/ci/codefresh.rb +18 -0
  33. data/lib/knapsack_pro/config/ci/codeship.rb +18 -0
  34. data/lib/knapsack_pro/config/ci/github_actions.rb +26 -0
  35. data/lib/knapsack_pro/config/ci/gitlab_ci.rb +20 -1
  36. data/lib/knapsack_pro/config/ci/heroku.rb +18 -0
  37. data/lib/knapsack_pro/config/ci/semaphore.rb +16 -0
  38. data/lib/knapsack_pro/config/ci/semaphore2.rb +19 -0
  39. data/lib/knapsack_pro/config/ci/travis.rb +18 -0
  40. data/lib/knapsack_pro/config/env.rb +46 -22
  41. data/lib/knapsack_pro/config/env_generator.rb +2 -0
  42. data/lib/knapsack_pro/config/temp_files.rb +8 -4
  43. data/lib/knapsack_pro/crypto/branch_encryptor.rb +2 -0
  44. data/lib/knapsack_pro/crypto/decryptor.rb +2 -0
  45. data/lib/knapsack_pro/crypto/digestor.rb +2 -0
  46. data/lib/knapsack_pro/crypto/encryptor.rb +2 -0
  47. data/lib/knapsack_pro/extensions/rspec_extension.rb +137 -0
  48. data/lib/knapsack_pro/formatters/rspec_json_formatter.rb +2 -0
  49. data/lib/knapsack_pro/formatters/time_tracker.rb +152 -0
  50. data/lib/knapsack_pro/formatters/time_tracker_fetcher.rb +20 -0
  51. data/lib/knapsack_pro/hooks/queue.rb +2 -0
  52. data/lib/knapsack_pro/logger_wrapper.rb +2 -0
  53. data/lib/knapsack_pro/mask_string.rb +9 -0
  54. data/lib/knapsack_pro/presenter.rb +6 -3
  55. data/lib/knapsack_pro/pure/queue/rspec_pure.rb +92 -0
  56. data/lib/knapsack_pro/queue_allocator.rb +2 -0
  57. data/lib/knapsack_pro/queue_allocator_builder.rb +2 -0
  58. data/lib/knapsack_pro/railtie.rb +2 -0
  59. data/lib/knapsack_pro/report.rb +15 -9
  60. data/lib/knapsack_pro/repository_adapter_initiator.rb +2 -0
  61. data/lib/knapsack_pro/repository_adapters/base_adapter.rb +2 -0
  62. data/lib/knapsack_pro/repository_adapters/env_adapter.rb +2 -0
  63. data/lib/knapsack_pro/repository_adapters/git_adapter.rb +50 -0
  64. data/lib/knapsack_pro/runners/base_runner.rb +2 -0
  65. data/lib/knapsack_pro/runners/cucumber_runner.rb +2 -0
  66. data/lib/knapsack_pro/runners/minitest_runner.rb +2 -0
  67. data/lib/knapsack_pro/runners/queue/base_runner.rb +29 -0
  68. data/lib/knapsack_pro/runners/queue/cucumber_runner.rb +9 -6
  69. data/lib/knapsack_pro/runners/queue/minitest_runner.rb +13 -6
  70. data/lib/knapsack_pro/runners/queue/rspec_runner.rb +128 -135
  71. data/lib/knapsack_pro/runners/rspec_runner.rb +22 -3
  72. data/lib/knapsack_pro/runners/spinach_runner.rb +2 -0
  73. data/lib/knapsack_pro/runners/test_unit_runner.rb +2 -0
  74. data/lib/knapsack_pro/slow_test_file_determiner.rb +2 -0
  75. data/lib/knapsack_pro/slow_test_file_finder.rb +2 -0
  76. data/lib/knapsack_pro/task_loader.rb +2 -0
  77. data/lib/knapsack_pro/test_case_detectors/rspec_test_example_detector.rb +2 -0
  78. data/lib/knapsack_pro/test_case_mergers/base_merger.rb +2 -0
  79. data/lib/knapsack_pro/test_case_mergers/rspec_merger.rb +2 -0
  80. data/lib/knapsack_pro/test_file_cleaner.rb +2 -0
  81. data/lib/knapsack_pro/test_file_finder.rb +2 -0
  82. data/lib/knapsack_pro/test_file_pattern.rb +2 -0
  83. data/lib/knapsack_pro/test_file_presenter.rb +2 -0
  84. data/lib/knapsack_pro/test_files_with_test_cases_composer.rb +2 -0
  85. data/lib/knapsack_pro/test_flat_distributor.rb +2 -0
  86. data/lib/knapsack_pro/tracker.rb +3 -3
  87. data/lib/knapsack_pro/urls.rb +4 -0
  88. data/lib/knapsack_pro/utils.rb +2 -0
  89. data/lib/knapsack_pro/version.rb +3 -1
  90. data/lib/knapsack_pro.rb +5 -3
  91. data/lib/tasks/cucumber.rake +2 -0
  92. data/lib/tasks/encrypted_branch_names.rake +2 -0
  93. data/lib/tasks/encrypted_test_file_names.rake +2 -0
  94. data/lib/tasks/minitest.rake +2 -0
  95. data/lib/tasks/queue/cucumber.rake +13 -0
  96. data/lib/tasks/queue/minitest.rake +13 -0
  97. data/lib/tasks/queue/rspec.rake +13 -0
  98. data/lib/tasks/rspec.rake +5 -0
  99. data/lib/tasks/salt.rake +2 -0
  100. data/lib/tasks/spinach.rake +2 -0
  101. data/lib/tasks/test_unit.rake +2 -0
  102. data/spec/integration/api/build_distributions_subset_spec.rb +1 -0
  103. data/spec/integration/runners/queue/rspec_runner.rb +80 -0
  104. data/spec/integration/runners/queue/rspec_runner_spec.rb +2232 -0
  105. data/spec/knapsack_pro/adapters/base_adapter_spec.rb +30 -11
  106. data/spec/knapsack_pro/adapters/cucumber_adapter_spec.rb +2 -5
  107. data/spec/knapsack_pro/adapters/rspec_adapter_spec.rb +146 -174
  108. data/spec/knapsack_pro/base_allocator_builder_spec.rb +22 -48
  109. data/spec/knapsack_pro/client/api/v1/build_distributions_spec.rb +19 -27
  110. data/spec/knapsack_pro/client/api/v1/queues_spec.rb +23 -43
  111. data/spec/knapsack_pro/client/connection_spec.rb +59 -7
  112. data/spec/knapsack_pro/config/ci/app_veyor_spec.rb +22 -8
  113. data/spec/knapsack_pro/config/ci/base_spec.rb +1 -0
  114. data/spec/knapsack_pro/config/ci/buildkite_spec.rb +51 -16
  115. data/spec/knapsack_pro/config/ci/circle_spec.rb +48 -13
  116. data/spec/knapsack_pro/config/ci/cirrus_ci_spec.rb +12 -12
  117. data/spec/knapsack_pro/config/ci/codefresh_spec.rb +21 -6
  118. data/spec/knapsack_pro/config/ci/codeship_spec.rb +20 -6
  119. data/spec/knapsack_pro/config/ci/github_actions_spec.rb +37 -10
  120. data/spec/knapsack_pro/config/ci/gitlab_ci_spec.rb +48 -13
  121. data/spec/knapsack_pro/config/ci/heroku_spec.rb +12 -12
  122. data/spec/knapsack_pro/config/ci/semaphore2_spec.rb +11 -11
  123. data/spec/knapsack_pro/config/ci/semaphore_spec.rb +12 -12
  124. data/spec/knapsack_pro/config/ci/travis_spec.rb +8 -8
  125. data/spec/knapsack_pro/config/env_spec.rb +204 -124
  126. data/spec/knapsack_pro/formatters/time_tracker_specs.rb +424 -0
  127. data/spec/knapsack_pro/hooks/queue_spec.rb +2 -2
  128. data/spec/knapsack_pro/presenter_spec.rb +1 -1
  129. data/spec/knapsack_pro/pure/queue/rspec_pure_spec.rb +224 -0
  130. data/spec/knapsack_pro/repository_adapters/git_adapter_spec.rb +72 -0
  131. data/spec/knapsack_pro/runners/queue/cucumber_runner_spec.rb +18 -16
  132. data/spec/knapsack_pro/runners/queue/minitest_runner_spec.rb +17 -14
  133. data/spec/knapsack_pro/runners/rspec_runner_spec.rb +40 -23
  134. data/spec/knapsack_pro/test_case_detectors/rspec_test_example_detector_spec.rb +1 -0
  135. data/spec/knapsack_pro/tracker_spec.rb +0 -4
  136. data/spec/knapsack_pro_spec.rb +3 -3
  137. data/spec/spec_helper.rb +0 -1
  138. metadata +26 -23
  139. data/lib/knapsack_pro/config/ci/snap_ci.rb +0 -35
  140. data/lib/knapsack_pro/config/ci/solano_ci.rb +0 -32
  141. data/lib/knapsack_pro/extensions/time.rb +0 -7
  142. data/lib/knapsack_pro/formatters/rspec_queue_profile_formatter_extension.rb +0 -56
  143. data/lib/knapsack_pro/formatters/rspec_queue_summary_formatter.rb +0 -112
  144. data/spec/knapsack_pro/config/ci/snap_ci_spec.rb +0 -104
  145. data/spec/knapsack_pro/config/ci/solano_ci_spec.rb +0 -73
  146. data/spec/knapsack_pro/extensions/time_spec.rb +0 -5
  147. data/spec/knapsack_pro/runners/queue/rspec_runner_spec.rb +0 -342
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Config
3
5
  module CI
6
+ # Semaphore Classic is deprecated
7
+ # https://semaphoreci.com/blog/semaphore-classic-deprecation
4
8
  class Semaphore < Base
5
9
  def node_total
6
10
  ENV['SEMAPHORE_THREAD_COUNT']
@@ -26,6 +30,18 @@ module KnapsackPro
26
30
  def project_dir
27
31
  ENV['SEMAPHORE_PROJECT_DIR']
28
32
  end
33
+
34
+ def detected
35
+ ENV.key?('SEMAPHORE_BUILD_NUMBER') ? self.class : nil
36
+ end
37
+
38
+ def fixed_queue_split
39
+ false
40
+ end
41
+
42
+ def ci_provider
43
+ "Semaphore CI 1.0"
44
+ end
29
45
  end
30
46
  end
31
47
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # https://docs.semaphoreci.com/article/12-environment-variables
2
4
  module KnapsackPro
3
5
  module Config
@@ -29,6 +31,23 @@ module KnapsackPro
29
31
  "#{ENV['HOME']}/#{ENV['SEMAPHORE_GIT_DIR']}"
30
32
  end
31
33
  end
34
+
35
+ def user_seat
36
+ # not provided
37
+ end
38
+
39
+ def detected
40
+ # check 2 keys to be sure we are using Semaphore 2.0
41
+ ENV.key?('SEMAPHORE') && ENV.key?('SEMAPHORE_WORKFLOW_ID') ? self.class : nil
42
+ end
43
+
44
+ def fixed_queue_split
45
+ false
46
+ end
47
+
48
+ def ci_provider
49
+ "Semaphore CI 2.0"
50
+ end
32
51
  end
33
52
  end
34
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Config
3
5
  module CI
@@ -17,6 +19,22 @@ module KnapsackPro
17
19
  def project_dir
18
20
  ENV['TRAVIS_BUILD_DIR']
19
21
  end
22
+
23
+ def user_seat
24
+ # not provided
25
+ end
26
+
27
+ def detected
28
+ ENV.key?('TRAVIS') ? self.class : nil
29
+ end
30
+
31
+ def fixed_queue_split
32
+ true
33
+ end
34
+
35
+ def ci_provider
36
+ "Travis CI"
37
+ end
20
38
  end
21
39
  end
22
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Config
3
5
  class Env
@@ -23,9 +25,10 @@ module KnapsackPro
23
25
  end
24
26
 
25
27
  def ci_node_build_id
26
- ENV['KNAPSACK_PRO_CI_NODE_BUILD_ID'] ||
28
+ env_name = 'KNAPSACK_PRO_CI_NODE_BUILD_ID'
29
+ ENV[env_name] ||
27
30
  ci_env_for(:node_build_id) ||
28
- 'missing-build-id'
31
+ raise("Missing environment variable #{env_name}. Read more at #{KnapsackPro::Urls::KNAPSACK_PRO_CI_NODE_BUILD_ID}")
29
32
  end
30
33
 
31
34
  def ci_node_retry_count
@@ -58,6 +61,17 @@ module KnapsackPro
58
61
  ci_env_for(:project_dir)
59
62
  end
60
63
 
64
+ def user_seat
65
+ ENV['KNAPSACK_PRO_USER_SEAT'] ||
66
+ ci_env_for(:user_seat)
67
+ end
68
+
69
+ def masked_user_seat
70
+ return unless user_seat
71
+
72
+ KnapsackPro::MaskString.call(user_seat)
73
+ end
74
+
61
75
  def test_file_pattern
62
76
  ENV['KNAPSACK_PRO_TEST_FILE_PATTERN']
63
77
  end
@@ -130,14 +144,6 @@ module KnapsackPro
130
144
  test_files_encrypted == 'true'
131
145
  end
132
146
 
133
- def modify_default_rspec_formatters
134
- ENV.fetch('KNAPSACK_PRO_MODIFY_DEFAULT_RSPEC_FORMATTERS', true)
135
- end
136
-
137
- def modify_default_rspec_formatters?
138
- modify_default_rspec_formatters.to_s == 'true'
139
- end
140
-
141
147
  def branch_encrypted
142
148
  ENV['KNAPSACK_PRO_BRANCH_ENCRYPTED']
143
149
  end
@@ -175,7 +181,16 @@ module KnapsackPro
175
181
  end
176
182
 
177
183
  def fixed_queue_split
178
- ENV.fetch('KNAPSACK_PRO_FIXED_QUEUE_SPLIT', false)
184
+ @fixed_queue_split ||= begin
185
+ env_name = 'KNAPSACK_PRO_FIXED_QUEUE_SPLIT'
186
+ computed = ENV.fetch(env_name, ci_env_for(:fixed_queue_split)).to_s
187
+
188
+ if !ENV.key?(env_name)
189
+ KnapsackPro.logger.info("#{env_name} is not set. Using default value: #{computed}. Learn more at #{KnapsackPro::Urls::FIXED_QUEUE_SPLIT}")
190
+ end
191
+
192
+ computed
193
+ end
179
194
  end
180
195
 
181
196
  def fixed_queue_split?
@@ -235,21 +250,25 @@ module KnapsackPro
235
250
  end
236
251
 
237
252
  def ci_env_for(env_name)
238
- value = nil
239
- ci_list = KnapsackPro::Config::CI.constants - [:Base, :GitlabCI]
240
- # load GitLab CI first to avoid edge case with order of loading envs for CI_NODE_INDEX
241
- ci_list = [:GitlabCI] + ci_list
242
- ci_list.each do |ci_name|
243
- ci_class = Object.const_get("KnapsackPro::Config::CI::#{ci_name}")
244
- ci = ci_class.new
245
- value = ci.send(env_name)
246
- break unless value.nil?
253
+ detected_ci.new.send(env_name)
254
+ end
255
+
256
+ def detected_ci
257
+ detected = KnapsackPro::Config::CI.constants.map do |constant|
258
+ Object.const_get("KnapsackPro::Config::CI::#{constant}").new.detected
247
259
  end
248
- value
260
+ .compact
261
+ .first
262
+
263
+ detected || KnapsackPro::Config::CI::Base
264
+ end
265
+
266
+ def ci_provider
267
+ detected_ci.new.ci_provider
249
268
  end
250
269
 
251
270
  def log_level
252
- LOG_LEVELS[ENV['KNAPSACK_PRO_LOG_LEVEL'].to_s.downcase] || ::Logger::DEBUG
271
+ LOG_LEVELS[ENV['KNAPSACK_PRO_LOG_LEVEL'].to_s.downcase] || ::Logger::INFO
253
272
  end
254
273
 
255
274
  def log_dir
@@ -264,6 +283,11 @@ module KnapsackPro
264
283
  ENV['KNAPSACK_PRO_TEST_RUNNER_ADAPTER'] = adapter_class.to_s.split('::').last
265
284
  end
266
285
 
286
+ def ci?
287
+ ENV.fetch('CI', 'false').downcase == 'true' ||
288
+ detected_ci != KnapsackPro::Config::CI::Base
289
+ end
290
+
267
291
  private
268
292
 
269
293
  def required_env(env_name)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Config
3
5
  class EnvGenerator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Config
3
5
  class TempFiles
@@ -22,10 +24,12 @@ module KnapsackPro
22
24
  end
23
25
 
24
26
  def self.gitignore_file_content
25
- "# This directory is used by knapsack_pro gem for storing temporary files during tests runtime.\n" <<
26
- "# Ignore all files, and do not commit this directory into your repository.\n" <<
27
- "# Learn more at https://knapsackpro.com\n" <<
28
- "*"
27
+ <<~GITIGNORE
28
+ # This directory is used by knapsack_pro gem for storing temporary files during tests runtime.
29
+ # Ignore all files, and do not commit this directory into your repository.
30
+ # Learn more at https://knapsackpro.com
31
+ *
32
+ GITIGNORE
29
33
  end
30
34
 
31
35
  def self.create_gitignore_file!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Crypto
3
5
  class BranchEncryptor
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Crypto
3
5
  class Decryptor
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Crypto
3
5
  class Digestor
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Crypto
3
5
  class Encryptor
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ module Extensions
5
+ # Facade to abstract calls to internal RSpec methods.
6
+ # To allow comparing the monkey patch with the original RSpec code, keep a similar method structure and permalink to the source.
7
+ module RSpecExtension
8
+ Seed = Struct.new(:value, :used?)
9
+
10
+ def self.setup!
11
+ RSpec::Core::World.prepend(World)
12
+ RSpec::Core::Runner.prepend(Runner)
13
+ RSpec::Core::Configuration.prepend(Configuration)
14
+ end
15
+
16
+ module World
17
+ # Based on:
18
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/world.rb#L171
19
+ #
20
+ # Filters are not announced because we do not load tests during setup. It would print `No examples found.` and we don't want that.
21
+ def knapsack__announce_filters
22
+ fail_if_config_and_cli_options_invalid
23
+ end
24
+ end
25
+
26
+ module Runner
27
+ # Based on:
28
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L98
29
+ #
30
+ # `@configuration.load_spec_files` is not called because we load tests in batches with `knapsack__load_spec_files_batch` later on.
31
+ def knapsack__setup(stream_error = $stderr, stream_out = $stdout)
32
+ configure(stream_error, stream_out)
33
+ ensure
34
+ world.knapsack__announce_filters
35
+ end
36
+
37
+ def knapsack__wants_to_quit?
38
+ world.wants_to_quit
39
+ end
40
+
41
+ def knapsack__rspec_is_quitting?
42
+ world.respond_to?(:rspec_is_quitting) && world.rspec_is_quitting
43
+ end
44
+
45
+ def knapsack__exit_early
46
+ _exit_status = configuration.reporter.exit_early(exit_code)
47
+ end
48
+
49
+ # Based on:
50
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/configuration.rb#L546
51
+ def knapsack__error_exit_code
52
+ configuration.error_exit_code # nil unless `--error-exit-code` is specified
53
+ end
54
+
55
+ # must be called after `Runner#knapsack__setup` that loads the `spec_helper.rb` configuration
56
+ def knapsack__deprecated_run_all_when_everything_filtered_enabled?
57
+ configuration.respond_to?(:run_all_when_everything_filtered) && !!configuration.run_all_when_everything_filtered
58
+ end
59
+
60
+ def knapsack__seed
61
+ Seed.new(configuration.seed.to_s, configuration.seed_used?)
62
+ end
63
+
64
+ # @param test_file_paths Array[String]
65
+ # Example: ['a_spec.rb', 'b_spec.rb[1:1]']
66
+ def knapsack__load_spec_files_batch(test_file_paths)
67
+ world.reset
68
+
69
+ # Reset filters, but do not reset `configuration.static_config_filter_manager` to preserve the --tag option
70
+ filter_manager = RSpec::Core::FilterManager.new
71
+ options.configure_filter_manager(filter_manager)
72
+ configuration.filter_manager = filter_manager
73
+
74
+ configuration.knapsack__load_spec_files(test_file_paths)
75
+ end
76
+
77
+ # Based on:
78
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L113
79
+ #
80
+ # Ignore `configuration.fail_if_no_examples` in Queue Mode:
81
+ # * a late CI node, started after all tests were executed by other nodes, is expected to receive an empty batch
82
+ # * a batch could contain tests with no examples (e.g. commented out)
83
+ #
84
+ # @return [Fixnum] exit status code.
85
+ def knapsack__run_specs(queue_runner)
86
+ # Based on:
87
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/world.rb#L53
88
+ ordering_strategy = configuration.ordering_registry.fetch(:global)
89
+ node_examples_passed = true
90
+
91
+ configuration.reporter.report(_expected_example_count = 0) do |reporter|
92
+ configuration.with_suite_hooks do
93
+ queue_runner.with_batch do |test_file_paths|
94
+ knapsack__load_spec_files_batch(test_file_paths)
95
+
96
+ examples_passed = ordering_strategy.order(world.example_groups).map do |example_group|
97
+ queue_runner.handle_signal!
98
+ example_group.run(reporter)
99
+ end.all?
100
+
101
+ node_examples_passed = false unless examples_passed
102
+
103
+ knapsack__persist_example_statuses
104
+
105
+ if reporter.fail_fast_limit_met?
106
+ queue_runner.log_fail_fast_limit_met
107
+ break
108
+ end
109
+ end
110
+ end
111
+
112
+ exit_code(node_examples_passed)
113
+ end
114
+ end
115
+
116
+ # Based on:
117
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L90
118
+ def knapsack__persist_example_statuses
119
+ persist_example_statuses
120
+ end
121
+ end
122
+
123
+ module Configuration
124
+ # Based on:
125
+ # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/configuration.rb#L1619
126
+ def knapsack__load_spec_files(test_file_paths)
127
+ batch_of_files_to_run = get_files_to_run(test_file_paths)
128
+ batch_of_files_to_run.each do |f|
129
+ file = File.expand_path(f)
130
+ load_file_handling_errors(:load, file)
131
+ loaded_spec_files << file
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RSpec::Support.require_rspec_core('formatters/json_formatter')
2
4
 
3
5
  # based on https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/formatters/json_formatter.rb
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module KnapsackPro
6
+ module Formatters
7
+ class TimeTracker
8
+ ::RSpec::Core::Formatters.register self,
9
+ :example_group_started,
10
+ :example_started,
11
+ :example_finished,
12
+ :example_group_finished
13
+
14
+ attr_reader :output # RSpec < v3.10.2
15
+
16
+ def initialize(_output)
17
+ @output = StringIO.new
18
+ @time_each = nil
19
+ @time_all = nil
20
+ @before_all = 0.0
21
+ @group = {}
22
+ @paths = {}
23
+ @suite_started = now
24
+ end
25
+
26
+ def example_group_started(notification)
27
+ return unless top_level_group?(notification.group)
28
+ @time_all = now
29
+ end
30
+
31
+ def example_started(_notification)
32
+ @before_all = now - @time_all if @before_all == 0.0
33
+ @time_each = now
34
+ end
35
+
36
+ def example_finished(notification)
37
+ record_example(@group, notification.example, @time_each)
38
+ @time_all = now
39
+ end
40
+
41
+ def example_group_finished(notification)
42
+ return unless top_level_group?(notification.group)
43
+
44
+ after_all = @time_all.nil? ? 0.0 : now - @time_all
45
+ add_hooks_time(@group, @before_all, after_all)
46
+ @before_all = 0.0
47
+ @paths = merge(@paths, @group)
48
+ @group = {}
49
+ end
50
+
51
+ def queue(scheduled_paths)
52
+ recorded_paths = @paths.values.map do |example|
53
+ KnapsackPro::Adapters::RSpecAdapter.parse_file_path(example[:path])
54
+ end
55
+
56
+ missing = (scheduled_paths - recorded_paths).each_with_object({}) do |path, object|
57
+ object[path] = { path: path, time_execution: 0.0 }
58
+ end
59
+
60
+ merge(@paths, missing).values.map do |example|
61
+ example.transform_keys(&:to_s)
62
+ end
63
+ end
64
+
65
+ def batch
66
+ @paths.values.map do |example|
67
+ example.transform_keys(&:to_s)
68
+ end
69
+ end
70
+
71
+ def duration
72
+ now - @suite_started
73
+ end
74
+
75
+ def unexecuted_test_files(scheduled_paths)
76
+ pending_paths = @paths.values
77
+ .filter { |example| example[:time_execution] == 0.0 }
78
+ .map { |example| example[:path] }
79
+
80
+ not_run_paths = scheduled_paths -
81
+ @paths.values
82
+ .map { |example| example[:path] }
83
+
84
+ pending_paths + not_run_paths
85
+ end
86
+
87
+ private
88
+
89
+ def top_level_group?(group)
90
+ group.metadata[:parent_example_group].nil?
91
+ end
92
+
93
+ def add_hooks_time(group, before_all, after_all)
94
+ group.each do |_, example|
95
+ next if example[:time_execution] == 0.0
96
+ example[:time_execution] += before_all + after_all
97
+ end
98
+ end
99
+
100
+ def record_example(accumulator, example, started_at)
101
+ path = path_for(example)
102
+ return if path.nil?
103
+
104
+ time_execution = time_execution_for(example, started_at)
105
+ if accumulator.key?(path)
106
+ accumulator[path][:time_execution] += time_execution
107
+ else
108
+ accumulator[path] = { path: path, time_execution: time_execution }
109
+ end
110
+ end
111
+
112
+ def path_for(example)
113
+ file = file_path_for(example)
114
+ return nil if file == ""
115
+
116
+ path = rspec_split_by_test_example?(file) ? example.id : file
117
+ KnapsackPro::TestFileCleaner.clean(path)
118
+ end
119
+
120
+ def rspec_split_by_test_example?(file)
121
+ return false unless KnapsackPro::Config::Env.rspec_split_by_test_examples?
122
+ return false unless KnapsackPro::Adapters::RSpecAdapter.slow_test_file?(KnapsackPro::Adapters::RSpecAdapter, file)
123
+ true
124
+ end
125
+
126
+ def file_path_for(example)
127
+ KnapsackPro::Adapters::RSpecAdapter.file_path_for(example)
128
+ end
129
+
130
+ def time_execution_for(example, started_at)
131
+ if example.execution_result.status.to_s == "pending"
132
+ 0.0
133
+ else
134
+ (now - started_at).to_f
135
+ end
136
+ end
137
+
138
+ def merge(h1, h2)
139
+ h1.merge(h2) do |key, v1, v2|
140
+ {
141
+ path: key,
142
+ time_execution: v1[:time_execution] + v2[:time_execution]
143
+ }
144
+ end
145
+ end
146
+
147
+ def now
148
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ module Formatters
5
+ class TimeTrackerFetcher
6
+ def self.call
7
+ ::RSpec
8
+ .configuration
9
+ .formatters
10
+ .find { |f| f.class.to_s == "KnapsackPro::Formatters::TimeTracker" }
11
+ end
12
+
13
+ def self.unexecuted_test_files(scheduled_paths)
14
+ time_tracker = call
15
+ return [] unless time_tracker
16
+ time_tracker.unexecuted_test_files(scheduled_paths)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  module Hooks
3
5
  class Queue
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  class LoggerWrapper
3
5
  def initialize(logger)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ class MaskString
5
+ def self.call(string)
6
+ string.gsub(/(?<=\w{2})[a-zA-Z]/, "*")
7
+ end
8
+ end
9
+ end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KnapsackPro
2
4
  class Presenter
3
5
  class << self
4
- def global_time
5
- global_time = pretty_seconds(KnapsackPro.tracker.global_time)
6
- "Global time execution for tests: #{global_time}"
6
+ def global_time(time = nil)
7
+ time = KnapsackPro.tracker.global_time if time.nil?
8
+ global_time = pretty_seconds(time)
9
+ "Global test execution duration: #{global_time}"
7
10
  end
8
11
 
9
12
  def pretty_seconds(seconds)