@@ -7,6 +7,8 @@ using Dates
77using Printf: @sprintf
88using Base. Filesystem: path_separator
99using Statistics
10+ using Scratch
11+ using Serialization
1012import Test
1113import Random
1214import IOCapture
187189function runtest (:: Type{TestRecord} , f, name, init_code, color)
188190 function inner ()
189191 # generate a temporary module to execute the tests in
190- mod_name = Symbol (" Test" , rand (1 : 100 ), " Main_" , replace (name, ' /' => ' _' ))
191- mod = @eval (Main, module $ mod_name end )
192+ mod = @eval (Main, module $ (gensym (name)) end )
192193 @eval (mod, import ParallelTestRunner: Test, Random, IOCapture)
193194 @eval (mod, using . Test, . Random)
194195
@@ -243,6 +244,33 @@ function default_njobs(; cpu_threads = Sys.CPU_THREADS, free_memory = Sys.free_m
243244 return max (1 , min (jobs, memory_jobs))
244245end
245246
247+ # Historical test duration database
248+ function get_history_file (mod:: Module )
249+ scratch_dir = @get_scratch! (" durations" )
250+ return joinpath (scratch_dir, " v$(VERSION . major) .$(VERSION . minor) " , " $(nameof (mod)) .jls" )
251+ end
252+ function load_test_history (mod:: Module )
253+ history_file = get_history_file (mod)
254+ if isfile (history_file)
255+ try
256+ return deserialize (history_file)
257+ catch e
258+ @warn " Failed to load test history from $history_file " exception= e
259+ return Dict {String, Float64} ()
260+ end
261+ else
262+ return Dict {String, Float64} ()
263+ end
264+ end
265+ function save_test_history (mod:: Module , history:: Dict{String, Float64} )
266+ history_file = get_history_file (mod)
267+ try
268+ serialize (history_file, history)
269+ catch e
270+ @warn " Failed to save test history to $history_file " exception= e
271+ end
272+ end
273+
246274function test_exe ()
247275 test_exeflags = Base. julia_cmd ()
248276 filter! (test_exeflags. exec) do c
@@ -278,14 +306,15 @@ function recycle_worker(p)
278306end
279307
280308"""
281- runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord, custom_tests = Dict())
309+ runtests(mod::Module, ARGS; testfilter = Returns(true), RecordType = TestRecord, custom_tests = Dict())
282310
283311Run Julia tests in parallel across multiple worker processes.
284312
285313## Arguments
286314
287- The primary argument is a command line arguments array, typically from `Base.ARGS`. When you
288- run the tests with `Pkg.test`, this can be changed with the `test_args` keyword argument.
315+ - `mod`: The module calling runtests
316+ - `ARGS`: Command line arguments array, typically from `Base.ARGS`. When you run the tests
317+ with `Pkg.test`, this can be changed with the `test_args` keyword argument.
289318
290319Several keyword arguments are also supported:
291320
@@ -321,24 +350,24 @@ Several keyword arguments are also supported:
321350
322351```julia
323352# Run all tests with default settings
324- runtests(ARGS)
353+ runtests(MyModule, ARGS)
325354
326355# Run only tests matching "integration"
327- runtests(["integration"])
356+ runtests(MyModule, ["integration"])
328357
329358# Run with custom filter function
330- runtests(ARGS, test -> occursin("unit", test))
359+ runtests(MyModule, ARGS; testfilter = test -> occursin("unit", test))
331360
332361# Use custom test record type
333- runtests(ARGS, Returns(true), MyCustomTestRecord)
362+ runtests(MyModule, ARGS; RecordType = MyCustomTestRecord)
334363```
335364
336365## Memory Management
337366
338367Workers are automatically recycled when they exceed memory limits to prevent out-of-memory
339368issues during long test runs. The memory limit is set based on system architecture.
340369"""
341- function runtests (ARGS ; testfilter = Returns (true ), RecordType = TestRecord,
370+ function runtests (mod :: Module , ARGS ; testfilter = Returns (true ), RecordType = TestRecord,
342371 custom_tests:: Dict{String, Expr} = Dict {String, Expr} (), init_code = :(),
343372 test_worker = Returns (nothing ), stdout = Base. stdout , stderr = Base. stderr )
344373 #
@@ -417,6 +446,8 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
417446 # # finalize
418447 unique! (tests)
419448 Random. shuffle! (tests)
449+ historical_durations = load_test_history (mod)
450+ sort! (tests, by = x -> - get (historical_durations, x, Inf ))
420451
421452 # list tests, if requested
422453 if do_list
@@ -503,9 +534,6 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
503534 end
504535
505536 function update_status ()
506- # only draw the status bar on actual terminals
507- io_ctx. stdout isa Base. TTY || return
508-
509537 # only draw if we have something to show
510538 isempty (running_tests) && return
511539 completed = length (results)
@@ -529,33 +557,39 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
529557 # line 3: progress + ETA
530558 line3 = " Progress: $completed /$total tests completed"
531559 if completed > 0
532- # gather stats
533- durations_done = [end_time - start_time for (_, _, start_time, end_time) in results]
534- durations_running = [time () - start_time for (_, start_time) in values (running_tests)]
535- n_done = length (durations_done)
536- n_running = length (durations_running)
537- n_remaining = length (tests)
538- n_total = n_done + n_running + n_remaining
539-
540560 # estimate per-test time (slightly pessimistic)
561+ durations_done = [end_time - start_time for (_, _, start_time, end_time) in results]
541562 μ = mean (durations_done)
542563 σ = length (durations_done) > 1 ? std (durations_done) : 0.0
543564 est_per_test = μ + 0.5 σ
544565
545- # estimate remaining time
546- est_remaining = sum (durations_running) + n_remaining * est_per_test
566+ est_remaining = 0.0
567+ # # currently-running
568+ for (test, (_, start_time)) in running_tests
569+ elapsed = time () - start_time
570+ duration = get (historical_durations, test, est_per_test)
571+ est_remaining += max (0.0 , duration - elapsed)
572+ end
573+ # # yet-to-run
574+ for test in tests
575+ est_remaining += get (historical_durations, test, est_per_test)
576+ end
577+
547578 eta_sec = est_remaining / jobs
548579 eta_mins = round (Int, eta_sec / 60 )
549580 line3 *= " | ETA: ~$eta_mins min"
550581 end
551582
552- # display
553- clear_status ()
554- println (io_ctx. stdout , line1)
555- println (io_ctx. stdout , line2)
556- print (io_ctx. stdout , line3)
557- flush (io_ctx. stdout )
558- status_lines_visible[] = 3
583+ # only display the status bar on actual terminals
584+ # (but make sure we cover this code in CI)
585+ if io_ctx. stdout isa Base. TTY
586+ clear_status ()
587+ println (io_ctx. stdout , line1)
588+ println (io_ctx. stdout , line2)
589+ print (io_ctx. stdout , line3)
590+ flush (io_ctx. stdout )
591+ status_lines_visible[] = 3
592+ end
559593 end
560594
561595 # Message types for the printer channel
@@ -763,7 +797,7 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
763797 end
764798 end
765799
766- # construct a testset containing all results
800+ # process test results and convert into a testset
767801 function create_testset (name; start= nothing , stop= nothing , kwargs... )
768802 if start === nothing
769803 testset = Test. DefaultTestSet (name; kwargs... )
@@ -801,6 +835,7 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
801835 # decode or fake a testset
802836 if isa (result, AbstractTestRecord)
803837 testset = result. test
838+ historical_durations[testname] = stop - start
804839 else
805840 testset = create_testset (testname; start, stop)
806841 if isa (result, RemoteException) &&
@@ -855,6 +890,7 @@ function runtests(ARGS; testfilter = Returns(true), RecordType = TestRecord,
855890 Test. TESTSET_PRINT_ENABLE[] = old_print_setting
856891 end
857892 end
893+ save_test_history (mod, historical_durations)
858894
859895 # display the results
860896 println (io_ctx. stdout )
0 commit comments