@@ -61,6 +61,149 @@ def test_retry_queue_with_all_tests_passing_2
6161 assert_equal retry_test_order , retry_test_order
6262 end
6363
64+ def test_retry_queue_preserves_full_entries_with_file_paths
65+ # Use stream_populate with file-path entries (as in preresolved mode),
66+ # then verify retry_queue preserves the full entry including the file path.
67+ @redis . flushdb
68+ build_id = 'retry-file-paths'
69+ leader = worker ( 1 , populate : false , lazy_load_streaming_timeout : 2 , queue_init_timeout : 2 , build_id : build_id )
70+ consumer = worker ( 2 , populate : false , lazy_load_streaming_timeout : 2 , queue_init_timeout : 2 , build_id : build_id )
71+ consumer . entry_resolver = -> ( entry ) { entry }
72+
73+ tests = [
74+ EntryTest . new ( 'ATest#test_foo' , CI ::Queue ::QueueEntry . format ( 'ATest#test_foo' , '/tmp/a_test.rb' ) ) ,
75+ EntryTest . new ( 'ATest#test_bar' , CI ::Queue ::QueueEntry . format ( 'ATest#test_bar' , '/tmp/a_test.rb' ) ) ,
76+ ]
77+
78+ leader_thread = Thread . new do
79+ leader . stream_populate ( tests , random : Random . new ( 0 ) , batch_size : 10 )
80+ end
81+
82+ timeout_at = Process . clock_gettime ( Process ::CLOCK_MONOTONIC ) + 2
83+ loop do
84+ status = @redis . get ( leader . send ( :key , 'master-status' ) )
85+ break if status == 'ready'
86+ raise "streaming status not set" if Process . clock_gettime ( Process ::CLOCK_MONOTONIC ) > timeout_at
87+ sleep 0.01
88+ end
89+
90+ # Consumer polls all tests, failing the first one
91+ failed_entry = nil
92+ consumer . poll do |entry |
93+ if failed_entry . nil?
94+ failed_entry = entry
95+ consumer . report_failure!
96+ # record_error calls acknowledge internally
97+ consumer . build . record_error ( entry , 'Failed' )
98+ else
99+ consumer . report_success!
100+ consumer . acknowledge ( entry )
101+ end
102+ end
103+
104+ leader_thread . join ( 2 )
105+
106+ retry_queue = consumer . retry_queue
107+ refute_predicate retry_queue , :exhausted?
108+
109+ retry_entries = retry_queue . instance_variable_get ( :@queue ) . dup
110+ assert_equal 1 , retry_entries . size
111+ # The critical assertion: retry entry must be a JSON entry with file_path,
112+ # not just the bare test ID. A regression in retry_queue would strip this.
113+ parsed = CI ::Queue ::QueueEntry . parse ( retry_entries . first )
114+ assert parsed [ :file_path ] , "Retry entry should preserve the full entry with file path"
115+ failed_test_id = CI ::Queue ::QueueEntry . test_id ( failed_entry )
116+ assert_equal failed_test_id , CI ::Queue ::QueueEntry . test_id ( retry_entries . first )
117+ ensure
118+ leader_thread &.kill
119+ end
120+
121+ def test_retry_queue_stream_populate_is_noop
122+ target = shuffled_test_list . first
123+ @queue . poll do |test |
124+ if test == target
125+ @queue . report_failure!
126+ # record_error calls acknowledge internally
127+ @queue . build . record_error ( test . queue_entry , 'Failed' )
128+ else
129+ @queue . report_success!
130+ @queue . acknowledge ( test . queue_entry )
131+ end
132+ end
133+
134+ retry_queue = @queue . retry_queue
135+ original_queue_contents = retry_queue . instance_variable_get ( :@queue ) . dup
136+ refute_empty original_queue_contents
137+
138+ # stream_populate should NOT replace the retry queue's contents
139+ dummy_entries = Enumerator . new do |yielder |
140+ yielder << CI ::Queue ::QueueEntry . format ( "ZTest#test_zzz" , "/tmp/z_test.rb" )
141+ end
142+ retry_queue . stream_populate ( dummy_entries , random : Random . new ( 0 ) )
143+
144+ assert_equal original_queue_contents , retry_queue . instance_variable_get ( :@queue ) ,
145+ "stream_populate should not replace retry queue contents"
146+ end
147+
148+ def test_retry_queue_works_with_entry_resolver
149+ # Fail a test, then verify retry queue works with entry_resolver (lazy loading)
150+ target = shuffled_test_list . first
151+ @queue . poll do |test |
152+ if test == target
153+ @queue . report_failure!
154+ # record_error calls acknowledge internally
155+ @queue . build . record_error ( test . queue_entry , 'Failed' )
156+ else
157+ @queue . report_success!
158+ @queue . acknowledge ( test . queue_entry )
159+ end
160+ end
161+
162+ retry_queue = @queue . retry_queue
163+
164+ # Set up entry_resolver (as configure_lazy_queue would do)
165+ resolved_entries = [ ]
166+ retry_queue . entry_resolver = -> ( entry ) {
167+ resolved_entries << entry
168+ entry
169+ }
170+
171+ # stream_populate is a no-op, preserving the retry entries
172+ retry_queue . stream_populate ( Enumerator . new { |y | } , random : Random . new ( 0 ) )
173+
174+ # Poll should use entry_resolver, not index.fetch — no KeyError crash
175+ polled = [ ]
176+ retry_queue . poll do |test |
177+ polled << test
178+ retry_queue . acknowledge ( test )
179+ end
180+
181+ assert_equal retry_queue . total , polled . size
182+ assert_equal polled . size , resolved_entries . size ,
183+ "All polled entries should have gone through entry_resolver"
184+ end
185+
186+ def test_retry_queue_with_multiple_failures_deduplicates
187+ # Fail multiple tests, verify retry queue deduplicates by test_id
188+ failed_ids = [ ]
189+ @queue . poll do |test |
190+ @queue . report_failure!
191+ @queue . build . record_error ( test . queue_entry , 'Failed' )
192+ failed_ids << test . id
193+ end
194+
195+ assert_operator failed_ids . size , :>= , 2 , "Need multiple failures for this test"
196+
197+ retry_queue = @queue . retry_queue
198+ retry_entries = retry_queue . instance_variable_get ( :@queue ) . dup
199+
200+ # Each failed test should appear exactly once (no duplicates from requeues)
201+ retry_test_ids = retry_entries . map { |e | CI ::Queue ::QueueEntry . test_id ( e ) }
202+ assert_equal retry_test_ids . uniq , retry_test_ids ,
203+ "Retry queue should not contain duplicate test IDs"
204+ assert_equal failed_ids . uniq . sort , retry_test_ids . sort
205+ end
206+
64207 def test_shutdown
65208 poll ( @queue ) do
66209 @queue . shutdown!
0 commit comments