ci-queue 0.82.0 → 0.83.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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +87 -0
- data/lib/ci/queue/class_resolver.rb +38 -0
- data/lib/ci/queue/configuration.rb +62 -1
- data/lib/ci/queue/file_loader.rb +101 -0
- data/lib/ci/queue/queue_entry.rb +56 -0
- data/lib/ci/queue/redis/_entry_helpers.lua +10 -0
- data/lib/ci/queue/redis/acknowledge.lua +10 -7
- data/lib/ci/queue/redis/base.rb +33 -7
- data/lib/ci/queue/redis/build_record.rb +12 -0
- data/lib/ci/queue/redis/heartbeat.lua +9 -4
- data/lib/ci/queue/redis/monitor.rb +19 -5
- data/lib/ci/queue/redis/requeue.lua +19 -11
- data/lib/ci/queue/redis/reserve.lua +47 -8
- data/lib/ci/queue/redis/reserve_lost.lua +5 -1
- data/lib/ci/queue/redis/supervisor.rb +3 -3
- data/lib/ci/queue/redis/worker.rb +216 -23
- data/lib/ci/queue/version.rb +1 -1
- data/lib/ci/queue.rb +27 -0
- data/lib/minitest/queue/junit_reporter.rb +2 -2
- data/lib/minitest/queue/lazy_entry_resolver.rb +55 -0
- data/lib/minitest/queue/lazy_test_discovery.rb +169 -0
- data/lib/minitest/queue/local_requeue_reporter.rb +11 -0
- data/lib/minitest/queue/order_reporter.rb +9 -2
- data/lib/minitest/queue/queue_population_strategy.rb +176 -0
- data/lib/minitest/queue/runner.rb +97 -22
- data/lib/minitest/queue/test_data.rb +14 -1
- data/lib/minitest/queue/worker_profile_reporter.rb +77 -0
- data/lib/minitest/queue.rb +271 -6
- metadata +9 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'zlib'
|
|
5
|
+
|
|
6
|
+
module Minitest
|
|
7
|
+
module Queue
|
|
8
|
+
class LazyTestDiscovery
|
|
9
|
+
def initialize(loader:, resolver:)
|
|
10
|
+
@loader = loader
|
|
11
|
+
@resolver = resolver
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def enumerator(files)
|
|
15
|
+
Enumerator.new do |yielder|
|
|
16
|
+
each_test(files) do |test|
|
|
17
|
+
yielder << test
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def each_test(files)
|
|
23
|
+
discovery_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
24
|
+
total_files = 0
|
|
25
|
+
new_runnable_files = 0
|
|
26
|
+
reopened_files = 0
|
|
27
|
+
reopened_candidates = 0
|
|
28
|
+
reopened_scan_time = 0.0
|
|
29
|
+
|
|
30
|
+
seen = Set.new
|
|
31
|
+
runnables = Minitest::Test.runnables
|
|
32
|
+
known_count = runnables.size
|
|
33
|
+
by_full_name = {}
|
|
34
|
+
by_short_name = Hash.new { |h, k| h[k] = [] }
|
|
35
|
+
index_runnables(runnables, by_full_name, by_short_name)
|
|
36
|
+
|
|
37
|
+
files.each do |file|
|
|
38
|
+
total_files += 1
|
|
39
|
+
file_path = File.expand_path(file)
|
|
40
|
+
begin
|
|
41
|
+
@loader.load_file(file_path)
|
|
42
|
+
rescue CI::Queue::FileLoadError => error
|
|
43
|
+
method_name = "load_file_#{Zlib.crc32(file_path).to_s(16)}"
|
|
44
|
+
class_name = "CIQueue::FileLoadError"
|
|
45
|
+
test_id = "#{class_name}##{method_name}"
|
|
46
|
+
entry = CI::Queue::QueueEntry.format(
|
|
47
|
+
test_id,
|
|
48
|
+
CI::Queue::QueueEntry.encode_load_error(file_path, error),
|
|
49
|
+
)
|
|
50
|
+
yield Minitest::Queue::LazySingleExample.new(
|
|
51
|
+
class_name,
|
|
52
|
+
method_name,
|
|
53
|
+
file_path,
|
|
54
|
+
loader: @loader,
|
|
55
|
+
resolver: @resolver,
|
|
56
|
+
load_error: error,
|
|
57
|
+
queue_entry: entry,
|
|
58
|
+
)
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
runnables = Minitest::Test.runnables
|
|
63
|
+
candidates = []
|
|
64
|
+
if runnables.size > known_count
|
|
65
|
+
new_runnables = runnables[known_count..]
|
|
66
|
+
known_count = runnables.size
|
|
67
|
+
index_runnables(new_runnables, by_full_name, by_short_name)
|
|
68
|
+
candidates.concat(new_runnables)
|
|
69
|
+
new_runnable_files += 1
|
|
70
|
+
else
|
|
71
|
+
# Re-opened classes do not increase runnables size. In that case, map
|
|
72
|
+
# declared class names in the file to known runnables directly.
|
|
73
|
+
reopened_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
74
|
+
reopened = reopened_runnables_for_file(file_path, by_full_name, by_short_name)
|
|
75
|
+
reopened_scan_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - reopened_start
|
|
76
|
+
unless reopened.empty?
|
|
77
|
+
reopened_files += 1
|
|
78
|
+
reopened_candidates += reopened.size
|
|
79
|
+
end
|
|
80
|
+
candidates.concat(reopened)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
enqueue_discovered_tests(candidates.uniq, file_path, seen) do |test|
|
|
84
|
+
yield test
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
ensure
|
|
88
|
+
debug_discovery_profile(
|
|
89
|
+
discovery_start: discovery_start,
|
|
90
|
+
total_files: total_files,
|
|
91
|
+
new_runnable_files: new_runnable_files,
|
|
92
|
+
reopened_files: reopened_files,
|
|
93
|
+
reopened_candidates: reopened_candidates,
|
|
94
|
+
reopened_scan_time: reopened_scan_time,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def reopened_runnables_for_file(file_path, by_full_name, by_short_name)
|
|
101
|
+
declared = declared_class_names(file_path)
|
|
102
|
+
return [] if declared.empty?
|
|
103
|
+
|
|
104
|
+
declared.each_with_object([]) do |name, runnables|
|
|
105
|
+
runnable = by_full_name[name]
|
|
106
|
+
if runnable
|
|
107
|
+
runnables << runnable
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
short_name = name.split('::').last
|
|
112
|
+
runnables.concat(by_short_name[short_name])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def index_runnables(runnables, by_full_name, by_short_name)
|
|
117
|
+
runnables.each do |runnable|
|
|
118
|
+
name = runnable.name
|
|
119
|
+
next unless name
|
|
120
|
+
|
|
121
|
+
by_full_name[name] ||= runnable
|
|
122
|
+
short_name = name.split('::').last
|
|
123
|
+
by_short_name[short_name] << runnable
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def debug_discovery_profile(discovery_start:, total_files:, new_runnable_files:, reopened_files:, reopened_candidates:, reopened_scan_time:)
|
|
128
|
+
return unless CI::Queue.debug?
|
|
129
|
+
|
|
130
|
+
total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - discovery_start
|
|
131
|
+
puts "[ci-queue][lazy-discovery] files=#{total_files} new_runnable_files=#{new_runnable_files} " \
|
|
132
|
+
"reopened_files=#{reopened_files} reopened_candidates=#{reopened_candidates} " \
|
|
133
|
+
"reopened_scan_time=#{reopened_scan_time.round(2)}s total_time=#{total_time.round(2)}s"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def enqueue_discovered_tests(runnables, file_path, seen)
|
|
137
|
+
runnables.each do |runnable|
|
|
138
|
+
runnable.runnable_methods.each do |method_name|
|
|
139
|
+
test_id = "#{runnable.name}##{method_name}"
|
|
140
|
+
next if seen.include?(test_id)
|
|
141
|
+
|
|
142
|
+
seen.add(test_id)
|
|
143
|
+
yield Minitest::Queue::LazySingleExample.new(
|
|
144
|
+
runnable.name,
|
|
145
|
+
method_name,
|
|
146
|
+
file_path,
|
|
147
|
+
loader: @loader,
|
|
148
|
+
resolver: @resolver,
|
|
149
|
+
)
|
|
150
|
+
rescue NameError, NoMethodError
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def declared_class_names(file_path)
|
|
157
|
+
names = Set.new
|
|
158
|
+
::File.foreach(file_path) do |line|
|
|
159
|
+
match = line.match(/^\s*class\s+([A-Z]\w*(?:::[A-Z]\w*)*)\b/)
|
|
160
|
+
names.add(match[1]) if match
|
|
161
|
+
end
|
|
162
|
+
names
|
|
163
|
+
rescue SystemCallError
|
|
164
|
+
Set.new
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -46,6 +46,17 @@ module Minitest
|
|
|
46
46
|
def result_line
|
|
47
47
|
"#{super}, #{requeues} requeues"
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def location(exception)
|
|
51
|
+
backtrace = exception.backtrace
|
|
52
|
+
return super if backtrace && !backtrace.empty?
|
|
53
|
+
|
|
54
|
+
nested_exception = exception.respond_to?(:error) ? exception.error : nil
|
|
55
|
+
nested_backtrace = nested_exception&.backtrace
|
|
56
|
+
return super(nested_exception) if nested_backtrace && !nested_backtrace.empty?
|
|
57
|
+
|
|
58
|
+
'unknown'
|
|
59
|
+
end
|
|
49
60
|
end
|
|
50
61
|
end
|
|
51
62
|
end
|
|
@@ -5,6 +5,9 @@ class Minitest::Queue::OrderReporter < Minitest::Reporters::BaseReporter
|
|
|
5
5
|
def initialize(options = {})
|
|
6
6
|
@path = options.delete(:path)
|
|
7
7
|
@file = nil
|
|
8
|
+
@flush_every = Integer(ENV.fetch('CI_QUEUE_ORDER_FLUSH_EVERY', '50'))
|
|
9
|
+
@flush_every = 1 if @flush_every < 1
|
|
10
|
+
@pending = 0
|
|
8
11
|
super
|
|
9
12
|
end
|
|
10
13
|
|
|
@@ -16,10 +19,15 @@ class Minitest::Queue::OrderReporter < Minitest::Reporters::BaseReporter
|
|
|
16
19
|
def before_test(test)
|
|
17
20
|
super
|
|
18
21
|
file.puts("#{test.class.name}##{test.name}")
|
|
19
|
-
|
|
22
|
+
@pending += 1
|
|
23
|
+
if @pending >= @flush_every
|
|
24
|
+
file.flush
|
|
25
|
+
@pending = 0
|
|
26
|
+
end
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def report
|
|
30
|
+
file.flush
|
|
23
31
|
file.close
|
|
24
32
|
end
|
|
25
33
|
|
|
@@ -29,4 +37,3 @@ class Minitest::Queue::OrderReporter < Minitest::Reporters::BaseReporter
|
|
|
29
37
|
@file ||= File.open(@path, 'a+')
|
|
30
38
|
end
|
|
31
39
|
end
|
|
32
|
-
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'minitest/queue/lazy_entry_resolver'
|
|
5
|
+
require 'minitest/queue/lazy_test_discovery'
|
|
6
|
+
|
|
7
|
+
module Minitest
|
|
8
|
+
module Queue
|
|
9
|
+
class QueuePopulationStrategy
|
|
10
|
+
attr_reader :load_tests_duration, :total_files
|
|
11
|
+
|
|
12
|
+
def initialize(queue:, queue_config:, argv:, test_files_file:, ordering_seed:, preresolved_test_list: nil)
|
|
13
|
+
@queue = queue
|
|
14
|
+
@queue_config = queue_config
|
|
15
|
+
@argv = argv
|
|
16
|
+
@test_files_file = test_files_file
|
|
17
|
+
@ordering_seed = ordering_seed
|
|
18
|
+
@preresolved_test_list = preresolved_test_list
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_and_populate!
|
|
22
|
+
load_tests
|
|
23
|
+
populate_queue
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :queue, :queue_config, :argv, :test_files_file, :ordering_seed, :preresolved_test_list
|
|
29
|
+
|
|
30
|
+
def populate_queue
|
|
31
|
+
if preresolved_test_list && queue.respond_to?(:stream_populate)
|
|
32
|
+
configure_lazy_queue
|
|
33
|
+
queue.stream_populate(preresolved_entry_enumerator, random: ordering_seed, batch_size: queue_config.lazy_load_stream_batch_size)
|
|
34
|
+
elsif queue_config.lazy_load && queue.respond_to?(:stream_populate)
|
|
35
|
+
configure_lazy_queue
|
|
36
|
+
queue.stream_populate(lazy_test_enumerator, random: ordering_seed, batch_size: queue_config.lazy_load_stream_batch_size)
|
|
37
|
+
else
|
|
38
|
+
queue.populate(Minitest.loaded_tests, random: ordering_seed)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_tests
|
|
43
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
if preresolved_test_list || queue_config.lazy_load
|
|
45
|
+
# In preresolved or lazy-load mode, test files are loaded on-demand by the entry resolver.
|
|
46
|
+
# Load test helpers (e.g., test/test_helper.rb via CI_QUEUE_LAZY_LOAD_TEST_HELPERS)
|
|
47
|
+
# to boot the app for all workers.
|
|
48
|
+
queue_config.lazy_load_test_helper_paths.each do |helper_path|
|
|
49
|
+
require File.expand_path(helper_path)
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
test_file_list.sort.each do |file_path|
|
|
53
|
+
require File.expand_path(file_path)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
ensure
|
|
57
|
+
@load_tests_duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
58
|
+
@total_files = begin
|
|
59
|
+
preresolved_test_list ? nil : test_file_list.size
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def configure_lazy_queue
|
|
66
|
+
return unless queue.respond_to?(:entry_resolver=)
|
|
67
|
+
|
|
68
|
+
queue.entry_resolver = lazy_entry_resolver
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def lazy_entry_resolver
|
|
72
|
+
loader = queue.respond_to?(:file_loader) ? queue.file_loader : CI::Queue::FileLoader.new
|
|
73
|
+
resolver = CI::Queue::ClassResolver
|
|
74
|
+
Minitest::Queue::LazyEntryResolver.new(loader: loader, resolver: resolver)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Reads a pre-resolved test list file (one entry per line: "TestId|file/path.rb")
|
|
78
|
+
# and yields entries in the internal tab-delimited QueueEntry format.
|
|
79
|
+
#
|
|
80
|
+
# When --test-files is also provided, those files act as a reconcile set:
|
|
81
|
+
# preresolved entries whose file path matches are skipped (they may be stale),
|
|
82
|
+
# and the files are lazily discovered to enqueue their current tests.
|
|
83
|
+
# This runs only on the leader (stream_populate is leader-only), so discovery
|
|
84
|
+
# happens exactly once per build.
|
|
85
|
+
def preresolved_entry_enumerator
|
|
86
|
+
override_files = reconcile_file_set
|
|
87
|
+
|
|
88
|
+
Enumerator.new do |yielder|
|
|
89
|
+
preresolved_kept = 0
|
|
90
|
+
preresolved_skipped = 0
|
|
91
|
+
|
|
92
|
+
File.foreach(preresolved_test_list) do |line|
|
|
93
|
+
line = line.chomp
|
|
94
|
+
next if line.strip.empty?
|
|
95
|
+
|
|
96
|
+
# Split on the LAST pipe — test method names can contain '|'
|
|
97
|
+
# (e.g., regex patterns, boolean conditions) but file paths never do.
|
|
98
|
+
test_id, _, file_path = line.rpartition('|')
|
|
99
|
+
if test_id.empty?
|
|
100
|
+
# No pipe found — treat the whole line as a test ID with no file path
|
|
101
|
+
test_id = file_path
|
|
102
|
+
file_path = nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Skip entries for files that will be re-discovered via --test-files
|
|
106
|
+
if file_path && override_files.include?(file_path)
|
|
107
|
+
preresolved_skipped += 1
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
preresolved_kept += 1
|
|
112
|
+
yielder << CI::Queue::QueueEntry.format(test_id, file_path)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if CI::Queue.debug?
|
|
116
|
+
puts "[ci-queue][preresolved] kept=#{preresolved_kept} skipped=#{preresolved_skipped} " \
|
|
117
|
+
"reconcile_files=#{override_files.size}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Lazily discover current tests for each reconciled file
|
|
121
|
+
unless override_files.empty?
|
|
122
|
+
discovery_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
123
|
+
discovered = 0
|
|
124
|
+
lazy_test_discovery.each_test(override_files.to_a) do |example|
|
|
125
|
+
discovered += 1
|
|
126
|
+
yielder << example.queue_entry
|
|
127
|
+
end
|
|
128
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - discovery_start
|
|
129
|
+
if CI::Queue.debug?
|
|
130
|
+
puts "[ci-queue][reconcile-discovery] discovered=#{discovered} files=#{override_files.size} " \
|
|
131
|
+
"duration=#{duration.round(2)}s"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def lazy_test_discovery
|
|
138
|
+
@lazy_test_discovery ||= begin
|
|
139
|
+
loader = queue.respond_to?(:file_loader) ? queue.file_loader : CI::Queue::FileLoader.new
|
|
140
|
+
Minitest::Queue::LazyTestDiscovery.new(loader: loader, resolver: CI::Queue::ClassResolver)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# In preresolved mode, --test-files provides the reconcile set: test files
|
|
145
|
+
# whose preresolved entries should be discarded and re-discovered.
|
|
146
|
+
# Returns an empty Set if no test files file is provided.
|
|
147
|
+
def reconcile_file_set
|
|
148
|
+
return Set.new unless test_files_file
|
|
149
|
+
return Set.new unless File.exist?(test_files_file)
|
|
150
|
+
|
|
151
|
+
Set.new(File.readlines(test_files_file, chomp: true).reject(&:empty?))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def lazy_test_enumerator
|
|
155
|
+
loader = queue.respond_to?(:file_loader) ? queue.file_loader : CI::Queue::FileLoader.new
|
|
156
|
+
resolver = CI::Queue::ClassResolver
|
|
157
|
+
files = test_file_list.sort
|
|
158
|
+
|
|
159
|
+
Minitest::Queue::LazyTestDiscovery.new(loader: loader, resolver: resolver).enumerator(files)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns the list of test files to process. Prefers --test-files FILE
|
|
163
|
+
# (reads paths from a file, one per line) over positional argv arguments.
|
|
164
|
+
# --test-files avoids ARG_MAX limits for large test suites (36K+ files).
|
|
165
|
+
def test_file_list
|
|
166
|
+
@test_file_list ||= begin
|
|
167
|
+
if test_files_file
|
|
168
|
+
File.readlines(test_files_file, chomp: true).reject { |f| f.strip.empty? }
|
|
169
|
+
else
|
|
170
|
+
argv
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -7,6 +7,8 @@ require 'ci/queue'
|
|
|
7
7
|
require 'digest/md5'
|
|
8
8
|
require 'minitest/reporters/bisect_reporter'
|
|
9
9
|
require 'minitest/reporters/statsd_reporter'
|
|
10
|
+
require 'minitest/queue/queue_population_strategy'
|
|
11
|
+
require 'minitest/queue/worker_profile_reporter'
|
|
10
12
|
|
|
11
13
|
module Minitest
|
|
12
14
|
module Queue
|
|
@@ -16,6 +18,10 @@ module Minitest
|
|
|
16
18
|
Error = Class.new(StandardError)
|
|
17
19
|
MissingParameter = Class.new(Error)
|
|
18
20
|
|
|
21
|
+
class << self
|
|
22
|
+
attr_accessor :run_start, :load_tests_duration, :total_files
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
def self.invoke(argv)
|
|
20
26
|
new(argv).run!
|
|
21
27
|
end
|
|
@@ -49,6 +55,8 @@ module Minitest
|
|
|
49
55
|
end
|
|
50
56
|
|
|
51
57
|
def run_command
|
|
58
|
+
Runner.run_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
59
|
+
|
|
52
60
|
require_worker_id!
|
|
53
61
|
# if it's an automatic job retry we should process the main queue
|
|
54
62
|
if manual_retry?
|
|
@@ -98,19 +106,27 @@ module Minitest
|
|
|
98
106
|
if remaining <= running
|
|
99
107
|
puts green("Queue almost empty, exiting early...")
|
|
100
108
|
else
|
|
101
|
-
|
|
102
|
-
populate_queue
|
|
109
|
+
prepare_queue_for_execution
|
|
103
110
|
end
|
|
104
111
|
else
|
|
105
|
-
|
|
106
|
-
populate_queue
|
|
112
|
+
prepare_queue_for_execution
|
|
107
113
|
end
|
|
108
114
|
end
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
if queue_config.lazy_load
|
|
117
|
+
# In lazy-load mode, run minitest explicitly instead of relying on
|
|
118
|
+
# minitest/autorun's at_exit hook, which may not be registered since
|
|
119
|
+
# test files haven't been loaded yet. exit! prevents double-execution
|
|
120
|
+
# if minitest/autorun was loaded by the leader during streaming.
|
|
121
|
+
passed = Minitest.run []
|
|
111
122
|
verify_reporters!(reporters)
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
exit!(passed ? 0 : 1)
|
|
124
|
+
else
|
|
125
|
+
at_exit {
|
|
126
|
+
verify_reporters!(reporters)
|
|
127
|
+
}
|
|
128
|
+
# Let minitest's at_exit hook trigger
|
|
129
|
+
end
|
|
114
130
|
end
|
|
115
131
|
|
|
116
132
|
def verify_reporters!(reporters)
|
|
@@ -171,11 +187,9 @@ module Minitest
|
|
|
171
187
|
trap('TERM') { Minitest.queue.shutdown! }
|
|
172
188
|
trap('INT') { Minitest.queue.shutdown! }
|
|
173
189
|
|
|
174
|
-
load_tests
|
|
175
|
-
|
|
176
190
|
@queue = CI::Queue::Grind.new(grind_list, queue_config)
|
|
177
191
|
Minitest.queue = queue
|
|
178
|
-
|
|
192
|
+
prepare_queue_for_execution
|
|
179
193
|
|
|
180
194
|
# Let minitest's at_exit hook trigger
|
|
181
195
|
end
|
|
@@ -184,10 +198,9 @@ module Minitest
|
|
|
184
198
|
invalid_usage! "Missing the FAILING_TEST argument." unless queue_config.failing_test
|
|
185
199
|
|
|
186
200
|
set_load_path
|
|
187
|
-
load_tests
|
|
188
201
|
@queue = CI::Queue::Bisect.new(queue_url, queue_config)
|
|
189
202
|
Minitest.queue = queue
|
|
190
|
-
|
|
203
|
+
prepare_queue_for_execution
|
|
191
204
|
|
|
192
205
|
step("Testing the failing test in isolation")
|
|
193
206
|
unless queue.failing_test_present?
|
|
@@ -285,6 +298,7 @@ module Minitest
|
|
|
285
298
|
reporter.write_failure_file(queue_config.failure_file) if queue_config.failure_file
|
|
286
299
|
reporter.write_flaky_tests_file(queue_config.export_flaky_tests_file) if queue_config.export_flaky_tests_file
|
|
287
300
|
exit_code = reporter.report
|
|
301
|
+
print_worker_profiles(supervisor)
|
|
288
302
|
exit! exit_code
|
|
289
303
|
end
|
|
290
304
|
|
|
@@ -322,7 +336,7 @@ module Minitest
|
|
|
322
336
|
|
|
323
337
|
attr_reader :queue_config, :options, :command, :argv
|
|
324
338
|
attr_writer :queue_url
|
|
325
|
-
attr_accessor :queue, :grind_list, :grind_count, :load_paths, :verbose
|
|
339
|
+
attr_accessor :queue, :grind_list, :grind_count, :load_paths, :verbose, :test_files_file, :preresolved_test_list
|
|
326
340
|
|
|
327
341
|
def require_worker_id!
|
|
328
342
|
if queue.distributed?
|
|
@@ -372,10 +386,6 @@ module Minitest
|
|
|
372
386
|
queue.build.reset_worker_error
|
|
373
387
|
end
|
|
374
388
|
|
|
375
|
-
def populate_queue
|
|
376
|
-
Minitest.queue.populate(Minitest.loaded_tests, random: ordering_seed)
|
|
377
|
-
end
|
|
378
|
-
|
|
379
389
|
def set_load_path
|
|
380
390
|
if paths = load_paths
|
|
381
391
|
paths.split(':').reverse.each do |path|
|
|
@@ -384,10 +394,22 @@ module Minitest
|
|
|
384
394
|
end
|
|
385
395
|
end
|
|
386
396
|
|
|
387
|
-
def
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
397
|
+
def prepare_queue_for_execution
|
|
398
|
+
strategy = queue_population_strategy
|
|
399
|
+
strategy.load_and_populate!
|
|
400
|
+
Runner.load_tests_duration = strategy.load_tests_duration
|
|
401
|
+
Runner.total_files = strategy.total_files
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def queue_population_strategy
|
|
405
|
+
Minitest::Queue::QueuePopulationStrategy.new(
|
|
406
|
+
queue: queue,
|
|
407
|
+
queue_config: queue_config,
|
|
408
|
+
argv: argv,
|
|
409
|
+
test_files_file: test_files_file,
|
|
410
|
+
ordering_seed: ordering_seed,
|
|
411
|
+
preresolved_test_list: preresolved_test_list,
|
|
412
|
+
)
|
|
391
413
|
end
|
|
392
414
|
|
|
393
415
|
def parse(argv)
|
|
@@ -396,6 +418,10 @@ module Minitest
|
|
|
396
418
|
return command, argv
|
|
397
419
|
end
|
|
398
420
|
|
|
421
|
+
def print_worker_profiles(supervisor)
|
|
422
|
+
Minitest::Queue::WorkerProfileReporter.new(supervisor).print_summary
|
|
423
|
+
end
|
|
424
|
+
|
|
399
425
|
def parser
|
|
400
426
|
@parser ||= OptionParser.new do |opts|
|
|
401
427
|
opts.banner = "Usage: minitest-queue [options] COMMAND [ARGS]"
|
|
@@ -502,7 +528,56 @@ module Minitest
|
|
|
502
528
|
end
|
|
503
529
|
|
|
504
530
|
help = <<~EOS
|
|
505
|
-
|
|
531
|
+
Load test files on demand instead of eagerly loading all tests.
|
|
532
|
+
EOS
|
|
533
|
+
opts.separator ""
|
|
534
|
+
opts.on('--lazy-load', help) do
|
|
535
|
+
queue_config.lazy_load = true
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
help = <<~EOS
|
|
539
|
+
Batch size for streaming tests to Redis in lazy mode.
|
|
540
|
+
Defaults to 5000.
|
|
541
|
+
EOS
|
|
542
|
+
opts.separator ""
|
|
543
|
+
opts.on('--lazy-load-stream-batch-size SIZE', Integer, help) do |size|
|
|
544
|
+
queue_config.lazy_load_stream_batch_size = size
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
help = <<~EOS
|
|
548
|
+
Read test file paths from FILE (one per line) instead of positional arguments.
|
|
549
|
+
Useful for large test suites to avoid ARG_MAX limits.
|
|
550
|
+
EOS
|
|
551
|
+
opts.separator ""
|
|
552
|
+
opts.on('--test-files FILE', help) do |file|
|
|
553
|
+
self.test_files_file = file
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
help = <<~EOS
|
|
557
|
+
Read a pre-resolved test list from FILE instead of discovering tests from files.
|
|
558
|
+
Each line must be in the format: TestClass#method_name|path/to/test_file.rb
|
|
559
|
+
The leader streams entries directly to Redis without loading or discovering tests.
|
|
560
|
+
Implies lazy-load mode for all workers.
|
|
561
|
+
When combined with --test-files, entries whose file path appears in that list
|
|
562
|
+
are skipped and the files are lazily re-discovered (reconciliation).
|
|
563
|
+
EOS
|
|
564
|
+
opts.separator ""
|
|
565
|
+
opts.on('--preresolved-tests FILE', help) do |file|
|
|
566
|
+
self.preresolved_test_list = file
|
|
567
|
+
queue_config.lazy_load = true
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
help = <<~EOS
|
|
571
|
+
Timeout in seconds without new streamed batches before failing.
|
|
572
|
+
Defaults to the max of --queue-init-timeout and 300 seconds.
|
|
573
|
+
EOS
|
|
574
|
+
opts.separator ""
|
|
575
|
+
opts.on('--lazy-load-stream-timeout SECONDS', Integer, help) do |seconds|
|
|
576
|
+
queue_config.lazy_load_streaming_timeout = seconds
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
help = <<~EOS
|
|
580
|
+
Specify a seed used to shuffle the test suite.
|
|
506
581
|
On Buildkite, CircleCI, Heroku CI, and Travis, the commit revision will be used by default.
|
|
507
582
|
EOS
|
|
508
583
|
opts.separator ""
|
|
@@ -137,11 +137,13 @@ module Minitest
|
|
|
137
137
|
def error_location(exception)
|
|
138
138
|
@error_location ||= begin
|
|
139
139
|
last_before_assertion = ''
|
|
140
|
-
exception.
|
|
140
|
+
backtrace_for(exception).reverse_each do |s|
|
|
141
141
|
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
|
142
142
|
|
|
143
143
|
last_before_assertion = s
|
|
144
144
|
end
|
|
145
|
+
return ['unknown', 0] if last_before_assertion.empty?
|
|
146
|
+
|
|
145
147
|
path = last_before_assertion.sub(/:in .*$/, '')
|
|
146
148
|
# the path includes the linenumber at the end,
|
|
147
149
|
# which is seperated by a :
|
|
@@ -151,6 +153,17 @@ module Minitest
|
|
|
151
153
|
[result.first, result.last.to_i]
|
|
152
154
|
end
|
|
153
155
|
end
|
|
156
|
+
|
|
157
|
+
def backtrace_for(exception)
|
|
158
|
+
backtrace = exception.backtrace
|
|
159
|
+
return backtrace if backtrace && !backtrace.empty?
|
|
160
|
+
|
|
161
|
+
nested_exception = exception.respond_to?(:error) ? exception.error : nil
|
|
162
|
+
nested_backtrace = nested_exception&.backtrace
|
|
163
|
+
return nested_backtrace if nested_backtrace && !nested_backtrace.empty?
|
|
164
|
+
|
|
165
|
+
[]
|
|
166
|
+
end
|
|
154
167
|
end
|
|
155
168
|
end
|
|
156
169
|
end
|