knapsack_pro 11.0.1 → 11.1.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: 9ef9cbd99ef5c4bdb062a04337881da1422b6d06d9b217f915501ae29205e223
4
- data.tar.gz: 36f197ac02ab0a653e46f4400d0b1cafd8600528255a4d1262c7d1db988345cd
3
+ metadata.gz: 4048ec74df7fa6a88b8f44df5afe764f841c2f8823ae08964e03c6ae87e5639c
4
+ data.tar.gz: 312475082e64ade4db5a8b32160b9ddb294878140149140f1eb6921e08c4d242
5
5
  SHA512:
6
- metadata.gz: 9381634364fd597f823ee9bb836fad096b1ce9e38c03dc106d95a07ceadaf7f1930f0f4849e5e0e478316d3950b4ce1d509207150ca8f50a863972ee071a319e
7
- data.tar.gz: 4dcb81bf900413c96199674a8077cfb1e6abc8027813c742dfb2a4f625258d72794e1f24c165bd3c0e77849ded2afc75bf9ef7048c7a8a343e6998efb8668b29
6
+ metadata.gz: 4c85eb9aa82c714abf8bbbd609725e8f9f05f7c862fd46daf75cb4db4f52ea45e50d90fe731669af1bb592380ed1644441ed73362f90a6f6468a477edbbbf32f
7
+ data.tar.gz: a8cc23ab19ffb5a4c193942e3ae712c3ff28904098a246583389dafc691f95ed61b4c04509aa9dc6b6115d0470a30a09a45c61000c0f3a609e7c3142e4739555
@@ -6,28 +6,7 @@ module KnapsackPro
6
6
  TEST_DIR_PATTERN = 'features/**{,/*/**}/*.feature'
7
7
 
8
8
  def self.test_path(object)
9
- if ::Cucumber::VERSION.to_i >= 2
10
- test_case = object
11
- test_case.location.file
12
- else
13
- if object.respond_to?(:scenario_outline)
14
- if object.scenario_outline.respond_to?(:feature)
15
- # Cucumber < 1.3
16
- object.scenario_outline.feature.file
17
- else
18
- # Cucumber >= 1.3
19
- object.scenario_outline.file
20
- end
21
- else
22
- if object.respond_to?(:feature)
23
- # Cucumber < 1.3
24
- object.feature.file
25
- else
26
- # Cucumber >= 1.3
27
- object.file
28
- end
29
- end
30
- end
9
+ object.location.file
31
10
  end
32
11
 
33
12
  def bind_time_tracker
@@ -50,7 +29,7 @@ module KnapsackPro
50
29
  exit_status = latest_error.status if latest_error.is_a?(SystemExit)
51
30
  # saving report makes API call which changes exit status
52
31
  # from cucumber so we need to preserve cucumber exit status
53
- KnapsackPro::Report.save
32
+ KnapsackPro::Report.save(KnapsackPro.tracker.to_a, KnapsackPro.tracker.current_batch_failed_paths)
54
33
  ::Kernel.exit exit_status if exit_status
55
34
  end
56
35
  end
@@ -75,11 +54,7 @@ module KnapsackPro
75
54
  private
76
55
 
77
56
  def Around(*tag_expressions, &proc)
78
- if ::Cucumber::VERSION.to_i >= 3
79
- ::Cucumber::Glue::Dsl.register_rb_hook('around', tag_expressions, proc)
80
- else
81
- ::Cucumber::RbSupport::RbDsl.register_rb_hook('around', tag_expressions, proc)
82
- end
57
+ ::Cucumber::Glue::Dsl.register_rb_hook('around', tag_expressions, proc)
83
58
  end
84
59
  end
85
60
  end
@@ -23,29 +23,33 @@ module KnapsackPro
23
23
  end
24
24
 
25
25
  module BindTimeTrackerMinitestPlugin
26
- def before_setup
27
- super
28
- KnapsackPro.tracker.current_test_path = KnapsackPro::Adapters::MinitestAdapter.test_path(self)
29
- KnapsackPro.tracker.start_timer
30
- end
26
+ def run
27
+ path = KnapsackPro::Adapters::MinitestAdapter.test_path(self)
28
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+
30
+ result = super
31
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
32
+
33
+ failed = result.failures.any? { |failure| !failure.is_a?(::Minitest::Skip) }
34
+ KnapsackPro.tracker.stage(path, elapsed, failed: failed)
31
35
 
32
- def after_teardown
33
- KnapsackPro.tracker.stop_timer
34
- super
36
+ result
35
37
  end
36
38
  end
37
39
 
38
40
  def bind_time_tracker
39
- ::Minitest::Test.send(:include, BindTimeTrackerMinitestPlugin)
41
+ ::Minitest::Test.send(:prepend, BindTimeTrackerMinitestPlugin)
40
42
 
41
43
  add_post_run_callback do
44
+ KnapsackPro.tracker.flush_staged_tests
42
45
  KnapsackPro.logger.debug(KnapsackPro::Presenter.global_time)
43
46
  end
44
47
  end
45
48
 
46
49
  def bind_save_report
47
50
  add_post_run_callback do
48
- KnapsackPro::Report.save
51
+ KnapsackPro.tracker.flush_staged_tests
52
+ KnapsackPro::Report.save(KnapsackPro.tracker.to_a, KnapsackPro.tracker.current_batch_failed_paths)
49
53
  end
50
54
  end
51
55
 
@@ -55,29 +59,30 @@ module KnapsackPro
55
59
  end
56
60
 
57
61
  module BindNormalModeMinitestPlugin
58
- def before_setup
59
- super
60
-
62
+ def run
61
63
  unless ENV['KNAPSACK_PRO_BEFORE_QUEUE_HOOK_CALLED']
62
64
  KnapsackPro::Hooks::Queue.call_before_queue
63
65
  ENV['KNAPSACK_PRO_BEFORE_QUEUE_HOOK_CALLED'] = 'true'
64
66
  end
65
67
 
66
- KnapsackPro.tracker.current_test_path = KnapsackPro::Adapters::MinitestAdapter.test_path(self)
67
- KnapsackPro.tracker.start_timer
68
- end
68
+ path = KnapsackPro::Adapters::MinitestAdapter.test_path(self)
69
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
+
71
+ result = super
72
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
69
73
 
70
- def after_teardown
71
- KnapsackPro.tracker.stop_timer
74
+ failed = result.failures.any? { |failure| !failure.is_a?(::Minitest::Skip) }
75
+ KnapsackPro.tracker.stage(path, elapsed, failed: failed)
72
76
 
73
- super
77
+ result
74
78
  end
75
79
  end
76
80
 
77
81
  def bind_normal_mode
78
- ::Minitest::Test.send(:include, BindNormalModeMinitestPlugin)
82
+ ::Minitest::Test.send(:prepend, BindNormalModeMinitestPlugin)
79
83
 
80
84
  add_post_run_callback do
85
+ KnapsackPro.tracker.flush_staged_tests
81
86
  KnapsackPro.logger.debug(KnapsackPro::Presenter.global_time)
82
87
  end
83
88
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../formatters/time_tracker_fetcher'
3
+ require_relative '../formatters/rspec/time_tracker_fetcher'
4
4
 
5
5
  module KnapsackPro
6
6
  module Adapters
@@ -136,7 +136,7 @@ module KnapsackPro
136
136
  def log_tests_duration
137
137
  ::RSpec.configure do |config|
138
138
  config.append_after(:suite) do
139
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
139
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
140
140
  formatted = KnapsackPro::Presenter.global_time(time_tracker.duration)
141
141
  KnapsackPro.logger.debug(formatted)
142
142
  end
@@ -148,7 +148,7 @@ module KnapsackPro
148
148
 
149
149
  ::RSpec.configure do |config|
150
150
  config.append_before(:suite) do
151
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
151
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
152
152
  time_tracker.schedule(KnapsackPro::Adapters::RSpecAdapter.scheduled_paths)
153
153
  end
154
154
  end
@@ -157,8 +157,8 @@ module KnapsackPro
157
157
  def bind_save_report
158
158
  ::RSpec.configure do |config|
159
159
  config.after(:suite) do
160
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
161
- KnapsackPro::Report.save(time_tracker.batch)
160
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
161
+ KnapsackPro::Report.save(time_tracker.batch, time_tracker.current_batch_failed_paths)
162
162
  end
163
163
  end
164
164
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ module Formatters
5
+ module Cucumber
6
+ class FailuresTracker
7
+ def initialize(config)
8
+ failed_locations = {}
9
+
10
+ config.on_event :test_case_finished do |event|
11
+ location = event.test_case.location
12
+ id_path = "#{location.file}:#{location.line}"
13
+
14
+ ok =
15
+ if config.respond_to?(:strict)
16
+ event.result.ok?(strict: config.strict)
17
+ else
18
+ event.result.ok?
19
+ end
20
+
21
+ if ok
22
+ failed_locations.delete(id_path)
23
+ else
24
+ failed_locations[id_path] = location
25
+ end
26
+ end
27
+
28
+ config.on_event :test_run_finished do |_event|
29
+ paths = failed_locations.map do |id_path, location|
30
+ # TODO: Tracker currently only tracks by file_path. Track id_path and use the following:
31
+ # KnapsackPro::Config::Env.test_files_encrypted? ? location.file : id_path
32
+ location.file
33
+ end
34
+
35
+ paths.uniq.each do |path|
36
+ KnapsackPro.tracker.mark_failed(path)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'set'
5
+ require_relative '../../utils'
6
+
7
+ module KnapsackPro
8
+ module Formatters
9
+ module RSpec
10
+ class TimeTracker
11
+ ::RSpec::Core::Formatters.register self,
12
+ :example_group_started,
13
+ :example_started,
14
+ :example_finished,
15
+ :example_group_finished
16
+
17
+ attr_reader :output # RSpec < v3.10.2
18
+
19
+ def initialize(_output)
20
+ @output = StringIO.new
21
+ @time_each = nil
22
+ @time_all = nil
23
+ @time_all_by_group_id_path = Hash.new(0)
24
+ @group = {}
25
+ @paths = {}
26
+ @suite_started = now
27
+ @batched_scheduled_paths = []
28
+ @split_by_test_example_file_paths = Set.new
29
+ @current_batch_examples = []
30
+ end
31
+
32
+ def current_batch_failed_paths
33
+ examples_by_file_path = @current_batch_examples
34
+ .group_by { |example| file_path_for(example) }
35
+
36
+ paths =
37
+ examples_by_file_path.flat_map do |file_path, examples|
38
+ failed_id_paths = examples.filter { |example| example.execution_result.status.to_s == "failed" }.map(&:id)
39
+ next [] if failed_id_paths.none?
40
+ next file_path if KnapsackPro::Config::Env.test_files_encrypted?
41
+ # Other nodes may have run some examples from this file, it's not safe to compact.
42
+ next failed_id_paths if rspec_split_by_test_example?(file_path)
43
+ next file_path if failed_id_paths.size == examples.size
44
+
45
+ failed_id_paths
46
+ end
47
+
48
+ paths.map { |path| KnapsackPro::TestFileCleaner.clean(path) }
49
+ end
50
+
51
+ def schedule(paths)
52
+ @current_batch_examples = []
53
+ @batched_scheduled_paths << paths
54
+
55
+ paths.each do |path|
56
+ next unless KnapsackPro::Adapters::RSpecAdapter.id_path?(path)
57
+
58
+ file_path = KnapsackPro::Adapters::RSpecAdapter.parse_file_path(path)
59
+ @split_by_test_example_file_paths << KnapsackPro::TestFileCleaner.clean(file_path)
60
+ end
61
+ end
62
+
63
+ def example_group_started(notification)
64
+ record_time_all(notification.group.parent_groups[1], @time_all_by_group_id_path, @time_all)
65
+ @time_all = now
66
+ end
67
+
68
+ def example_started(notification)
69
+ record_time_all(notification.example.example_group, @time_all_by_group_id_path, @time_all)
70
+ @time_each = now
71
+ end
72
+
73
+ def example_finished(notification)
74
+ record_example(@group, notification.example, @time_each)
75
+ @time_all = now
76
+ end
77
+
78
+ def example_group_finished(notification)
79
+ record_time_all(notification.group, @time_all_by_group_id_path, @time_all)
80
+ @time_all = now
81
+ return unless top_level_group?(notification.group)
82
+
83
+ add_hooks_time(@group, @time_all_by_group_id_path)
84
+ @time_all_by_group_id_path = Hash.new(0)
85
+ merge!(@paths, @group)
86
+ @group = {}
87
+ end
88
+
89
+ def queue
90
+ recorded_paths = @paths.values.map do |example|
91
+ KnapsackPro::Adapters::RSpecAdapter.parse_file_path(example[:path])
92
+ end
93
+
94
+ missing = (@batched_scheduled_paths.flatten - recorded_paths).each_with_object({}) do |path, object|
95
+ object[path] = { path: path, time_execution: 0.0 }
96
+ end
97
+
98
+ merge!(@paths, missing)
99
+
100
+ @paths.values.map do |example|
101
+ example.transform_keys(&:to_s)
102
+ end
103
+ end
104
+
105
+ def batch
106
+ @paths.values.map do |example|
107
+ example.transform_keys(&:to_s)
108
+ end
109
+ end
110
+
111
+ def duration
112
+ now - @suite_started
113
+ end
114
+
115
+ def unexecuted_test_paths
116
+ pending_paths = @paths.values
117
+ .filter { |example| example[:time_execution] == 0.0 }
118
+ .map { |example| example[:path] }
119
+
120
+ not_run_paths = @batched_scheduled_paths.flatten -
121
+ @paths.values
122
+ .map { |example| example[:path] }
123
+
124
+ pending_paths + not_run_paths
125
+ end
126
+
127
+ private
128
+
129
+ def top_level_group?(group)
130
+ group.metadata[:parent_example_group].nil?
131
+ end
132
+
133
+ def add_hooks_time(group, time_all_by_group_id_path)
134
+ group.each do |_, example|
135
+ next if example[:time_execution] == 0.0
136
+
137
+ example[:time_execution] += time_all_by_group_id_path.reduce(0.0) do |sum, (group_id_path, time)|
138
+ # :path is a file path (a_spec.rb), sum any before/after(:all) in the file
139
+ next sum + time if group_id_path.start_with?(example[:path])
140
+ # :path is an id path (a_spec.rb[1:1]), sum any before/after(:all) above it
141
+ next sum + time if example[:path].start_with?(group_id_path[0..-2])
142
+ sum
143
+ end
144
+ end
145
+ end
146
+
147
+ def record_example(accumulator, example, started_at)
148
+ path = path_for(example)
149
+ return if path.nil?
150
+
151
+ @current_batch_examples << example
152
+
153
+ time_execution = time_execution_for(example, started_at)
154
+ if accumulator.key?(path)
155
+ accumulator[path][:time_execution] += time_execution
156
+ else
157
+ accumulator[path] = { path: path, time_execution: time_execution }
158
+ end
159
+ end
160
+
161
+ def record_time_all(group, time_all_by_group_id_path, time_all)
162
+ return unless group # above top level group
163
+
164
+ group_id_path = KnapsackPro::TestFileCleaner.clean(group.id)
165
+ time_all_by_group_id_path[group_id_path] += now - time_all
166
+ end
167
+
168
+ def path_for(example)
169
+ file_path = file_path_for(example)
170
+ return nil if file_path == ""
171
+
172
+ if rspec_split_by_test_example?(file_path)
173
+ KnapsackPro::TestFileCleaner.clean(example.id)
174
+ else
175
+ file_path
176
+ end
177
+ end
178
+
179
+ def rspec_split_by_test_example?(file_path)
180
+ @split_by_test_example_file_paths.include?(file_path)
181
+ end
182
+
183
+ def file_path_for(example)
184
+ KnapsackPro::TestFileCleaner.clean(KnapsackPro::Adapters::RSpecAdapter.file_path_for(example))
185
+ end
186
+
187
+ def time_execution_for(example, started_at)
188
+ if example.execution_result.status.to_s == "pending"
189
+ 0.0
190
+ else
191
+ (now - started_at).to_f
192
+ end
193
+ end
194
+
195
+ def merge!(h1, h2)
196
+ h2.each do |path, example|
197
+ if recorded = h1[path]
198
+ recorded[:time_execution] += example[:time_execution]
199
+ else
200
+ h1[path] = example
201
+ end
202
+ end
203
+ end
204
+
205
+ def now
206
+ KnapsackPro::Utils.time_now
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KnapsackPro
4
+ module Formatters
5
+ module RSpec
6
+ class TimeTrackerFetcher
7
+ def self.call
8
+ ::RSpec
9
+ .configuration
10
+ .formatters
11
+ .find { |f| f.class.to_s == "KnapsackPro::Formatters::RSpec::TimeTracker" }
12
+ end
13
+
14
+ def self.unexecuted_test_paths
15
+ time_tracker = call
16
+ return [] unless time_tracker
17
+ time_tracker.unexecuted_test_paths
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -6,7 +6,7 @@ module KnapsackPro
6
6
  class RSpecPure
7
7
  FAILURE_EXIT_CODE = 1
8
8
  FORMATTERS = [
9
- 'KnapsackPro::Formatters::TimeTracker',
9
+ 'KnapsackPro::Formatters::RSpec::TimeTracker',
10
10
  ]
11
11
 
12
12
  def add_knapsack_pro_formatters_to(spec_opts)
@@ -51,27 +51,27 @@ module KnapsackPro
51
51
  @current_batch = nil
52
52
  end
53
53
 
54
- def test_file_paths(can_initialize_queue, executed_test_files, time_tracker: nil)
54
+ def test_file_paths(can_initialize_queue, executed_test_files, failed_paths)
55
55
  @batch_index += 1
56
56
  return [] if @fallback_mode
57
57
 
58
- batch = pull_tests_from_queue(can_initialize_queue, time_tracker: time_tracker)
58
+ batch = pull_tests_from_queue(can_initialize_queue, failed_paths)
59
59
 
60
60
  return switch_to_fallback_mode(executed_test_files: executed_test_files) if batch.connection_failed?
61
61
  return normalize_test_files(batch.test_files) if batch.queue_exists?
62
62
 
63
63
  test_files_result = test_suite.calculate_test_files
64
64
 
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
+ return try_initializing_queue(test_files_result.test_files.map { |test| test.fetch("path") }, failed_paths) if test_files_result.quick?
66
66
 
67
67
  # The tests to run were found slowly. By that time, the queue could have already been initialized by another CI node.
68
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).
69
- batch = pull_tests_from_queue(can_initialize_queue, time_tracker: time_tracker)
69
+ batch = pull_tests_from_queue(can_initialize_queue, failed_paths)
70
70
 
71
71
  return switch_to_fallback_mode(executed_test_files: executed_test_files) if batch.connection_failed?
72
72
  return normalize_test_files(batch.test_files) if batch.queue_exists?
73
73
 
74
- try_initializing_queue(test_files_result.test_files.map { |test| test.fetch("path") }, time_tracker: time_tracker)
74
+ try_initializing_queue(test_files_result.test_files.map { |test| test.fetch("path") }, failed_paths)
75
75
  end
76
76
 
77
77
  private
@@ -87,16 +87,15 @@ module KnapsackPro
87
87
  KnapsackPro::TestFilePresenter.paths(decrypted_test_files)
88
88
  end
89
89
 
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)
90
+ def build_action(can_initialize_queue:, attempt_connect_to_queue:, failed_paths:, paths: nil)
92
91
  if can_initialize_queue && !attempt_connect_to_queue
93
92
  raise 'Test files are required when initializing a new queue.' if paths.nil?
94
93
  paths = KnapsackPro::Crypto::Encryptor.paths(paths)
95
94
  end
96
95
 
97
96
  failed_paths =
98
- if time_tracker
99
- KnapsackPro::Crypto::Encryptor.paths(time_tracker.current_batch_failed_paths)
97
+ if failed_paths
98
+ KnapsackPro::Crypto::Encryptor.paths(failed_paths)
100
99
  elsif @batch_index.positive?
101
100
  KnapsackPro::Crypto::Encryptor.paths(@current_batch.paths)
102
101
  end
@@ -116,22 +115,22 @@ module KnapsackPro
116
115
  )
117
116
  end
118
117
 
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)
118
+ def pull_tests_from_queue(can_initialize_queue, failed_paths)
119
+ action = build_action(can_initialize_queue: can_initialize_queue, attempt_connect_to_queue: can_initialize_queue, failed_paths: failed_paths)
121
120
  connection = KnapsackPro::Client::Connection.new(action)
122
121
  response = connection.call
123
122
  @current_batch = Batch.new(connection, response)
124
123
  end
125
124
 
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)
125
+ def initialize_queue(paths, failed_paths)
126
+ action = build_action(can_initialize_queue: true, attempt_connect_to_queue: false, paths: paths, failed_paths: failed_paths)
128
127
  connection = KnapsackPro::Client::Connection.new(action)
129
128
  response = connection.call
130
129
  @current_batch = Batch.new(connection, response)
131
130
  end
132
131
 
133
- def try_initializing_queue(paths, time_tracker: nil)
134
- result = initialize_queue(paths, time_tracker: time_tracker)
132
+ def try_initializing_queue(paths, failed_paths)
133
+ result = initialize_queue(paths, failed_paths)
135
134
  return switch_to_fallback_mode(executed_test_files: []) if result.connection_failed?
136
135
 
137
136
  normalize_test_files(result.test_files)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module KnapsackPro
4
4
  class Report
5
- def self.save(test_files = nil)
5
+ def self.save(test_files = nil, failed_paths = nil)
6
6
  if test_files.nil?
7
7
  test_files = KnapsackPro.tracker.to_a
8
8
  end
@@ -11,31 +11,39 @@ module KnapsackPro
11
11
  KnapsackPro.logger.info("No tests were executed because the test queue is empty.")
12
12
  end
13
13
 
14
- acknowledge_single_batch(test_files)
14
+ acknowledge_single_batch(test_files, failed_paths)
15
15
  create_build_subset(test_files)
16
16
  end
17
17
 
18
18
  def self.save_subset_queue_to_file
19
19
  test_files = KnapsackPro.tracker.to_a
20
-
21
20
  subset_queue_id = KnapsackPro::Config::Env.subset_queue_id
22
-
23
21
  KnapsackPro::Config::TempFiles.ensure_temp_directory_exists!
24
22
  FileUtils.mkdir_p(queue_path)
25
-
26
23
  subset_queue_file_name = "#{subset_queue_id}.json"
27
24
  report_path = File.join(queue_path, subset_queue_file_name)
28
25
  report_json = JSON.pretty_generate(test_files)
29
-
30
26
  File.open(report_path, 'w+') do |f|
31
27
  f.write(report_json)
32
28
  end
29
+
30
+ failed_paths = KnapsackPro.tracker.current_batch_failed_paths
31
+ json = JSON.pretty_generate(failed_paths)
32
+ File.open(current_batch_failed_paths_path, 'w+') do |f|
33
+ f.write(json)
34
+ end
35
+ end
36
+
37
+ def self.current_batch_failed_paths
38
+ JSON.parse(File.read(current_batch_failed_paths_path))
33
39
  end
34
40
 
35
41
  def self.save_node_queue_to_api(test_files = nil)
36
42
  if test_files.nil?
37
43
  test_files = []
38
44
  Dir.glob("#{queue_path}/*.json").each do |file|
45
+ next if file == current_batch_failed_paths_path
46
+
39
47
  report = JSON.parse(File.read(file))
40
48
  test_files += report
41
49
  end
@@ -59,11 +67,10 @@ module KnapsackPro
59
67
  create_build_subset(test_files)
60
68
  end
61
69
 
62
- # TODO: Add support for Retry-only-Failures
63
- def self.acknowledge_single_batch(test_files)
70
+ def self.acknowledge_single_batch(test_files, failed_paths)
64
71
  repository_adapter = KnapsackPro::RepositoryAdapterInitiator.call
65
72
  paths = KnapsackPro::Utils.unsymbolize(test_files).map { |test_file| test_file.fetch('path') }
66
- failed_paths = KnapsackPro::Crypto::Encryptor.paths(paths)
73
+ failed_paths = paths if failed_paths.nil?
67
74
  action = KnapsackPro::Client::API::V2::Queues.queue(
68
75
  attempt_connect_to_queue: false,
69
76
  batch_id: ENV['KNAPSACK_PRO_BATCH_ID'], # nil in Fallback Mode
@@ -71,7 +78,7 @@ module KnapsackPro
71
78
  branch: KnapsackPro::Crypto::BranchEncryptor.call(repository_adapter.branch),
72
79
  can_initialize_queue: false,
73
80
  commit_hash: repository_adapter.commit_hash,
74
- failed_paths: failed_paths,
81
+ failed_paths: KnapsackPro::Crypto::Encryptor.paths(failed_paths),
75
82
  node_total: KnapsackPro::Config::Env.ci_node_total,
76
83
  node_index: KnapsackPro::Config::Env.ci_node_index,
77
84
  node_uuid: ENV.fetch('KNAPSACK_PRO_NODE_UUID'),
@@ -115,5 +122,9 @@ module KnapsackPro
115
122
  def self.queue_path
116
123
  "#{KnapsackPro::Config::TempFiles::TEMP_DIRECTORY_PATH}/queue/#{KnapsackPro::Config::Env.queue_id}"
117
124
  end
125
+
126
+ def self.current_batch_failed_paths_path
127
+ File.join(KnapsackPro::Report.queue_path, "current_batch_failed_paths.json")
128
+ end
118
129
  end
119
130
  end
@@ -26,7 +26,7 @@ module KnapsackPro
26
26
 
27
27
  def test_files_to_execute_exist?
28
28
  if test_file_paths.empty?
29
- KnapsackPro.logger.info("Knapsack Pro API returned no test files to execute for the node this time. The reason might be that you changed recently a number of total nodes or you removed some test files. Please create a new commit to get a better test suite split next time.")
29
+ KnapsackPro.logger.info("No tests were executed because the test queue is empty.")
30
30
  false
31
31
  else
32
32
  true
@@ -24,8 +24,19 @@ module KnapsackPro
24
24
  Rake::Task[task_name].clear
25
25
  end
26
26
 
27
+ require "cucumber/cli/main"
28
+ require "shellwords"
29
+ formats = Cucumber::Cli::Main.new(Shellwords.split(args || "")).configuration.formats
30
+ formats << ["KnapsackPro::Formatters::Cucumber::FailuresTracker", {}, File::NULL]
31
+ formats_arg = formats.flat_map do |name, options, output|
32
+ format = [name, *options.map { |key, value| "#{key}=#{value}" }].join(",")
33
+ formats = ["--format", format]
34
+ formats.concat(["--out", output]) if output.is_a?(String)
35
+ formats
36
+ end
37
+
27
38
  ::Cucumber::Rake::Task.new(task_name) do |t|
28
- t.cucumber_opts = "#{args} --require #{runner.test_dir} -- #{runner.stringify_test_file_paths}"
39
+ t.cucumber_opts = "#{args} --require #{runner.test_dir} #{formats_arg.shelljoin} -- #{runner.stringify_test_file_paths}"
29
40
  end
30
41
  Rake::Task[task_name].invoke
31
42
  end
@@ -25,8 +25,8 @@ module KnapsackPro
25
25
  def test_file_paths(args)
26
26
  can_initialize_queue = args.fetch(:can_initialize_queue)
27
27
  executed_test_files = args.fetch(:executed_test_files)
28
- time_tracker = args.fetch(:time_tracker, nil)
29
- allocator.test_file_paths(can_initialize_queue, executed_test_files, time_tracker: time_tracker)
28
+ failed_paths = args.fetch(:failed_paths)
29
+ allocator.test_file_paths(can_initialize_queue, executed_test_files, failed_paths)
30
30
  end
31
31
 
32
32
  def test_dir
@@ -21,7 +21,7 @@ module KnapsackPro
21
21
  can_initialize_queue: true,
22
22
  args: args,
23
23
  exitstatus: 0,
24
- node_test_file_paths: [],
24
+ node_test_file_paths: []
25
25
  }
26
26
  while accumulator[:status] == :next
27
27
  handle_signal!
@@ -43,7 +43,8 @@ module KnapsackPro
43
43
 
44
44
  test_file_paths = runner.test_file_paths(
45
45
  can_initialize_queue: can_initialize_queue,
46
- executed_test_files: node_test_file_paths
46
+ executed_test_files: node_test_file_paths,
47
+ failed_paths: can_initialize_queue ? [] : KnapsackPro::Report.current_batch_failed_paths
47
48
  )
48
49
 
49
50
  if test_file_paths.empty?
@@ -83,7 +84,7 @@ module KnapsackPro
83
84
  can_initialize_queue: false,
84
85
  args: args,
85
86
  exitstatus: exitstatus,
86
- node_test_file_paths: node_test_file_paths,
87
+ node_test_file_paths: node_test_file_paths
87
88
  }
88
89
  end
89
90
  end
@@ -91,11 +92,22 @@ module KnapsackPro
91
92
  private
92
93
 
93
94
  def self.cucumber_run(runner, test_file_paths, args)
95
+ require "cucumber/cli/main"
96
+ require "shellwords"
97
+ formats = Cucumber::Cli::Main.new(Shellwords.split(args || "")).configuration.formats
98
+ formats << ["KnapsackPro::Formatters::Cucumber::FailuresTracker", {}, File::NULL]
99
+ formats_arg = formats.flat_map do |name, options, output|
100
+ format = [name, *options.map { |key, value| "#{key}=#{value}" }].join(",")
101
+ formats = ["--format", format]
102
+ formats.concat(["--out", output]) if output.is_a?(String)
103
+ formats
104
+ end
105
+
94
106
  stringify_test_file_paths = KnapsackPro::TestFilePresenter.stringify_paths(test_file_paths)
95
107
 
96
108
  cmd = [
97
109
  KnapsackPro::Config::Env.cucumber_queue_prefix,
98
- %Q[cucumber #{args} --require #{runner.test_dir} -- #{stringify_test_file_paths}]
110
+ %Q[cucumber #{args} --require #{runner.test_dir} #{formats_arg.shelljoin} -- #{stringify_test_file_paths}]
99
111
  ].join(' ')
100
112
 
101
113
  Kernel.system(cmd)
@@ -54,7 +54,8 @@ module KnapsackPro
54
54
 
55
55
  test_file_paths = runner.test_file_paths(
56
56
  can_initialize_queue: can_initialize_queue,
57
- executed_test_files: node_test_file_paths
57
+ executed_test_files: node_test_file_paths,
58
+ failed_paths: can_initialize_queue ? [] : KnapsackPro::Report.current_batch_failed_paths
58
59
  )
59
60
 
60
61
  if test_file_paths.empty?
@@ -86,6 +87,7 @@ module KnapsackPro
86
87
 
87
88
  KnapsackPro::Hooks::Queue.call_after_subset_queue
88
89
 
90
+ KnapsackPro.tracker.flush_staged_tests
89
91
  KnapsackPro::Report.save_subset_queue_to_file
90
92
 
91
93
  return {
@@ -7,8 +7,8 @@ module KnapsackPro
7
7
  def self.run(args, stream_error = $stderr, stream_out = $stdout)
8
8
  require 'rspec/core'
9
9
  require_relative '../../extensions/rspec_extension'
10
- require_relative '../../formatters/time_tracker'
11
- require_relative '../../formatters/time_tracker_fetcher'
10
+ require_relative '../../formatters/rspec/time_tracker'
11
+ require_relative '../../formatters/rspec/time_tracker_fetcher'
12
12
 
13
13
  KnapsackPro::Extensions::RSpecExtension.setup!
14
14
 
@@ -163,7 +163,7 @@ module KnapsackPro
163
163
 
164
164
  log_rspec_queue_command
165
165
 
166
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
166
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
167
167
  KnapsackPro::Report.save_node_queue_to_api(time_tracker&.queue)
168
168
 
169
169
  Kernel.exit(exit_code)
@@ -178,11 +178,11 @@ module KnapsackPro
178
178
  end
179
179
 
180
180
  def pull_tests_from_queue(can_initialize_queue: false)
181
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
181
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
182
182
  test_file_paths = test_file_paths(
183
183
  can_initialize_queue: can_initialize_queue,
184
184
  executed_test_files: @node_test_file_paths,
185
- time_tracker: time_tracker
185
+ failed_paths: time_tracker.current_batch_failed_paths
186
186
  )
187
187
  @node_test_file_paths += test_file_paths
188
188
  time_tracker.schedule(test_file_paths)
@@ -225,7 +225,7 @@ module KnapsackPro
225
225
  end
226
226
 
227
227
  def unexecuted_test_paths
228
- KnapsackPro::Formatters::TimeTrackerFetcher.unexecuted_test_paths
228
+ KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.unexecuted_test_paths
229
229
  end
230
230
  end
231
231
  end
@@ -39,7 +39,7 @@ module KnapsackPro
39
39
 
40
40
  # Use RSpec::Core::ConfigurationOptions to respect external configurations like .rspec
41
41
  def self.formatters
42
- require_relative '../formatters/time_tracker'
42
+ require_relative '../formatters/rspec/time_tracker'
43
43
 
44
44
  formatters = ::RSpec::Core::ConfigurationOptions
45
45
  .new([])
@@ -51,7 +51,7 @@ module KnapsackPro
51
51
  arg
52
52
  end
53
53
  formatters = ['--format progress'] if formatters.empty?
54
- formatters += ["--format #{KnapsackPro::Formatters::TimeTracker}"]
54
+ formatters += ["--format #{KnapsackPro::Formatters::RSpec::TimeTracker}"]
55
55
  formatters.join(' ')
56
56
  end
57
57
  end
@@ -6,7 +6,7 @@ module KnapsackPro
6
6
 
7
7
  DEFAULT_TEST_FILE_TIME = 0.0 # seconds
8
8
 
9
- attr_reader :global_time, :test_files_with_time, :prerun_tests_loaded
9
+ attr_reader :global_time, :test_files_with_time, :prerun_tests_loaded, :current_batch_failed_paths
10
10
  attr_writer :current_test_path
11
11
 
12
12
  def initialize
@@ -74,20 +74,48 @@ module KnapsackPro
74
74
  load_prerun_tests unless prerun_tests_loaded
75
75
 
76
76
  test_files = []
77
+
77
78
  @test_files_with_time.each do |path, hash|
78
79
  test_files << {
79
80
  path: path,
80
81
  time_execution: hash[:time_execution]
81
82
  }
82
83
  end
84
+
83
85
  test_files
84
86
  end
85
87
 
88
+ def mark_failed(path)
89
+ @current_batch_failed_paths << KnapsackPro::TestFileCleaner.clean(path)
90
+ end
91
+
92
+ # Thread-safe: first stage then flush_staged_tests
93
+ def stage(path, execution_time, failed: false)
94
+ @staged_tests << [KnapsackPro::TestFileCleaner.clean(path), execution_time, failed]
95
+ end
96
+
97
+ # Thread-safe: first stage then flush_staged_tests
98
+ def flush_staged_tests
99
+ loop do
100
+ begin
101
+ path, execution_time, failed = @staged_tests.pop(true)
102
+ rescue ThreadError
103
+ break
104
+ end
105
+
106
+ update_global_time(execution_time)
107
+ update_test_file_time(execution_time, path)
108
+ mark_failed(path) if failed
109
+ end
110
+ end
111
+
86
112
  private
87
113
 
88
114
  def set_defaults
115
+ @staged_tests = ::Queue.new
89
116
  @global_time = 0
90
117
  @test_files_with_time = {}
118
+ @current_batch_failed_paths = []
91
119
  @current_test_path = nil
92
120
  @prerun_tests_loaded = false
93
121
  end
@@ -134,13 +162,13 @@ module KnapsackPro
134
162
  @global_time += execution_time
135
163
  end
136
164
 
137
- def update_test_file_time(execution_time)
138
- @test_files_with_time[current_test_path] ||= {
165
+ def update_test_file_time(execution_time, path = current_test_path)
166
+ @test_files_with_time[path] ||= {
139
167
  time_execution: 0,
140
168
  measured_time: false,
141
169
  }
142
170
 
143
- hash = @test_files_with_time[current_test_path]
171
+ hash = @test_files_with_time[path]
144
172
 
145
173
  if hash[:measured_time]
146
174
  hash[:time_execution] += execution_time
@@ -149,7 +177,7 @@ module KnapsackPro
149
177
  hash[:measured_time] = true
150
178
  end
151
179
 
152
- @test_files_with_time[current_test_path] = hash
180
+ @test_files_with_time[path] = hash
153
181
  end
154
182
 
155
183
  def now_without_mock_time
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KnapsackPro
4
- VERSION = '11.0.1'
4
+ VERSION = '11.1.0'
5
5
  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: 11.0.1
4
+ version: 11.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ArturT
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: '8'
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
67
+ version: '8'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: minitest
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -242,9 +242,10 @@ files:
242
242
  - lib/knapsack_pro/crypto/digestor.rb
243
243
  - lib/knapsack_pro/crypto/encryptor.rb
244
244
  - lib/knapsack_pro/extensions/rspec_extension.rb
245
+ - lib/knapsack_pro/formatters/cucumber/failures_tracker.rb
246
+ - lib/knapsack_pro/formatters/rspec/time_tracker.rb
247
+ - lib/knapsack_pro/formatters/rspec/time_tracker_fetcher.rb
245
248
  - lib/knapsack_pro/formatters/rspec_json_formatter.rb
246
- - lib/knapsack_pro/formatters/time_tracker.rb
247
- - lib/knapsack_pro/formatters/time_tracker_fetcher.rb
248
249
  - lib/knapsack_pro/hooks/queue.rb
249
250
  - lib/knapsack_pro/mask_string.rb
250
251
  - lib/knapsack_pro/presenter.rb
@@ -1,209 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'stringio'
4
- require 'set'
5
- require_relative '../utils'
6
-
7
- module KnapsackPro
8
- module Formatters
9
- class TimeTracker
10
- ::RSpec::Core::Formatters.register self,
11
- :example_group_started,
12
- :example_started,
13
- :example_finished,
14
- :example_group_finished
15
-
16
- attr_reader :output # RSpec < v3.10.2
17
-
18
- def initialize(_output)
19
- @output = StringIO.new
20
- @time_each = nil
21
- @time_all = nil
22
- @time_all_by_group_id_path = Hash.new(0)
23
- @group = {}
24
- @paths = {}
25
- @suite_started = now
26
- @batched_scheduled_paths = []
27
- @split_by_test_example_file_paths = Set.new
28
- @current_batch_examples = []
29
- end
30
-
31
- def current_batch_failed_paths
32
- examples_by_file_path = @current_batch_examples
33
- .group_by { |example| file_path_for(example) }
34
-
35
- paths =
36
- examples_by_file_path.flat_map do |file_path, examples|
37
- failed_id_paths = examples.filter { |example| example.execution_result.status.to_s == "failed" }.map(&:id)
38
- next [] if failed_id_paths.none?
39
- next file_path if KnapsackPro::Config::Env.test_files_encrypted?
40
- # Other nodes may have run some examples from this file, it's not safe to compact.
41
- next failed_id_paths if rspec_split_by_test_example?(file_path)
42
- next file_path if failed_id_paths.size == examples.size
43
-
44
- failed_id_paths
45
- end
46
-
47
- paths.map { |path| KnapsackPro::TestFileCleaner.clean(path) }
48
- end
49
-
50
- def schedule(paths)
51
- @current_batch_examples = []
52
- @batched_scheduled_paths << paths
53
-
54
- paths.each do |path|
55
- next unless KnapsackPro::Adapters::RSpecAdapter.id_path?(path)
56
-
57
- file_path = KnapsackPro::Adapters::RSpecAdapter.parse_file_path(path)
58
- @split_by_test_example_file_paths << KnapsackPro::TestFileCleaner.clean(file_path)
59
- end
60
- end
61
-
62
- def example_group_started(notification)
63
- record_time_all(notification.group.parent_groups[1], @time_all_by_group_id_path, @time_all)
64
- @time_all = now
65
- end
66
-
67
- def example_started(notification)
68
- record_time_all(notification.example.example_group, @time_all_by_group_id_path, @time_all)
69
- @time_each = now
70
- end
71
-
72
- def example_finished(notification)
73
- record_example(@group, notification.example, @time_each)
74
- @time_all = now
75
- end
76
-
77
- def example_group_finished(notification)
78
- record_time_all(notification.group, @time_all_by_group_id_path, @time_all)
79
- @time_all = now
80
- return unless top_level_group?(notification.group)
81
-
82
- add_hooks_time(@group, @time_all_by_group_id_path)
83
- @time_all_by_group_id_path = Hash.new(0)
84
- merge!(@paths, @group)
85
- @group = {}
86
- end
87
-
88
- def queue
89
- recorded_paths = @paths.values.map do |example|
90
- KnapsackPro::Adapters::RSpecAdapter.parse_file_path(example[:path])
91
- end
92
-
93
- missing = (@batched_scheduled_paths.flatten - recorded_paths).each_with_object({}) do |path, object|
94
- object[path] = { path: path, time_execution: 0.0 }
95
- end
96
-
97
- merge!(@paths, missing)
98
-
99
- @paths.values.map do |example|
100
- example.transform_keys(&:to_s)
101
- end
102
- end
103
-
104
- def batch
105
- @paths.values.map do |example|
106
- example.transform_keys(&:to_s)
107
- end
108
- end
109
-
110
- def duration
111
- now - @suite_started
112
- end
113
-
114
- def unexecuted_test_paths
115
- pending_paths = @paths.values
116
- .filter { |example| example[:time_execution] == 0.0 }
117
- .map { |example| example[:path] }
118
-
119
- not_run_paths = @batched_scheduled_paths.flatten -
120
- @paths.values
121
- .map { |example| example[:path] }
122
-
123
- pending_paths + not_run_paths
124
- end
125
-
126
- private
127
-
128
- def top_level_group?(group)
129
- group.metadata[:parent_example_group].nil?
130
- end
131
-
132
- def add_hooks_time(group, time_all_by_group_id_path)
133
- group.each do |_, example|
134
- next if example[:time_execution] == 0.0
135
-
136
- example[:time_execution] += time_all_by_group_id_path.reduce(0.0) do |sum, (group_id_path, time)|
137
- # :path is a file path (a_spec.rb), sum any before/after(:all) in the file
138
- next sum + time if group_id_path.start_with?(example[:path])
139
- # :path is an id path (a_spec.rb[1:1]), sum any before/after(:all) above it
140
- next sum + time if example[:path].start_with?(group_id_path[0..-2])
141
- sum
142
- end
143
- end
144
- end
145
-
146
- def record_example(accumulator, example, started_at)
147
- path = path_for(example)
148
- return if path.nil?
149
-
150
- @current_batch_examples << example
151
-
152
- time_execution = time_execution_for(example, started_at)
153
- if accumulator.key?(path)
154
- accumulator[path][:time_execution] += time_execution
155
- else
156
- accumulator[path] = { path: path, time_execution: time_execution }
157
- end
158
- end
159
-
160
- def record_time_all(group, time_all_by_group_id_path, time_all)
161
- return unless group # above top level group
162
-
163
- group_id_path = KnapsackPro::TestFileCleaner.clean(group.id)
164
- time_all_by_group_id_path[group_id_path] += now - time_all
165
- end
166
-
167
- def path_for(example)
168
- file_path = file_path_for(example)
169
- return nil if file_path == ""
170
-
171
- if rspec_split_by_test_example?(file_path)
172
- KnapsackPro::TestFileCleaner.clean(example.id)
173
- else
174
- file_path
175
- end
176
- end
177
-
178
- def rspec_split_by_test_example?(file_path)
179
- @split_by_test_example_file_paths.include?(file_path)
180
- end
181
-
182
- def file_path_for(example)
183
- KnapsackPro::TestFileCleaner.clean(KnapsackPro::Adapters::RSpecAdapter.file_path_for(example))
184
- end
185
-
186
- def time_execution_for(example, started_at)
187
- if example.execution_result.status.to_s == "pending"
188
- 0.0
189
- else
190
- (now - started_at).to_f
191
- end
192
- end
193
-
194
- def merge!(h1, h2)
195
- h2.each do |path, example|
196
- if recorded = h1[path]
197
- recorded[:time_execution] += example[:time_execution]
198
- else
199
- h1[path] = example
200
- end
201
- end
202
- end
203
-
204
- def now
205
- KnapsackPro::Utils.time_now
206
- end
207
- end
208
- end
209
- end
@@ -1,20 +0,0 @@
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_paths
14
- time_tracker = call
15
- return [] unless time_tracker
16
- time_tracker.unexecuted_test_paths
17
- end
18
- end
19
- end
20
- end