Skip to content

Commit 8620227

Browse files
Bug fixes and cleanup
1 parent 481ae1c commit 8620227

10 files changed

Lines changed: 120 additions & 18 deletions

File tree

ruby/lib/ci/queue.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ def shuffle(tests, random)
6464
end
6565

6666
def debug?
67+
return @debug if defined?(@debug)
68+
6769
value = ENV['CI_QUEUE_DEBUG']
68-
value && !value.strip.empty? && !%w[0 false].include?(value.strip.downcase)
70+
@debug = value && !value.strip.empty? && !%w[0 false].include?(value.strip.downcase)
6971
end
7072

7173
def from_uri(url, config)

ruby/lib/ci/queue/configuration.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ class Configuration
77
attr_accessor :max_test_duration, :max_test_duration_percentile, :track_test_duration
88
attr_accessor :max_test_failed, :redis_ttl, :warnings_file, :debug_log, :max_missed_heartbeat_seconds
99
attr_accessor :lazy_load, :lazy_load_stream_batch_size
10-
attr_accessor :lazy_load_streaming_timeout, :lazy_load_test_helpers
10+
attr_writer :lazy_load_streaming_timeout
11+
attr_accessor :lazy_load_test_helpers
1112
attr_accessor :skip_stale_tests
1213
attr_reader :circuit_breakers
1314
attr_writer :seed, :build_id

ruby/lib/ci/queue/redis/worker.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def stream_populate(tests, random: Random.new, batch_size: 10_000)
4747

4848
if @master = (value == status)
4949
@total = 0
50-
puts "Worker elected as leader, streaming tests to the queue."
50+
puts "Worker elected as leader, streaming tests to the queue."
5151

5252
duration = measure do
5353
start_streaming!
@@ -213,6 +213,7 @@ def requeue(test, offset: Redis.requeue_offset)
213213
unless requeued
214214
reserved_tests << test_id
215215
reserved_entries[test_id] = entry
216+
reserved_entry_ids[entry] = test_id
216217
end
217218
requeued
218219
end

ruby/lib/minitest/queue.rb

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ def run(reporter, *)
173173
report_load_stats(queue)
174174
ensure
175175
store_worker_profile(queue)
176+
queue.stop_heartbeat!
176177
end
177-
queue.stop_heartbeat!
178178
end
179179
end
180180

@@ -433,18 +433,15 @@ def with_timestamps
433433

434434
def run
435435
with_timestamps do
436-
begin
437-
return build_error_result(@load_error) if @load_error
438-
439-
klass = runnable
440-
if skip_stale_tests? && !(klass.method_defined?(@method_name) || klass.private_method_defined?(@method_name))
441-
return build_stale_skip_result
442-
end
443-
444-
Minitest.run_one_method(klass, @method_name)
445-
rescue StandardError, ScriptError => error
446-
build_error_result(error)
436+
if @load_error
437+
build_error_result(@load_error)
438+
elsif skip_stale_tests? && !(runnable.method_defined?(@method_name) || runnable.private_method_defined?(@method_name))
439+
build_stale_skip_result
440+
else
441+
Minitest.run_one_method(runnable, @method_name)
447442
end
443+
rescue StandardError, ScriptError => error
444+
build_error_result(error)
448445
end
449446
end
450447

ruby/lib/minitest/queue/lazy_entry_resolver.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ def call(entry)
2626
load_error: load_error,
2727
queue_entry: entry,
2828
)
29+
else
30+
error = StandardError.new("Corrupt load error payload for #{class_name}##{method_name}")
31+
load_error = CI::Queue::FileLoadError.new("unknown", error)
32+
return Minitest::Queue::LazySingleExample.new(
33+
class_name,
34+
method_name,
35+
nil,
36+
loader: @loader,
37+
resolver: @resolver,
38+
load_error: load_error,
39+
queue_entry: entry,
40+
)
2941
end
3042
end
3143

ruby/lib/minitest/queue/lazy_test_discovery.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'set'
4+
require 'zlib'
45

56
module Minitest
67
module Queue
@@ -39,7 +40,7 @@ def each_test(files)
3940
begin
4041
@loader.load_file(file_path)
4142
rescue CI::Queue::FileLoadError => error
42-
method_name = "load_file_#{file_path.hash.abs}"
43+
method_name = "load_file_#{Zlib.crc32(file_path).to_s(16)}"
4344
class_name = "CIQueue::FileLoadError"
4445
test_id = "#{class_name}##{method_name}"
4546
entry = CI::Queue::QueueEntry.format(

ruby/lib/minitest/queue/runner.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def retry_command
5555
end
5656

5757
def run_command
58-
@run_start = Runner.run_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
58+
Runner.run_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
5959

6060
require_worker_id!
6161
# if it's an automatic job retry we should process the main queue
@@ -577,7 +577,7 @@ def parser
577577
end
578578

579579
help = <<~EOS
580-
Sepcify a seed used to shuffle the test suite.
580+
Specify a seed used to shuffle the test suite.
581581
On Buildkite, CircleCI, Heroku CI, and Travis, the commit revision will be used by default.
582582
EOS
583583
opts.separator ""

ruby/test/minitest/queue/lazy_entry_resolver_test.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ def test_builds_lazy_single_example_for_regular_entry
1616
assert_equal entry, resolved.queue_entry
1717
end
1818

19+
def test_builds_error_result_for_corrupt_load_error_payload
20+
loader = CI::Queue::FileLoader.new
21+
resolver = CI::Queue::ClassResolver
22+
corrupt_payload = "__ciq_load_error__:not_valid_base64!!!"
23+
entry = CI::Queue::QueueEntry.format("CIQueue::FileLoadError#load_file_abc123", corrupt_payload)
24+
25+
resolved = LazyEntryResolver.new(loader: loader, resolver: resolver).call(entry)
26+
result = resolved.run
27+
28+
assert_instance_of Minitest::Queue::LazySingleExample, resolved
29+
assert result.error?
30+
assert_match(/Corrupt load error payload/, result.failure.error.message)
31+
end
32+
1933
def test_builds_lazy_single_example_with_load_error
2034
loader = CI::Queue::FileLoader.new
2135
resolver = CI::Queue::ClassResolver

ruby/test/minitest/queue/lazy_single_example_test.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,52 @@ def test_run_errors_on_stale_entry_when_skip_stale_tests_disabled
114114
end
115115
end
116116

117+
def test_run_sets_timestamps_on_load_error_result
118+
loader = CI::Queue::FileLoader.new
119+
resolver = CI::Queue::ClassResolver
120+
error = StandardError.new('boom')
121+
example = LazySingleExample.new('MissingClass', 'test_missing', '/tmp/missing.rb', loader: loader, resolver: resolver, load_error: error)
122+
123+
result = example.run
124+
125+
assert result.error?
126+
assert_kind_of Integer, result.start_timestamp
127+
assert_kind_of Integer, result.finish_timestamp
128+
assert result.finish_timestamp >= result.start_timestamp
129+
end
130+
131+
def test_run_sets_timestamps_on_stale_skip_result
132+
Dir.mktmpdir do |dir|
133+
class_name = "StaleTimestamp#{Process.pid}#{rand(1000)}"
134+
file_path = File.join(dir, "stale_timestamp_test.rb")
135+
File.write(
136+
file_path,
137+
"class #{class_name} < Minitest::Test\n" \
138+
" def test_exists\n" \
139+
" assert true\n" \
140+
" end\n" \
141+
"end\n"
142+
)
143+
144+
loader = CI::Queue::FileLoader.new
145+
resolver = CI::Queue::ClassResolver
146+
example = LazySingleExample.new(class_name, 'test_no_longer_exists', file_path, loader: loader, resolver: resolver)
147+
148+
old_queue = Minitest.queue
149+
Minitest.queue = Struct.new(:config).new(CI::Queue::Configuration.new(skip_stale_tests: true))
150+
151+
result = example.run
152+
153+
assert result.skipped?
154+
assert_kind_of Integer, result.start_timestamp
155+
assert_kind_of Integer, result.finish_timestamp
156+
assert result.finish_timestamp >= result.start_timestamp
157+
ensure
158+
Minitest.queue = old_queue
159+
Object.send(:remove_const, class_name) if Object.const_defined?(class_name)
160+
end
161+
end
162+
117163
def test_marshal_round_trip
118164
Dir.mktmpdir do |dir|
119165
class_name = "LazyMarshal#{Process.pid}#{rand(1000)}"

ruby/test/minitest/queue/lazy_test_discovery_test.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
11
# frozen_string_literal: true
22
require 'test_helper'
33
require 'minitest/queue/lazy_test_discovery'
4+
require 'zlib'
45

56
module Minitest::Queue
67
class LazyTestDiscoveryTest < Minitest::Test
8+
def test_load_error_generates_deterministic_test_id
9+
loader = CI::Queue::FileLoader.new
10+
resolver = CI::Queue::ClassResolver
11+
discovered = []
12+
13+
Dir.mktmpdir do |dir|
14+
file_path = File.join(dir, "broken_test.rb")
15+
File.write(file_path, "raise 'boom'\n")
16+
17+
2.times do
18+
discovered.clear
19+
discovery = LazyTestDiscovery.new(loader: loader, resolver: resolver)
20+
discovery.each_test([file_path]) { |test| discovered << test.id }
21+
end
22+
23+
assert_equal 1, discovered.size
24+
assert_match(/\ACIQueue::FileLoadError#load_file_[0-9a-f]+\z/, discovered.first)
25+
end
26+
end
27+
28+
def test_load_error_test_id_is_stable_across_invocations
29+
file_path = "/tmp/stable_hash_test_#{rand(1000)}.rb"
30+
method_name_a = "load_file_#{Zlib.crc32(file_path).to_s(16)}"
31+
method_name_b = "load_file_#{Zlib.crc32(file_path).to_s(16)}"
32+
assert_equal method_name_a, method_name_b
33+
end
34+
735
def test_discovers_methods_added_by_reopened_class
836
loader = CI::Queue::FileLoader.new
937
resolver = CI::Queue::ClassResolver

0 commit comments

Comments
 (0)