Skip to content

Commit b5c3434

Browse files
authored
Store historical test duration. (#28)
Use it for a more accurate ETA, and to order tests (longest first).
1 parent 96da192 commit b5c3434

File tree

4 files changed

+81
-41
lines changed

4 files changed

+81
-41
lines changed

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
99
IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
1010
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1111
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
12+
Scratch = "6c6a2e73-6563-6170-7368-637461726353"
13+
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
1214
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1315
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1416

@@ -18,6 +20,8 @@ Distributed = "1"
1820
IOCapture = "0.2.5"
1921
Printf = "1"
2022
Random = "1"
23+
Scratch = "1.3.0"
24+
Serialization = "1"
2125
Statistics = "1"
2226
Test = "1"
2327
julia = "1.10"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Then in your `test/runtests.jl` add:
3030
```julia
3131
using ParallelTestRunner
3232

33-
runtests(ARGS)
33+
runtests(MyModule, ARGS)
3434
```
3535

3636
### Filtering
@@ -45,7 +45,7 @@ function testfilter(test)
4545
return true
4646
end
4747

48-
runtests(ARGS; testfilter)
48+
runtests(MyModule, ARGS; testfilter)
4949
```
5050

5151
### Provide defaults
@@ -59,7 +59,7 @@ const init_code = quote
5959
using MyPackage
6060
end
6161

62-
runtests(ARGS; init_code)
62+
runtests(MyModule, ARGS; init_code)
6363
```
6464

6565
## Packages using ParallelTestRunner.jl

src/ParallelTestRunner.jl

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ using Dates
77
using Printf: @sprintf
88
using Base.Filesystem: path_separator
99
using Statistics
10+
using Scratch
11+
using Serialization
1012
import Test
1113
import Random
1214
import IOCapture
@@ -187,8 +189,7 @@ end
187189
function 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))
244245
end
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+
246274
function test_exe()
247275
test_exeflags = Base.julia_cmd()
248276
filter!(test_exeflags.exec) do c
@@ -278,14 +306,15 @@ function recycle_worker(p)
278306
end
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
283311
Run 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
290319
Several 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
338367
Workers are automatically recycled when they exceed memory limits to prevent out-of-memory
339368
issues 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)

test/runtests.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cd(@__DIR__)
88
@testset "basic test" begin
99
io = IOBuffer()
1010
io_color = IOContext(io, :color => true)
11-
runtests(["--verbose"]; stdout=io_color, stderr=io_color)
11+
runtests(ParallelTestRunner, ["--verbose"]; stdout=io_color, stderr=io_color)
1212
str = String(take!(io))
1313

1414
println()
@@ -39,7 +39,7 @@ end
3939
)
4040

4141
io = IOBuffer()
42-
runtests(["--verbose"]; init_code, custom_tests, stdout=io, stderr=io)
42+
runtests(ParallelTestRunner, ["--verbose"]; init_code, custom_tests, stdout=io, stderr=io)
4343

4444
str = String(take!(io))
4545
@test contains(str, r"basic .+ started at")
@@ -64,7 +64,7 @@ end
6464
)
6565

6666
io = IOBuffer()
67-
runtests(["--verbose"]; test_worker, custom_tests, stdout=io, stderr=io)
67+
runtests(ParallelTestRunner, ["--verbose"]; test_worker, custom_tests, stdout=io, stderr=io)
6868

6969
str = String(take!(io))
7070
@test contains(str, r"basic .+ started at")
@@ -82,7 +82,7 @@ end
8282

8383
io = IOBuffer()
8484
@test_throws Test.FallbackTestSetException("Test run finished with errors") begin
85-
runtests(["--verbose"]; custom_tests, stdout=io, stderr=io)
85+
runtests(ParallelTestRunner, ["--verbose"]; custom_tests, stdout=io, stderr=io)
8686
end
8787

8888
str = String(take!(io))
@@ -102,7 +102,7 @@ end
102102

103103
io = IOBuffer()
104104
@test_throws Test.FallbackTestSetException("Test run finished with errors") begin
105-
runtests(["--verbose"]; custom_tests, stdout=io, stderr=io)
105+
runtests(ParallelTestRunner, ["--verbose"]; custom_tests, stdout=io, stderr=io)
106106
end
107107

108108
str = String(take!(io))
@@ -123,7 +123,7 @@ end
123123

124124
io = IOBuffer()
125125
@test_throws Test.FallbackTestSetException("Test run finished with errors") begin
126-
runtests(["--verbose"]; custom_tests, stdout=io, stderr=io)
126+
runtests(ParallelTestRunner, ["--verbose"]; custom_tests, stdout=io, stderr=io)
127127
end
128128

129129
str = String(take!(io))
@@ -141,7 +141,7 @@ end
141141
)
142142

143143
io = IOBuffer()
144-
runtests(["--verbose"]; custom_tests, stdout=io, stderr=io)
144+
runtests(ParallelTestRunner, ["--verbose"]; custom_tests, stdout=io, stderr=io)
145145

146146
str = String(take!(io))
147147
@test contains(str, r"output .+ started at")

0 commit comments

Comments
 (0)