diff --git a/test/test_testserver_precompile.jl b/test/test_testserver_precompile.jl new file mode 100644 index 0000000..9bd350d --- /dev/null +++ b/test/test_testserver_precompile.jl @@ -0,0 +1,42 @@ +@testitem "The test process environment precompiles cleanly" begin + # `TestItemServer` assembles the vendored packages by hand, and an override that + # *redefines* one of their methods rather than adding a more specific one is a method + # overwrite — which Julia rejects outright while a package precompiles. + # + # Nothing else in this suite notices directly: precompilation is warn-only on the path a + # test process takes, so the package still loads. What it costs is a full source reload + # of the vendored stack in every test process (tens of seconds each) and a precompile + # banner on its stderr, which then lands in whatever a test item captures. Those + # second-order effects are how it last surfaced, in assertions about process output. + # Assert the cache is actually built instead. + env_dir = normpath(joinpath(@__DIR__, "..", "testprocess", "environments")) + versioned = joinpath(env_dir, "v$(VERSION.major).$(VERSION.minor)") + project = isdir(versioned) ? versioned : joinpath(env_dir, "fallback") + + # `Base.compilecache` rather than `Pkg.precompile`: it rebuilds unconditionally, so a + # cache some other test item already wrote cannot short-circuit the check, and it reports + # a failure as a `Core.PrecompilableError` return value — `Pkg.precompile` reports one as + # a `?` in its progress list and still exits 0. + code = """ + result = Base.compilecache(Base.identify_package("TestItemServer")) + result isa Tuple || exit(1) + """ + julia = joinpath(Sys.BINDIR, Base.julia_exename()) + cmd = `$julia --startup-file=no --history-file=no --project=$project -e $code` + + io = IOBuffer() + process = run(pipeline(ignorestatus(cmd), stdout=io, stderr=io)) + output = String(take!(io)) + + ok = success(process) && + !occursin("Method overwriting is not permitted", output) && + !occursin("overwritten at", output) + if !ok + println("── precompiling TestItemServer in $project ──") + println(output) + end + + @test success(process) + @test !occursin("Method overwriting is not permitted", output) + @test !occursin("overwritten at", output) +end diff --git a/testprocess/TestItemServer/src/pkg_imports.jl b/testprocess/TestItemServer/src/pkg_imports.jl index d6f39be..3cd473d 100644 --- a/testprocess/TestItemServer/src/pkg_imports.jl +++ b/testprocess/TestItemServer/src/pkg_imports.jl @@ -90,6 +90,24 @@ module Revise using ...LoweredCodeUtils: next_or_nothing!, callee_matches include("../../../packages/Revise/src/packagedef.jl") + + # Revise seeds the cache source hash with a `UInt64`, while the seed `hash` takes is a + # `UInt` — 32 bits wide on a 32 bit platform. Its package callback dies there with + # `MethodError: no method matching hash(::UInt64, ::UInt64)`, and every test item that + # loads a package in that process reports the failed callback as its own error. + # + # `packages/` holds git subtrees that are never edited by hand, so the method is added + # here instead, where this module is assembled. It is deliberately *more specific* than + # the vendored `cache_src_id(inc)` rather than a redefinition of it: redefining is + # method overwriting, which Julia rejects outright while this package precompiles. Both + # call sites pass a `Base.CacheHeaderIncludes`, so this one wins dispatch. `inc.hash` is + # a `UInt32`, so widening it to `UInt` is exact. Guarded like the definition it shadows + # (https://github.com/JuliaLang/julia/pull/49866); older versions hash `inc.mtime` and + # are unaffected. Drop this once the fix is in a released Revise. + @static if Sys.WORD_SIZE == 32 && VERSION >= v"1.11.0-DEV.683" && + isdefined(Base, :CacheHeaderIncludes) + cache_src_id(inc::Base.CacheHeaderIncludes) = hash(inc.fsize, UInt(inc.hash)) + end elseif VERSION >= v"1.6.0" using ..OrderedCollections using ..LoweredCodeUtils