From 93aa2a41ff87c6e2f322cf720306ee7459fd5def Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 18 Dec 2025 03:51:27 -0500 Subject: [PATCH 1/4] Fix compatibility with Julia 1.12+ ScopedValue for TESTSET_PRINT_ENABLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Julia 1.12+, Test.TESTSET_PRINT_ENABLE changed from a mutable type to a ScopedValue. ScopedValues don't support setindex!, so the previous pattern of `Test.TESTSET_PRINT_ENABLE[] = false` causes a MethodError. This commit adds compatibility helpers: - `_TESTSET_PRINT_ENABLE_IS_SCOPED`: Compile-time constant to detect type - `_with_testset_print_disabled(f)`: Wrapper that uses @with for ScopedValue or setindex! for older Julia versions - `_ensure_testset_print_enabled()`: No-op for ScopedValue, setindex! otherwise - `_worker_disable_testset_print_expr()`: Returns appropriate quote expression All code that previously used `Test.TESTSET_PRINT_ENABLE[] = value` has been updated to use these helpers, ensuring compatibility with both old and new Julia versions. Fixes the error: MethodError: no method matching setindex!(::Base.ScopedValues.ScopedValue{Bool}, ::Bool) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/ReTestItems.jl | 152 +++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 45 deletions(-) diff --git a/src/ReTestItems.jl b/src/ReTestItems.jl index ee0c121..6bf54f6 100644 --- a/src/ReTestItems.jl +++ b/src/ReTestItems.jl @@ -23,6 +23,54 @@ else const errmon = identity end +# Julia 1.12+ changed Test.TESTSET_PRINT_ENABLE from a mutable type to a ScopedValue. +# ScopedValues don't support setindex!, so we need different strategies for different Julia versions. +const _TESTSET_PRINT_ENABLE_IS_SCOPED = @static if isdefined(Base, :ScopedValues) + Test.TESTSET_PRINT_ENABLE isa Base.ScopedValues.ScopedValue +else + false +end + +# Helper to run code with Test.TESTSET_PRINT_ENABLE set to false +# For older Julia: uses setindex! to temporarily disable printing +# For Julia 1.12+: uses Base.ScopedValues.with to create a new scope +function _with_testset_print_disabled(f) + @static if _TESTSET_PRINT_ENABLE_IS_SCOPED + Base.ScopedValues.with(f, Test.TESTSET_PRINT_ENABLE => false) + else + old = Test.TESTSET_PRINT_ENABLE[] + Test.TESTSET_PRINT_ENABLE[] = false + try + return f() + finally + Test.TESTSET_PRINT_ENABLE[] = old + end + end +end + +# Helper to ensure Test.TESTSET_PRINT_ENABLE is true (for post-test cleanup) +# For older Julia: uses setindex! +# For Julia 1.12+: ScopedValue is already true by default, so this is a no-op +function _ensure_testset_print_enabled() + @static if !_TESTSET_PRINT_ENABLE_IS_SCOPED + Test.TESTSET_PRINT_ENABLE[] = true + end +end + +# Generate a quoted expression that disables test printing on a worker +# For older Julia: generates `Test.TESTSET_PRINT_ENABLE[] = false` +# For Julia 1.12+: generates nothing (workers will use @with blocks for test execution) +function _worker_disable_testset_print_expr() + @static if _TESTSET_PRINT_ENABLE_IS_SCOPED + # For ScopedValues, we can't set this globally. Instead, test execution + # on workers should be wrapped in the appropriate scope. + # Return a no-op expression. + :(nothing) + else + :(Test.TESTSET_PRINT_ENABLE[] = false) + end +end + # Used by failures_first to sort failures before unseen before passes. @enum _TEST_STATUS::UInt8 begin _FAILED = 0 @@ -430,40 +478,43 @@ function _runtests_in_current_env( end if nworkers == 0 length(cfg.worker_init_expr.args) > 0 && error("worker_init_expr is set, but will not run because number of workers is 0.") - # This is where we disable printing for the serial executor case. - Test.TESTSET_PRINT_ENABLE[] = false - ctx = TestContext(proj_name, ntestitems) - # we use a single TestSetupModules - ctx.setups_evaled = TestSetupModules() - for (i, testitem) in enumerate(testitems.testitems) - testitem.workerid[] = Libc.getpid() - testitem.eval_number[] = i - @atomic :monotonic testitems.count += 1 - run_number = 1 - max_runs = 1 + max(cfg.retries, testitem.retries) - is_non_pass = false - while run_number ≤ max_runs - res = runtestitem(testitem, ctx; cfg.test_end_expr, cfg.verbose_results, cfg.logs, failfast=cfg.testitem_failfast) - ts = res.testset - print_errors_and_captured_logs(testitem, run_number; cfg.logs) - report_empty_testsets(testitem, ts) - if cfg.gc_between_testitems - @debugv 2 "Running GC" - GC.gc(true) + # Disable test printing for the serial executor case. + # Uses _with_testset_print_disabled to handle both old Julia (setindex!) and + # new Julia 1.12+ (ScopedValue) where TESTSET_PRINT_ENABLE changed type. + _with_testset_print_disabled() do + ctx = TestContext(proj_name, ntestitems) + # we use a single TestSetupModules + ctx.setups_evaled = TestSetupModules() + for (i, testitem) in enumerate(testitems.testitems) + testitem.workerid[] = Libc.getpid() + testitem.eval_number[] = i + @atomic :monotonic testitems.count += 1 + run_number = 1 + max_runs = 1 + max(cfg.retries, testitem.retries) + is_non_pass = false + while run_number ≤ max_runs + res = runtestitem(testitem, ctx; cfg.test_end_expr, cfg.verbose_results, cfg.logs, failfast=cfg.testitem_failfast) + ts = res.testset + print_errors_and_captured_logs(testitem, run_number; cfg.logs) + report_empty_testsets(testitem, ts) + if cfg.gc_between_testitems + @debugv 2 "Running GC" + GC.gc(true) + end + testitem.is_non_pass[] = is_non_pass = any_non_pass(ts) + if is_non_pass && run_number != max_runs + run_number += 1 + @info "Retrying $(repr(testitem.name)). Run=$run_number." + else + break + end end - testitem.is_non_pass[] = is_non_pass = any_non_pass(ts) - if is_non_pass && run_number != max_runs - run_number += 1 - @info "Retrying $(repr(testitem.name)). Run=$run_number." - else + if cfg.failfast && is_non_pass + cancel!(testitems) + print_failfast_cancellation(testitem) break end end - if cfg.failfast && is_non_pass - cancel!(testitems) - print_failfast_cancellation(testitem) - break - end end elseif !isempty(testitems.testitems) # Try to free up memory on the coordinator before starting workers, since @@ -496,7 +547,7 @@ function _runtests_in_current_env( end end end - Test.TESTSET_PRINT_ENABLE[] = true # reenable printing so our `finish` prints + _ensure_testset_print_enabled() # reenable printing so our `finish` prints # Let users know if tests are done, and if all of them ran (or if we failed fast). # Print this above the final report as there might have been other logs printed # since a failfast-cancellation was printed, but print it ASAP after tests finish @@ -507,7 +558,7 @@ function _runtests_in_current_env( @debugv 1 "Calling Test.finish(testitems)" Test.finish(testitems) # print summary of total passes/failures/errors finally - Test.TESTSET_PRINT_ENABLE[] = true + _ensure_testset_print_enabled() @debugv 1 "Cleaning up test setup logs" foreach(Iterators.filter(endswith(".log"), readdir(RETESTITEMS_TEMP_FOLDER[], join=true))) do logfile try @@ -530,10 +581,14 @@ function start_worker(proj_name, nworker_threads::String, worker_init_expr::Expr i = worker_num == nothing ? "" : " $worker_num" proj = Base.active_project() # remote_fetch here because we want to make sure the worker is all setup before starting to eval testitems + # Build the worker initialization expression + # For Julia 1.12+, TESTSET_PRINT_ENABLE is a ScopedValue and can't be set with setindex! + # The _worker_disable_testset_print_expr() returns the appropriate expression for the Julia version + disable_print_expr = _worker_disable_testset_print_expr() remote_fetch(w, quote Base.set_active_project($proj) using ReTestItems, Test - Test.TESTSET_PRINT_ENABLE[] = false + $disable_print_expr const GLOBAL_TEST_CONTEXT = ReTestItems.TestContext($proj_name, $ntestitems) GLOBAL_TEST_CONTEXT.setups_evaled = ReTestItems.TestSetupModules() nthreads_str = $nworker_threads @@ -610,21 +665,24 @@ function record_worker_terminated!(testitem, worker::Worker, run_number::Int) end function record_test_error!(testitem, msg, elapsed_seconds::Real=0.0) - Test.TESTSET_PRINT_ENABLE[] = false - ts = DefaultTestSet(testitem.name) - err = ErrorException(msg) - Test.record(ts, Test.Error(:nontest_error, Test.Expr(:tuple), err, - Base.ExceptionStack([(exception=err, backtrace=Union{Ptr{Nothing}, Base.InterpreterIP}[])]), - LineNumberNode(testitem.line, testitem.file))) - try - Test.finish(ts) - catch e2 - e2 isa TestSetException || rethrow() + # Wrap in _with_testset_print_disabled to handle both old Julia (setindex!) and + # new Julia 1.12+ (ScopedValue) where TESTSET_PRINT_ENABLE changed type. + ts = _with_testset_print_disabled() do + ts = DefaultTestSet(testitem.name) + err = ErrorException(msg) + Test.record(ts, Test.Error(:nontest_error, Test.Expr(:tuple), err, + Base.ExceptionStack([(exception=err, backtrace=Union{Ptr{Nothing}, Base.InterpreterIP}[])]), + LineNumberNode(testitem.line, testitem.file))) + try + Test.finish(ts) + catch e2 + e2 isa TestSetException || rethrow() + end + ts end # Since we're manually constructing a TestSet here to report tests that already ran and # were killed, we need to manually set how long those tests were running (if known). ts.time_end = ts.time_start + elapsed_seconds - Test.TESTSET_PRINT_ENABLE[] = true push!(testitem.testsets, ts) push!(testitem.stats, PerfStats()) # No data since testitem didn't complete return testitem @@ -648,7 +706,11 @@ function manage_worker( end testitem.workerid[] = worker.pid timeout = something(testitem.timeout, cfg.testitem_timeout) - fut = remote_eval(worker, :(ReTestItems.runtestitem($testitem, GLOBAL_TEST_CONTEXT; test_end_expr=$(QuoteNode(cfg.test_end_expr)), verbose_results=$(cfg.verbose_results), logs=$(QuoteNode(cfg.logs)), failfast=$(cfg.testitem_failfast)))) + # Wrap runtestitem in _with_testset_print_disabled to handle both old Julia (setindex!) + # and new Julia 1.12+ (ScopedValue) where TESTSET_PRINT_ENABLE changed type. + fut = remote_eval(worker, :(ReTestItems._with_testset_print_disabled() do + ReTestItems.runtestitem($testitem, GLOBAL_TEST_CONTEXT; test_end_expr=$(QuoteNode(cfg.test_end_expr)), verbose_results=$(cfg.verbose_results), logs=$(QuoteNode(cfg.logs)), failfast=$(cfg.testitem_failfast)) + end)) max_runs = 1 + max(cfg.retries, testitem.retries) try timer = Timer(timeout) do tm From e9d6057812b4c13055c74935281323ee519ba2c9 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Thu, 18 Dec 2025 03:58:58 -0500 Subject: [PATCH 2/4] Update CI to test Julia 1.8, 1.10, 1.11, 1.12, and pre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expanded the Julia version matrix to include: - 1.8 (existing) - 1.10 (existing) - 1.11 (new) - 1.12 (replaces ~1.12.0-rc1) - pre (nightly, to catch future breaking changes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/CI.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 912f0c5..ad2bf9f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,7 +19,9 @@ jobs: version: - '1.8' - '1.10' - - '~1.12.0-rc1' + - '1.11' + - '1.12' + - 'pre' os: - ubuntu-latest - macOS-latest From ba8d802b66647bd5a67ce2ff2a5fd64ddf3a500f Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Thu, 18 Dec 2025 11:10:18 +0000 Subject: [PATCH 3/4] Update .github/workflows/CI.yml --- .github/workflows/CI.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ad2bf9f..e7a9f3f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -21,7 +21,6 @@ jobs: - '1.10' - '1.11' - '1.12' - - 'pre' os: - ubuntu-latest - macOS-latest From db2e984d2960783c7b33b3578462200e093f293d Mon Sep 17 00:00:00 2001 From: Nick Robinson Date: Thu, 18 Dec 2025 11:10:25 +0000 Subject: [PATCH 4/4] Update .github/workflows/CI.yml --- .github/workflows/CI.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e7a9f3f..9728974 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,7 +19,6 @@ jobs: version: - '1.8' - '1.10' - - '1.11' - '1.12' os: - ubuntu-latest