From 159e425eb29a3917665d6c2e515d0b4ae3652d96 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:55:07 -0300 Subject: [PATCH] Run previously failed tests first --- src/ParallelTestRunner.jl | 28 +++++++++++++++++++++------- test/runtests.jl | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 141c746..90a40be 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -510,6 +510,18 @@ function default_njobs(; cpu_threads = Sys.CPU_THREADS, free_memory = available_ end # Historical test duration database +struct TestHistoryEntry <: AbstractFloat + duration::Float64 + failed::Bool +end +# successful tests are sorted before failed ones, so they always run first +Base.isless(a::TestHistoryEntry, b::TestHistoryEntry) = a.failed == b.failed ? a.duration < b.duration : a.failed < b.failed +Base.promote_rule(::Type{T}, ::Type{TestHistoryEntry}) where {T} = promote_type(T, Float64) +Base.promote_rule(::Type{TestHistoryEntry}, ::Type{T}) where {T} = promote_type(Float64,T) +# for compatibility with older versions of ParallelTestRunner +Base.convert(::Type{TestHistoryEntry}, duration::Number) = TestHistoryEntry(duration, false) +Base.convert(::Type{Float64}, entry::TestHistoryEntry) = entry.duration + function get_history_file(mod::Module) scratch_dir = @get_scratch!("durations") return joinpath(scratch_dir, "v$(VERSION.major).$(VERSION.minor)", "$(nameof(mod)).jls") @@ -518,16 +530,17 @@ function load_test_history(mod::Module) history_file = get_history_file(mod) if isfile(history_file) try - return deserialize(history_file) + hist::Dict{String, TestHistoryEntry} = deserialize(history_file) + return hist catch e @warn "Failed to load test history from $history_file" exception=e - return Dict{String, Float64}() + return Dict{String, TestHistoryEntry}() end else - return Dict{String, Float64}() + return Dict{String, TestHistoryEntry}() end end -function save_test_history(mod::Module, history::Dict{String, Float64}) +function save_test_history(mod::Module, history::Dict{String, TestHistoryEntry}) history_file = get_history_file(mod) try mkpath(dirname(history_file)) @@ -943,7 +956,7 @@ function runtests(mod::Module, args::ParsedArgs; tests = collect(keys(testsuite)) Random.shuffle!(tests) historical_durations = load_test_history(mod) - sort!(tests, by = x -> -get(historical_durations, x, Inf)) + sort!(tests, by = x -> get(historical_durations, x, Inf), rev = true) # determine parallelism jobs = something(args.jobs, default_njobs()) @@ -1047,7 +1060,7 @@ function runtests(mod::Module, args::ParsedArgs; ## currently-running for (test, start_time) in running_snapshot elapsed = time() - start_time - duration = get(historical_durations, test, est_per_test) + duration::Float64 = get(historical_durations, test, est_per_test) est_remaining += max(0.0, duration - elapsed) end ## yet-to-run @@ -1357,7 +1370,7 @@ function runtests(mod::Module, args::ParsedArgs; if result isa AbstractTestRecord testset = result[]::DefaultTestSet - historical_durations[testname] = stop - start + historical_durations[testname] = TestHistoryEntry(stop - start, anynonpass(testset)) else # If this test raised an exception that means the test runner itself had some problem, # so we may have hit a segfault, deserialization errors or something similar. @@ -1366,6 +1379,7 @@ function runtests(mod::Module, args::ParsedArgs; @assert result isa Exception testset = create_testset(testname; start, stop) Test.record(testset, Test.Error(:nontest_error, testname, nothing, Base.ExceptionStack(NamedTuple[(;exception = result, backtrace = [])]), LineNumberNode(1))) + historical_durations[testname] = TestHistoryEntry(Inf, true) end with_testset(testset) do diff --git a/test/runtests.jl b/test/runtests.jl index 1a65c66..2f1d9c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -790,6 +790,39 @@ end @test any(contains("--color=yes"), exe.exec) end +@testset "TestHistoryEntry" begin + import ParallelTestRunner: TestHistoryEntry + + flow = TestHistoryEntry(1,true) + fhigh = TestHistoryEntry(10,true) + + slow = TestHistoryEntry(1,false) + shigh = TestHistoryEntry(10,false) + + # irreflexive: lt(x, x) always yields false + @test !isless(flow, flow) + @test !isless(slow, slow) + @test !isless(fhigh, fhigh) + @test !isless(shigh, shigh) + + # asymmetric: if lt(x, y) yields true then lt(y, x) yields false + @test isless(flow, fhigh) + @test !isless(fhigh, flow) + @test isless(shigh, flow) + @test !isless(flow, shigh) + + # transitive: lt(x, y) && lt(y, z) implies lt(x, z) + @test isless(slow, shigh) + @test isless(shigh, flow) + @test isless(slow, flow) + + # addition/subtraction with Float64 + @test TestHistoryEntry(1, true) + 1.0 == 2.0 + @test TestHistoryEntry(3, false) - 1.0 == 2.0 + @test TestHistoryEntry(1, false) + 1.0 == 2.0 + @test TestHistoryEntry(3, true) - 1.0 == 2.0 +end + # ── Integration tests ──────────────────────────────────────────────────────── @testset "non-verbose mode" begin