knapsack_pro 11.0.0 → 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: 228fc40233719e372291f82fe1c6d5c25ed0396ae12fb3e0f28edac71e69754d
4
- data.tar.gz: ce7cb564e98830745734efd59850e50c43c0abd49ed6131ecd4392801dd4758e
3
+ metadata.gz: 4048ec74df7fa6a88b8f44df5afe764f841c2f8823ae08964e03c6ae87e5639c
4
+ data.tar.gz: 312475082e64ade4db5a8b32160b9ddb294878140149140f1eb6921e08c4d242
5
5
  SHA512:
6
- metadata.gz: ff6be1076a78549e3f4a8f57362ec560cd482ddd332513280bf67155248e7066478291014cd6f166c904344be81d520f374fdb565884d4540f2344b8577739d1
7
- data.tar.gz: 8131b135fa3f86574b77ae3576ed538c88c871073c1d9756f283ad88245a84ed50dd55ab203ab330a39e578e5aeedf3e700051f8bab3f622611db9380926e42a
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,13 +1,15 @@
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
7
7
  class RSpecAdapter < BaseAdapter
8
8
  TEST_DIR_PATTERN = 'spec/**{,/*/**}/*_spec.rb'
9
9
  # https://github.com/rspec/rspec/blob/86b5e4218eece4c1913fe9aad24c0a96d8bc9f40/rspec-core/lib/rspec/core/example.rb#L122
10
- REGEX = /\A(.*?)(?:\[([\d\s:,]+)\])?\z/.freeze
10
+ ID_REGEX = /\[[\d\s:,]+\]\z/.freeze
11
+ ID_PATH_PREFIX = '['
12
+ ID_PATH_SUFFIX = ']'
11
13
 
12
14
  def self.split_by_test_cases_enabled?
13
15
  return false unless KnapsackPro::Config::Env.rspec_split_by_test_examples?
@@ -57,28 +59,33 @@ module KnapsackPro
57
59
  end
58
60
 
59
61
  def self.file_path_for(example)
60
- [
61
- -> { parse_file_path(example.id) },
62
- -> { example.metadata[:file_path] },
63
- -> { example.metadata[:example_group][:file_path] },
64
- -> { top_level_group(example)[:file_path] },
65
- ]
66
- .each do |path|
67
- p = path.call
68
- return p if p.include?('_spec.rb') || p.include?('.feature')
69
- end
62
+ p = parse_file_path(example.id)
63
+ return p if p.include?('_spec.rb') || p.include?('.feature')
64
+
65
+ p = example.metadata[:file_path]
66
+ return p if p.include?('_spec.rb') || p.include?('.feature')
67
+
68
+ p = example.metadata[:example_group][:file_path]
69
+ return p if p.include?('_spec.rb') || p.include?('.feature')
70
70
 
71
- return ''
71
+ p = top_level_group(example)[:file_path]
72
+ return p if p.include?('_spec.rb') || p.include?('.feature')
73
+
74
+ ''
72
75
  end
73
76
 
74
77
  def self.parse_file_path(path)
75
- file, _id = path.match(REGEX).captures
76
- file
78
+ return path unless path.end_with?(ID_PATH_SUFFIX)
79
+
80
+ index = path.rindex(ID_PATH_PREFIX)
81
+ return path if index.nil?
82
+ return path unless ID_REGEX.match?(path, index)
83
+
84
+ path[0, index]
77
85
  end
78
86
 
79
87
  def self.id_path?(path)
80
- _file, id = path.match(REGEX).captures
81
- !id.nil?
88
+ path.end_with?(ID_PATH_SUFFIX) && ID_REGEX.match?(path)
82
89
  end
83
90
 
84
91
  def self.concat_test_files(test_files, id_paths)
@@ -129,7 +136,7 @@ module KnapsackPro
129
136
  def log_tests_duration
130
137
  ::RSpec.configure do |config|
131
138
  config.append_after(:suite) do
132
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
139
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
133
140
  formatted = KnapsackPro::Presenter.global_time(time_tracker.duration)
134
141
  KnapsackPro.logger.debug(formatted)
135
142
  end
@@ -141,7 +148,7 @@ module KnapsackPro
141
148
 
142
149
  ::RSpec.configure do |config|
143
150
  config.append_before(:suite) do
144
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
151
+ time_tracker = KnapsackPro::Formatters::RSpec::TimeTrackerFetcher.call
145
152
  time_tracker.schedule(KnapsackPro::Adapters::RSpecAdapter.scheduled_paths)
146
153
  end
147
154
  end
@@ -150,8 +157,8 @@ module KnapsackPro
150
157
  def bind_save_report
151
158
  ::RSpec.configure do |config|
152
159
  config.after(:suite) do
153
- time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
154
- 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)
155
162
  end
156
163
  end
157
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.0'
4
+ VERSION = '11.1.0'
5
5
  end
data/lib/knapsack_pro.rb CHANGED
@@ -110,17 +110,17 @@ module KnapsackPro
110
110
  end
111
111
 
112
112
  def logger
113
- if KnapsackPro::Config::Env.log_dir
114
- default_logger = Logger.new("#{KnapsackPro::Config::Env.log_dir}/knapsack_pro_node_#{KnapsackPro::Config::Env.ci_node_index}.log")
115
- default_logger.level = KnapsackPro::Config::Env.log_level
116
- self.logger = default_logger
117
- end
113
+ return @logger if @logger
118
114
 
119
- unless @logger
120
- default_logger = ::Logger.new(stdout)
121
- default_logger.level = KnapsackPro::Config::Env.log_level
122
- self.logger = default_logger
123
- end
115
+ log_dir = KnapsackPro::Config::Env.log_dir
116
+ default_logger =
117
+ if log_dir
118
+ ::Logger.new("#{log_dir}/knapsack_pro_node_#{KnapsackPro::Config::Env.ci_node_index}.log")
119
+ else
120
+ ::Logger.new(stdout)
121
+ end
122
+ default_logger.level = KnapsackPro::Config::Env.log_level
123
+ self.logger = default_logger
124
124
 
125
125
  @logger
126
126
  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.0
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,206 +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
- @paths = 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).values.map do |example|
98
- example.transform_keys(&:to_s)
99
- end
100
- end
101
-
102
- def batch
103
- @paths.values.map do |example|
104
- example.transform_keys(&:to_s)
105
- end
106
- end
107
-
108
- def duration
109
- now - @suite_started
110
- end
111
-
112
- def unexecuted_test_paths
113
- pending_paths = @paths.values
114
- .filter { |example| example[:time_execution] == 0.0 }
115
- .map { |example| example[:path] }
116
-
117
- not_run_paths = @batched_scheduled_paths.flatten -
118
- @paths.values
119
- .map { |example| example[:path] }
120
-
121
- pending_paths + not_run_paths
122
- end
123
-
124
- private
125
-
126
- def top_level_group?(group)
127
- group.metadata[:parent_example_group].nil?
128
- end
129
-
130
- def add_hooks_time(group, time_all_by_group_id_path)
131
- group.each do |_, example|
132
- next if example[:time_execution] == 0.0
133
-
134
- example[:time_execution] += time_all_by_group_id_path.reduce(0.0) do |sum, (group_id_path, time)|
135
- # :path is a file path (a_spec.rb), sum any before/after(:all) in the file
136
- next sum + time if group_id_path.start_with?(example[:path])
137
- # :path is an id path (a_spec.rb[1:1]), sum any before/after(:all) above it
138
- next sum + time if example[:path].start_with?(group_id_path[0..-2])
139
- sum
140
- end
141
- end
142
- end
143
-
144
- def record_example(accumulator, example, started_at)
145
- path = path_for(example)
146
- return if path.nil?
147
-
148
- @current_batch_examples << example
149
-
150
- time_execution = time_execution_for(example, started_at)
151
- if accumulator.key?(path)
152
- accumulator[path][:time_execution] += time_execution
153
- else
154
- accumulator[path] = { path: path, time_execution: time_execution }
155
- end
156
- end
157
-
158
- def record_time_all(group, time_all_by_group_id_path, time_all)
159
- return unless group # above top level group
160
-
161
- group_id_path = KnapsackPro::TestFileCleaner.clean(group.id)
162
- time_all_by_group_id_path[group_id_path] += now - time_all
163
- end
164
-
165
- def path_for(example)
166
- file_path = file_path_for(example)
167
- return nil if file_path == ""
168
-
169
- if rspec_split_by_test_example?(file_path)
170
- KnapsackPro::TestFileCleaner.clean(example.id)
171
- else
172
- file_path
173
- end
174
- end
175
-
176
- def rspec_split_by_test_example?(file_path)
177
- @split_by_test_example_file_paths.include?(file_path)
178
- end
179
-
180
- def file_path_for(example)
181
- KnapsackPro::TestFileCleaner.clean(KnapsackPro::Adapters::RSpecAdapter.file_path_for(example))
182
- end
183
-
184
- def time_execution_for(example, started_at)
185
- if example.execution_result.status.to_s == "pending"
186
- 0.0
187
- else
188
- (now - started_at).to_f
189
- end
190
- end
191
-
192
- def merge(h1, h2)
193
- h1.merge(h2) do |key, v1, v2|
194
- {
195
- path: key,
196
- time_execution: v1[:time_execution] + v2[:time_execution]
197
- }
198
- end
199
- end
200
-
201
- def now
202
- KnapsackPro::Utils.time_now
203
- end
204
- end
205
- end
206
- 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