@@ -28,6 +28,7 @@ def run!
28
28
undef_method :puts , :print # Should only use @out.puts or @err.puts
29
29
30
30
RUNNERS = {
31
+ "bisect" => :bisect ,
31
32
"report" => :report ,
32
33
"run" => :run ,
33
34
} . freeze
@@ -51,6 +52,8 @@ def run
51
52
report
52
53
when nil , :run
53
54
run_tests
55
+ when :bisect
56
+ bisect_tests
54
57
else
55
58
raise InvalidArgument , "Parsing failure"
56
59
end
@@ -116,6 +119,32 @@ def report
116
119
QueueReporter . new ( @config , queue , @out ) . run ( default_reporters ) ? 0 : 1
117
120
end
118
121
122
+ def bisect_tests
123
+ require "megatest/multi_process"
124
+
125
+ queue = @config . build_queue
126
+ raise InvalidArgument , "Distributed queues can't be bisected" if queue . distributed?
127
+
128
+ @config . selectors = Selector . parse ( @argv )
129
+ Megatest . load_config ( @config )
130
+ Megatest . init ( @config )
131
+ test_cases = Megatest . load_tests ( @config )
132
+ queue . populate ( test_cases )
133
+ candidates = queue . dup
134
+
135
+ if test_cases . empty?
136
+ @err . puts "No tests to run"
137
+ return 1
138
+ end
139
+
140
+ unless failure = find_failing_test ( queue )
141
+ @err . puts "No failing test"
142
+ return 1
143
+ end
144
+
145
+ bisect_queue ( candidates , failure . test_id )
146
+ end
147
+
119
148
private
120
149
121
150
def default_reporters
@@ -141,6 +170,67 @@ def default_reporters
141
170
reporters
142
171
end
143
172
173
+ def find_failing_test ( queue )
174
+ @config . max_consecutive_failures = 1
175
+ @config . jobs_count = 1
176
+
177
+ executor = MultiProcess ::Executor . new ( @config . dup , @out )
178
+ executor . run ( queue , default_reporters )
179
+ queue . summary . failures . first
180
+ end
181
+
182
+ def bisect_queue ( queue , failing_test_id )
183
+ err = Output . new ( @err )
184
+ tests = queue . to_a
185
+ failing_test_index = tests . index { |test | test . id == failing_test_id }
186
+ failing_test = tests [ failing_test_index ]
187
+ suspects = tests [ 0 ...failing_test_index ]
188
+
189
+ check_passing = @config . build_queue
190
+ check_passing . populate ( [ failing_test ] )
191
+ executor = MultiProcess ::Executor . new ( @config . dup , @out , managed : true )
192
+ executor . run ( check_passing , [ ] )
193
+ unless check_passing . success?
194
+ err . puts err . red ( "Test failed by itself, no need to bisect" )
195
+ return 1
196
+ end
197
+
198
+ run_index = 0
199
+ while suspects . size > 1
200
+ run_index += 1
201
+ err . puts "Attempt #{ run_index } , #{ suspects . size } suspects left."
202
+
203
+ before , after = suspects [ 0 ...( suspects . size / 2 ) ] , suspects [ ( suspects . size / 2 ) ..]
204
+ candidates = @config . build_queue
205
+ candidates . populate ( before + [ failing_test ] )
206
+
207
+ executor = MultiProcess ::Executor . new ( @config . dup , @out , managed : true )
208
+ executor . run ( candidates , default_reporters )
209
+
210
+ if candidates . success?
211
+ suspects = after
212
+ else
213
+ suspects = before
214
+ end
215
+
216
+ err . puts
217
+ end
218
+ suspect = suspects . first
219
+
220
+ validation_queue = @config . build_queue
221
+ validation_queue . populate ( [ suspect , failing_test ] )
222
+ executor = MultiProcess ::Executor . new ( @config . dup , @out , managed : true )
223
+ executor . run ( validation_queue , [ ] )
224
+ if validation_queue . success?
225
+ err . puts err . red ( "Bisect inconclusive" )
226
+ return 1
227
+ end
228
+
229
+ err . print "Found test leak: "
230
+ err . puts err . yellow "#{ @config . program_name } #{ Megatest . relative_path ( suspect . location_id ) } #{ Megatest . relative_path ( failing_test . location_id ) } "
231
+ 0
232
+ end
233
+
144
234
def open_file ( path )
145
235
File . open ( path , "w+" )
146
236
rescue Errno ::ENOENT
@@ -176,6 +266,8 @@ def build_parser(runner)
176
266
opts . banner = "Usage: #{ @program_name } report [options]"
177
267
when :run
178
268
opts . banner = "Usage: #{ @program_name } run [options] [files or directories]"
269
+ when :bisect
270
+ opts . banner = "Usage: #{ @program_name } bisect [options] [files or directories]"
179
271
else
180
272
opts . banner = "Usage: #{ @program_name } command [options] [files or directories]"
181
273
opts . separator ""
@@ -190,6 +282,11 @@ def build_parser(runner)
190
282
opts . separator "\t report\t \t Wait for the queue to be entirely processed and report the status"
191
283
opts . separator "\t \t \t $ #{ @program_name } report --queue redis://ci-queue.example.com --build-id $CI_BUILD_ID"
192
284
opts . separator ""
285
+
286
+ opts . separator "\t bisect\t \t Repeatedly run subsets of the given tests."
287
+ opts . separator "\t \t \t $ #{ @program_name } bisect --seed 12345 test/integration"
288
+ opts . separator "\t \t \t $ #{ @program_name } bisect --queue path/to/test_order.log"
289
+ opts . separator ""
193
290
end
194
291
195
292
opts . separator ""
@@ -214,11 +311,13 @@ def build_parser(runner)
214
311
@junit = path
215
312
end
216
313
217
- if runner == : run
314
+ if %i[ run bisect ] . include? ( runner )
218
315
opts . on ( "--seed SEED" , Integer , "The seed used to define run order" ) do |seed |
219
316
@config . seed = seed
220
317
end
318
+ end
221
319
320
+ if runner == :run
222
321
opts . on ( "-j" , "--jobs JOBS" , Integer , "Number of processes to use" ) do |jobs |
223
322
@config . jobs_count = jobs
224
323
end
@@ -245,8 +344,10 @@ def build_parser(runner)
245
344
@config . queue_url = queue_url
246
345
end
247
346
248
- opts . on ( "--build-id ID" , String , "Unique identifier for the CI build" ) do |build_id |
249
- @config . build_id = build_id
347
+ if %i[ run report ] . include? ( runner )
348
+ opts . on ( "--build-id ID" , String , "Unique identifier for the CI build" ) do |build_id |
349
+ @config . build_id = build_id
350
+ end
250
351
end
251
352
252
353
if runner == :run
0 commit comments