@@ -23,6 +23,27 @@ function anynonpass(ts::Test.AbstractTestSet)
2323 end
2424end
2525
26+ const ID_COUNTER = Threads. Atomic {Int} (0 )
27+
28+ # Thin wrapper around Malt.Worker, to handle the stdio loop differently.
29+ struct PTRWorker <: Malt.AbstractWorker
30+ w:: Malt.Worker
31+ io:: IOBuffer
32+ id:: Int
33+ end
34+
35+ function PTRWorker (; exename= Base. julia_cmd ()[1 ], exeflags= String[], env= String[])
36+ io = IOBuffer ()
37+ wrkr = Malt. Worker (; exename, exeflags, env, monitor_stdout= false , monitor_stderr= false )
38+ stdio_loop (wrkr, io)
39+ id = ID_COUNTER[] += 1
40+ return PTRWorker (wrkr, io, id)
41+ end
42+
43+ worker_id (wrkr:: PTRWorker ) = wrkr. id
44+ Malt. isrunning (wrkr:: PTRWorker ) = Malt. isrunning (wrkr. w)
45+ Malt. stop (wrkr:: PTRWorker ) = Malt. stop (wrkr. w)
46+
2647# Always set the max rss so that if tests add large global variables (which they do) we don't make the GC's life too hard
2748if Sys. WORD_SIZE == 64
2849 const JULIA_TEST_MAXRSS_MB = 3800
@@ -57,7 +78,6 @@ abstract type AbstractTestRecord end
5778
5879struct TestRecord <: AbstractTestRecord
5980 value:: DefaultTestSet
60- output:: String # captured stdout/stderr
6181
6282 # stats
6383 time:: Float64
@@ -201,6 +221,25 @@ function print_test_crashed(wrkr, test, ctx::TestIOContext)
201221 end
202222end
203223
224+ # Adapted from `Malt._stdio_loop`
225+ function stdio_loop (worker:: Malt.Worker , io)
226+ Threads. @spawn while ! eof (worker. stdout ) && Malt. isrunning (worker)
227+ try
228+ bytes = readavailable (worker. stdout )
229+ write (io, bytes)
230+ catch
231+ break
232+ end
233+ end
234+ Threads. @spawn while ! eof (worker. stderr ) && Malt. isrunning (worker)
235+ try
236+ bytes = readavailable (worker. stderr )
237+ write (io, bytes)
238+ catch
239+ break
240+ end
241+ end
242+ end
204243
205244#
206245# entry point
@@ -236,7 +275,7 @@ function Test.finish(ts::WorkerTestSet)
236275 return ts. wrapped_ts
237276end
238277
239- function runtest (f, name, init_code, color )
278+ function runtest (f, name, init_code)
240279 function inner ()
241280 # generate a temporary module to execute the tests in
242281 mod = @eval (Main, module $ (gensym (name)) end )
@@ -252,28 +291,15 @@ function runtest(f, name, init_code, color)
252291 GC. gc (true )
253292 Random. seed! (1 )
254293
255- pipe = Pipe ()
256- pipe_initialized = Channel {Nothing} (1 )
257- reader = @async begin
258- take! (pipe_initialized)
259- read (pipe, String)
260- end
261- io = IOContext (pipe, :color => $ (color))
262- stats = redirect_stdio (; stdout = io, stderr = io) do
263- put! (pipe_initialized, nothing )
264-
265- # @testset CustomTestRecord switches the all lower-level testset to our custom testset,
266- # so we need to have two layers here such that the user-defined testsets are using `DefaultTestSet`.
267- # This also guarantees our invariant about `WorkerTestSet` containing a single `DefaultTestSet`.
268- @timed @testset WorkerTestSet " placeholder" begin
269- @testset DefaultTestSet $ name begin
270- $ f
271- end
294+ # @testset CustomTestRecord switches the all lower-level testset to our custom testset,
295+ # so we need to have two layers here such that the user-defined testsets are using `DefaultTestSet`.
296+ # This also guarantees our invariant about `WorkerTestSet` containing a single `DefaultTestSet`.
297+ stats = @timed @testset WorkerTestSet " placeholder" begin
298+ @testset DefaultTestSet $ name begin
299+ $ f
272300 end
273301 end
274- close (pipe. in)
275- output = fetch (reader)
276- (; testset= stats. value, output, stats. time, stats. bytes, stats. gctime)
302+ (; testset= stats. value, stats. time, stats. bytes, stats. gctime)
277303 end
278304
279305 # process results
@@ -392,7 +418,7 @@ function save_test_history(mod::Module, history::Dict{String, Float64})
392418 end
393419end
394420
395- function test_exe ()
421+ function test_exe (color :: Bool = false )
396422 test_exeflags = Base. julia_cmd ()
397423 filter! (test_exeflags. exec) do c
398424 ! (startswith (c, " --depwarn" ) || startswith (c, " --check-bounds" ))
@@ -401,16 +427,12 @@ function test_exe()
401427 push! (test_exeflags. exec, " --startup-file=no" )
402428 push! (test_exeflags. exec, " --depwarn=yes" )
403429 push! (test_exeflags. exec, " --project=$(Base. active_project ()) " )
430+ push! (test_exeflags. exec, " --color=$(color ? " yes" : " no" ) " )
404431 return test_exeflags
405432end
406433
407- # Map PIDs to logical worker IDs
408- # Malt doesn't have a global worker ID, and PID make printing ugly
409- const WORKER_IDS = Dict {Int32, Int32} ()
410- worker_id (wrkr) = WORKER_IDS[wrkr. proc_pid]
411-
412434"""
413- addworkers(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing)
435+ addworkers(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing, color::Bool=false )
414436
415437Add `X` worker processes.
416438To add a single worker, use [`addworker`](@ref).
@@ -419,11 +441,12 @@ To add a single worker, use [`addworker`](@ref).
419441- `env`: Vector of environment variable pairs to set for the worker process.
420442- `exename`: Custom executable to use for the worker process.
421443- `exeflags`: Custom flags to pass to the worker process.
444+ - `color`: Boolean flag to decide whether to start `julia` with `--color=yes` (if `true`) or `--color=no` (if `false`).
422445"""
423446addworkers (X; kwargs... ) = [addworker (; kwargs... ) for _ in 1 : X]
424447
425448"""
426- addworker(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing)
449+ addworker(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing; color::Bool=false )
427450
428451Add a single worker process.
429452To add multiple workers, use [`addworkers`](@ref).
@@ -432,12 +455,15 @@ To add multiple workers, use [`addworkers`](@ref).
432455- `env`: Vector of environment variable pairs to set for the worker process.
433456- `exename`: Custom executable to use for the worker process.
434457- `exeflags`: Custom flags to pass to the worker process.
458+ - `color`: Boolean flag to decide whether to start `julia` with `--color=yes` (if `true`) or `--color=no` (if `false`).
435459"""
436460function addworker (;
437461 env = Vector {Pair{String, String}} (),
438- exename = nothing , exeflags = nothing
462+ exename = nothing ,
463+ exeflags = nothing ,
464+ color:: Bool = false ,
439465 )
440- exe = test_exe ()
466+ exe = test_exe (color )
441467 if exename === nothing
442468 exename = exe[1 ]
443469 end
@@ -450,10 +476,7 @@ function addworker(;
450476 push! (env, " JULIA_NUM_THREADS" => " 1" )
451477 # Malt already sets OPENBLAS_NUM_THREADS to 1
452478 push! (env, " OPENBLAS_NUM_THREADS" => " 1" )
453-
454- wrkr = Malt. Worker (; exename, exeflags, env)
455- WORKER_IDS[wrkr. proc_pid] = length (WORKER_IDS) + 1
456- return wrkr
479+ return PTRWorker (; exename, exeflags, env)
457480end
458481
459482"""
@@ -840,7 +863,7 @@ function runtests(mod::Module, args::ParsedArgs;
840863 line3 = " Progress: $completed /$total tests completed"
841864 if completed > 0
842865 # estimate per-test time (slightly pessimistic)
843- durations_done = [end_time - start_time for (_, _, start_time, end_time) in results]
866+ durations_done = [end_time - start_time for (_, _,_, start_time, end_time) in results]
844867 μ = mean (durations_done)
845868 σ = length (durations_done) > 1 ? std (durations_done) : 0.0
846869 est_per_test = μ + 0.5 σ
@@ -970,15 +993,15 @@ function runtests(mod::Module, args::ParsedArgs;
970993 wrkr = p
971994 end
972995 if wrkr === nothing || ! Malt. isrunning (wrkr)
973- wrkr = p = addworker ()
996+ wrkr = p = addworker (; io_ctx . color )
974997 end
975998
976999 # run the test
9771000 put! (printer_channel, (:started , test, worker_id (wrkr)))
9781001 result = try
979- Malt. remote_eval_wait (Main, wrkr, :(import ParallelTestRunner))
980- Malt. remote_call_fetch (invokelatest, wrkr, runtest,
981- testsuite[test], test, init_code, io_ctx . color )
1002+ Malt. remote_eval_wait (Main, wrkr. w , :(import ParallelTestRunner))
1003+ Malt. remote_call_fetch (invokelatest, wrkr. w , runtest,
1004+ testsuite[test], test, init_code)
9821005 catch ex
9831006 if isa (ex, InterruptException)
9841007 # the worker got interrupted, signal other tasks to stop
@@ -989,7 +1012,8 @@ function runtests(mod::Module, args::ParsedArgs;
9891012 ex
9901013 end
9911014 test_t1 = time ()
992- push! (results, (test, result, test_t0, test_t1))
1015+ output = String (take! (wrkr. io))
1016+ push! (results, (test, result, output, test_t0, test_t1))
9931017
9941018 # act on the results
9951019 if result isa AbstractTestRecord
@@ -1070,10 +1094,10 @@ function runtests(mod::Module, args::ParsedArgs;
10701094 @async rmprocs (; waitfor= 0 )
10711095
10721096 # print the output generated by each testset
1073- for (testname, result, start, stop) in results
1074- if isa (result, AbstractTestRecord) && ! isempty (result . output)
1097+ for (testname, result, output, start, stop) in results
1098+ if ! isempty (output)
10751099 println (io_ctx. stdout , " \n Output generated during execution of '$testname ':" )
1076- lines = collect (eachline (IOBuffer (result . output)))
1100+ lines = collect (eachline (IOBuffer (output)))
10771101
10781102 for (i,line) in enumerate (lines)
10791103 prefix = if length (lines) == 1
@@ -1122,7 +1146,7 @@ function runtests(mod::Module, args::ParsedArgs;
11221146 function collect_results ()
11231147 with_testset (o_ts) do
11241148 completed_tests = Set {String} ()
1125- for (testname, result, start, stop) in results
1149+ for (testname, result, output, start, stop) in results
11261150 push! (completed_tests, testname)
11271151
11281152 if result isa AbstractTestRecord
0 commit comments