From b91b1e2b1eeee8f28683c8c95df898d0fe16bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Sun, 1 Mar 2026 17:34:35 +0000 Subject: [PATCH 1/6] Stop workers when there are no more tests to run --- src/ParallelTestRunner.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 5bdd9ab..c96de11 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -1059,7 +1059,14 @@ function runtests(mod::Module, args::ParsedArgs; Malt.stop(wrkr) end - delete!(running_tests, test) + Base.@lock test_lock begin + # Stop worker if there are no more tests remaining to run. + if isempty(tests) && !isnothing(wrkr) && Malt.isrunning(wrkr) + Malt.stop(wrkr) + end + + delete!(running_tests, test) + end end end) end From 7cf6f3c45e556a86eb627777fb42bc06b55d12ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Sun, 1 Mar 2026 19:46:21 +0000 Subject: [PATCH 2/6] Add tests for checking workers are stopped at the end --- test/runtests.jl | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 2317eff..93bc8b2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -401,4 +401,88 @@ end @test ParallelTestRunner.ID_COUNTER[] == old_id_counter + njobs end +# Count direct child processes of current process (for default-worker test). +# Returns -1 if unsupported so the test can be skipped. +function _count_child_pids() + pid = getpid() + if Sys.isunix() && !isnothing(Sys.which("ps")) + pids = Int[] + out = try + readchomp(`ps -o ppid= -o pid= -A`) + catch + return -1 + end + lines = split(out, '\n') + for line in lines + m = match(r" *(\d+) +(\d+)", line) + if !isnothing(m) + if parse(Int, m[1]) == pid + push!(pids, parse(Int, m[2])) + end + end + end + # The output of `ps` always contains `ps` itself because it's spawn by + # the current process, so we subtract one to always exclude it. + return length(pids) - 1 + else + return -1 + end +end + +# Issue . +@testset "default workers stopped at end" begin + # Use default workers (no test_worker) so the framework creates and should stop them. + # More tests than workers so some tasks finish early and must stop their worker. + testsuite = Dict( + "t1" => :(), + "t2" => :(), + "t3" => :(), + "t4" => :(), + "t5" => :(), + "t6" => :(), + ) + before = _count_child_pids() + if before < 0 + # Counting child PIDs not supported on this platform + @test_broken false + else + old_id_counter = ParallelTestRunner.ID_COUNTER[] + njobs = 2 + runtests(ParallelTestRunner, ["--jobs=$(njobs)", "--verbose"]; testsuite, stdout=devnull, stderr=devnull) + @test ParallelTestRunner.ID_COUNTER[] == old_id_counter + njobs + # Allow a moment for worker processes to exit + for _ in 1:50 + sleep(0.1) + after = _count_child_pids() + after >= 0 && after <= before && break + end + after = _count_child_pids() + @test after >= 0 + @test after == before + end +end + +# Custom workers are handled differently: +# . +# But we still want to make sure they're terminated at the end. +@testset "custom workers stopped at end" begin + testsuite = Dict( + "a" => :(), + "b" => :(), + "c" => :(), + "d" => :(), + "e" => :(), + "f" => :(), + ) + procs = Base.Process[] + procs_lock = ReentrantLock() + function test_worker(name) + wrkr = addworker() + Base.@lock procs_lock push!(procs, wrkr.w.proc) + return wrkr + end + runtests(ParallelTestRunner, Base.ARGS; test_worker, testsuite, stdout=devnull, stderr=devnull) + @test all(!Base.process_running, procs) +end + end From 1c9cdbc8c63dfb4eb3a7e6353c89cc23ee8d8d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Sun, 1 Mar 2026 20:36:03 +0000 Subject: [PATCH 3/6] Slightly simplify `_count_child_pids` --- test/runtests.jl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 93bc8b2..91ca544 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -408,22 +408,24 @@ function _count_child_pids() if Sys.isunix() && !isnothing(Sys.which("ps")) pids = Int[] out = try + # Suggested in . readchomp(`ps -o ppid= -o pid= -A`) catch return -1 end lines = split(out, '\n') + # The output of `ps` always contains `ps` itself because it's spawned by + # the current process, so we subtract one to always exclude it. + count = -1 for line in lines m = match(r" *(\d+) +(\d+)", line) if !isnothing(m) if parse(Int, m[1]) == pid - push!(pids, parse(Int, m[2])) + count += 1 end end end - # The output of `ps` always contains `ps` itself because it's spawn by - # the current process, so we subtract one to always exclude it. - return length(pids) - 1 + return count else return -1 end @@ -444,11 +446,12 @@ end before = _count_child_pids() if before < 0 # Counting child PIDs not supported on this platform - @test_broken false + @test_skip false else old_id_counter = ParallelTestRunner.ID_COUNTER[] njobs = 2 runtests(ParallelTestRunner, ["--jobs=$(njobs)", "--verbose"]; testsuite, stdout=devnull, stderr=devnull) + # Make sure we didn't spawn more workers than expected. @test ParallelTestRunner.ID_COUNTER[] == old_id_counter + njobs # Allow a moment for worker processes to exit for _ in 1:50 From 3530d91414be0c52042781a8906a594a405a404f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Sun, 1 Mar 2026 22:47:56 +0000 Subject: [PATCH 4/6] Add test for checking long-running test is running alone --- test/runtests.jl | 57 ++++++++++++++++++++++-------------------------- test/utils.jl | 30 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 test/utils.jl diff --git a/test/runtests.jl b/test/runtests.jl index 91ca544..467ccdd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,6 +3,8 @@ using Test cd(@__DIR__) +include(joinpath(@__DIR__, "utils.jl")) + @testset "ParallelTestRunner" verbose=true begin @testset "basic use" begin @@ -401,35 +403,6 @@ end @test ParallelTestRunner.ID_COUNTER[] == old_id_counter + njobs end -# Count direct child processes of current process (for default-worker test). -# Returns -1 if unsupported so the test can be skipped. -function _count_child_pids() - pid = getpid() - if Sys.isunix() && !isnothing(Sys.which("ps")) - pids = Int[] - out = try - # Suggested in . - readchomp(`ps -o ppid= -o pid= -A`) - catch - return -1 - end - lines = split(out, '\n') - # The output of `ps` always contains `ps` itself because it's spawned by - # the current process, so we subtract one to always exclude it. - count = -1 - for line in lines - m = match(r" *(\d+) +(\d+)", line) - if !isnothing(m) - if parse(Int, m[1]) == pid - count += 1 - end - end - end - return count - else - return -1 - end -end # Issue . @testset "default workers stopped at end" begin @@ -441,7 +414,17 @@ end "t3" => :(), "t4" => :(), "t5" => :(), - "t6" => :(), + "t6" => quote + # Make this test run longer than the others so that it runs alone... + sleep(5) + children = _count_child_pids($(getpid())) + # ...then check there's only one worker still running. WARNING: this test may be + # flaky on very busy systems, if at this point some of the other tests are still + # running, hope for the best. + if children >= 0 + @test children == 1 + end + end, ) before = _count_child_pids() if before < 0 @@ -450,7 +433,19 @@ end else old_id_counter = ParallelTestRunner.ID_COUNTER[] njobs = 2 - runtests(ParallelTestRunner, ["--jobs=$(njobs)", "--verbose"]; testsuite, stdout=devnull, stderr=devnull) + io = IOBuffer() + ioc = IOContext(io, :color => true) + try + runtests(ParallelTestRunner, ["--jobs=$(njobs)", "--verbose"]; + testsuite, stdout=ioc, stderr=ioc, init_code=:(include($(joinpath(@__DIR__, "utils.jl"))))) + catch + # Show output in case of failure, to help debugging. + output = String(take!(io)) + printstyled(stderr, "Output of failed test >>>>>>>>>>>>>>>>>>>>\n", color=:red, bold=true) + println(stderr, output) + printstyled(stderr, "End of output <<<<<<<<<<<<<<<<<<<<<<<<<<<<\n", color=:red, bold=true) + rethrow() + end # Make sure we didn't spawn more workers than expected. @test ParallelTestRunner.ID_COUNTER[] == old_id_counter + njobs # Allow a moment for worker processes to exit diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 0000000..ef6cc95 --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,30 @@ +# Count direct child processes of current process (for default-worker test). +# Returns -1 if unsupported so the test can be skipped. +function _count_child_pids(pid = getpid()) + if Sys.isunix() && !isnothing(Sys.which("ps")) + pids = Int[] + out = try + # Suggested in . + readchomp(`ps -o ppid= -o pid= -A`) + catch + return -1 + end + lines = split(out, '\n') + # The output of `ps` for the current process always contains `ps` itself + # because it's spawned by the current process, in that case we subtract + # one to always exclude it, otherwise if we're getting the number of + # children of another process we start from 0. + count = pid == getpid() : -1 : 0 + for line in lines + m = match(r" *(\d+) +(\d+)", line) + if !isnothing(m) + if parse(Int, m[1]) == pid + count += 1 + end + end + end + return count + else + return -1 + end +end From d31bca95d87273dbfb16c0f39693254ab392c054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mos=C3=A8=20Giordano?= Date: Mon, 2 Mar 2026 12:56:52 +0000 Subject: [PATCH 5/6] Delete entry from `running_tests` before stopping the worker --- src/ParallelTestRunner.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index c96de11..546a1c2 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -1060,12 +1060,12 @@ function runtests(mod::Module, args::ParsedArgs; end Base.@lock test_lock begin + delete!(running_tests, test) + # Stop worker if there are no more tests remaining to run. if isempty(tests) && !isnothing(wrkr) && Malt.isrunning(wrkr) Malt.stop(wrkr) end - - delete!(running_tests, test) end end end) From e9d860800aea838e08bf24a1a943d4b0c1582257 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Mon, 2 Mar 2026 14:10:01 +0100 Subject: [PATCH 6/6] Simplify. --- src/ParallelTestRunner.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 546a1c2..636a9b6 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -1061,13 +1061,11 @@ function runtests(mod::Module, args::ParsedArgs; Base.@lock test_lock begin delete!(running_tests, test) - - # Stop worker if there are no more tests remaining to run. - if isempty(tests) && !isnothing(wrkr) && Malt.isrunning(wrkr) - Malt.stop(wrkr) - end end end + if p !== nothing + Malt.stop(p) + end end) end