# REVISE: DO NOT PARSE   # For people with JULIA_REVISE_INCLUDE=1
using Revise
using Revise.CodeTracking
using Revise.CodeTracking: MethodInfoKey
using Revise.JuliaInterpreter
using Test

@test isempty(detect_ambiguities(Revise))

using Dates, Distributed, InteractiveUtils, Pkg, REPL, UUIDs, Unicode
import LibGit2
using Revise.OrderedCollections: OrderedSet
using Test: collect_test_logs
using Logging: ConsoleLogger, with_logger
using Base.CoreLogging: Debug,Info

# Some test cases (especially those that redirect stderr during precompilation)
# assume that dependency packages are already precompiled, so we make an
# explicit `precompile()` call here to ensure this.
Pkg.precompile()

if isdefined(Test, :detect_closure_boxes)
    @test isempty(Test.detect_closure_boxes(Revise))
end

# In addition to using this for the "More arg-modifying macros" test below,
# this package is used on CI to test what happens when you have multiple
# *.ji files for the package.
using EponymTuples

# # Also ensure packages that we'll `@require` are precompiled, as otherwise Pkg `@info`
# # may contaminate the log and cause test failures.
# Pkg.precompile(["EndpointRanges", "CatIndices", "IndirectArrays", "RoundingIntegers", "UnsafeArrays"])

include("common.jl")

throwing_function(bt) = bt[2]

# A junk module that we can evaluate into
module ReviseTestPrivate
struct Inner
    x::Float64
end

macro changeto1(args...)
    return 1
end

macro donothing(ex)
    esc(ex)
end

macro addint(ex)
    :($(esc(ex))::$(esc(Int)))
end

macro empty_function(name)
    return esc(quote
        function $name end
    end)
end

# The following two submodules are for testing #199
module A
f(x::Int) = 1
end

module B
f(x::Int) = 1
module Core end
end

end

function private_module()
    modname = gensym()
    Core.eval(ReviseTestPrivate, :(module $modname end))
end

sig_type_exprs(ex) = Revise.sig_type_exprs(Main, ex)   # just for testing purposes

# accommodate changes in Dict printing w/ Julia version
const pair_op_compact = let io = IOBuffer()
    print(IOContext(io, :compact=>true), Dict(1=>2))
    String(take!(io))[7:end-2]
end

const issue639report = []

module TypeInfoTracking end

function lower_and_track(ex::Expr)
    lwr = Meta.lower(TypeInfoTracking, ex)
    frame = Frame(TypeInfoTracking, lwr.args[1])
    exinfo = Revise.ExInfo(ex)
    Revise._methods_by_execution!(
        JuliaInterpreter.RecursiveInterpreter(), exinfo, frame, trues(length(frame.framecode.src.code)); mode=:sigs)
    return exinfo
end

@testset "Revise" begin
    do_test("PkgData") && @testset "PkgData" begin
        # Related to #358
        id = Base.PkgId(Main)
        pd = Revise.PkgData(id)
        @test isempty(Revise.basedir(pd))
    end

    do_test("Package contents") && @testset "Package contents" begin
        id = Base.PkgId(EponymTuples)
        path, mods_files_mtimes = Revise.pkg_fileinfo(id)
        @test occursin("EponymTuples", path)
    end

    do_test("LineSkipping") && @testset "LineSkipping" begin
        rex = Revise.RelocatableExpr(quote
                                    f(x) = x^2
                                    g(x) = sin(x)
                                    end)
        @test length(rex.ex.args) == 4  # including the line number expressions
        exs = collectexprs(rex)
        @test length(exs) == 2
        @test isequal(exs[1], Revise.RelocatableExpr(:(f(x) = x^2)))
        @test hash(exs[1]) == hash(Revise.RelocatableExpr(:(f(x) = x^2)))
        @test !isequal(exs[2], Revise.RelocatableExpr(:(f(x) = x^2)))
        @test isequal(exs[2], Revise.RelocatableExpr(:(g(x) = sin(x))))
        @test !isequal(exs[1], Revise.RelocatableExpr(:(g(x) = sin(x))))
        @test string(rex) == """
            quote
                f(x) = begin
                        x ^ 2
                    end
                g(x) = begin
                        sin(x)
                    end
            end"""
    end

    do_test("Equality and hashing") && @testset "Equality and hashing" begin
        # issue #233
        @test  isequal(Revise.RelocatableExpr(:(x = 1)), Revise.RelocatableExpr(:(x = 1)))
        @test !isequal(Revise.RelocatableExpr(:(x = 1)), Revise.RelocatableExpr(:(x = 1.0)))
        @test hash(Revise.RelocatableExpr(:(x = 1))) == hash(Revise.RelocatableExpr(:(x = 1)))
        @test hash(Revise.RelocatableExpr(:(x = 1))) != hash(Revise.RelocatableExpr(:(x = 1.0)))
        @test hash(Revise.RelocatableExpr(:(x = 1))) != hash(Revise.RelocatableExpr(:(x = 2)))
    end

    do_test("Parse errors") && @testset "Parse errors" begin
        md = Revise.ModuleExprsInfos(Main)
        errtype = Base.VERSION < v"1.10" ? LoadError : Base.Meta.ParseError
        @test_throws errtype parse_source!(md, """
            begin # this block should parse correctly, cf. issue #109

            end
            f(x) = 1
            g(x) = 2
            h{x) = 3  # error
            k(x) = 4
            """, "test", Main)

        # Issue #448
        testdir = newtestdir()
        file = joinpath(testdir, "badfile.jl")
        write(file, """
            function g()
                while t
                c =
                k
            end
            """)
        try
            includet(file)
        catch err
            @test isa(err, errtype)
            if  Base.VERSION < v"1.10"
                @test err.file == file
                @test endswith(err.error, "requires end")
            else
                @test occursin("Expected `end`", err.msg)
            end
        end
    end

    do_test("REPL input") && @testset "REPL input" begin
        # issue #573
        retex = Revise.revise_first(nothing)
        @test retex.head === :toplevel
        @test length(retex.args) == 2 && retex.args[end] === nothing
    end

    do_test("Signature extraction") && @testset "Signature extraction" begin
        jidir = dirname(dirname(pathof(JuliaInterpreter)))
        scriptfile = joinpath(jidir, "test", "toplevel_script.jl")
        modex = :(module Toplevel include($scriptfile) end)
        mod = eval(modex)
        mexs = parse_source(scriptfile, mod)
        Revise.instantiate_sigs!(mexs)
        nms = names(mod; all=true)
        modeval, modinclude = getfield(mod, :eval), getfield(mod, :include)
        failed = []
        n = 0
        for fsym in nms
            f = getfield(mod, fsym)
            isa(f, Base.Callable) || continue
            (f === modeval || f === modinclude) && continue
            for m in methods(f)
                # MyInt8 brings in lots of number & type machinery, which leads
                # to wandering through Base files. At this point we just want
                # to test whether we have the basics down, so for now avoid
                # looking in any file other than the script
                string(m.file) == scriptfile || continue
                isa(definition(m), Expr) || push!(failed, m.sig)
                n += 1
            end
        end
        @test isempty(failed)
        @test n > length(nms)/2

        # Method expressions with bad line number info
        ex = quote
            function nolineinfo(x)
                y = x^2 + 2x + 1
                @warn "oops"
                return y
            end
        end
        ex2 = ex.args[end].args[end]
        for (i, arg) in enumerate(ex2.args)
            if isa(arg, LineNumberNode)
                ex2.args[i] = LineNumberNode(0, :none)
            end
        end
        mexs = Revise.ModuleExprsInfos(ReviseTestPrivate)
        mexs[ReviseTestPrivate][Revise.RelocatableExpr(ex)] = nothing
        logs, _ = Test.collect_test_logs() do
            Revise.instantiate_sigs!(mexs; mode=:eval)
        end
        @test isempty(logs)
        @test isdefined(ReviseTestPrivate, :nolineinfo)
    end

    do_test("Comparison and line numbering") && @testset "Comparison and line numbering" begin
        # We'll also use these tests to try out the logging system
        rlogger = Revise.debug_logger()

        fl1 = joinpath(@__DIR__, "revisetest.jl")
        fl2 = joinpath(@__DIR__, "revisetest_revised.jl")
        fl3 = joinpath(@__DIR__, "revisetest_errors.jl")

        # Copy the files to a temporary file. This is to ensure that file name doesn't change
        # in docstring macros and backtraces.
        tmpfile = joinpath(tempdir(), randstring(10))*".jl"
        push!(to_remove, tmpfile)

        cp(fl1, tmpfile)
        include(tmpfile)  # So the modules are defined
        # test the "mistakes"
        @latestworld
        @test ReviseTest.cube(2) == 16
        @test ReviseTest.Internal.mult3(2) == 8
        @test ReviseTest.Internal.mult4(2) == -2
        # One method will be deleted, for log testing we need to grab it while we still have it
        delmeth = first(methods(ReviseTest.Internal.mult4))
        mmult3 = @which ReviseTest.Internal.mult3(2)

        mod_exs_infos_old = parse_source(tmpfile, Main)
        Revise.instantiate_sigs!(mod_exs_infos_old)
        mcube = @which ReviseTest.cube(2)

        cp(fl2, tmpfile; force=true)
        mod_exs_infos_new = parse_source(tmpfile, Main)
        mod_exs_infos_new = Revise.eval_revised(mod_exs_infos_new, mod_exs_infos_old)
        @latestworld
        @test ReviseTest.cube(2) == 8
        @test ReviseTest.Internal.mult3(2) == 6

        @test length(mod_exs_infos_new) == 3
        @test haskey(mod_exs_infos_new, ReviseTest) && haskey(mod_exs_infos_new, ReviseTest.Internal)

        dvs = collect(mod_exs_infos_new[ReviseTest])
        @test length(dvs) == 3
        (def, val) = dvs[1]
        @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(square(x) = x^2)))
        @test val == [Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.square),Any})]
        @test Revise.firstline(Revise.unwrap(def)).line == 5
        m = @which ReviseTest.square(1)
        @test m.line == 5
        @test whereis(m) == (tmpfile, 5)
        @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def)
        (def, val) = dvs[2]
        @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(cube(x) = x^3)))
        @test val == [Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.cube),Any})]
        m = @which ReviseTest.cube(1)
        @test m.line == 7
        @test whereis(m) == (tmpfile, 7)
        @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def)
        (def, val) = dvs[3]
        @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(fourth(x) = x^4)))
        @test val == [Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.fourth),Any})]
        m = @which ReviseTest.fourth(1)
        @test m.line == 9
        @test whereis(m) == (tmpfile, 9)
        @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def)

        dvs = collect(mod_exs_infos_new[ReviseTest.Internal])
        @test length(dvs) == 5
        (def, val) = dvs[1]
        @test isequal(Revise.unwrap(def),  Revise.RelocatableExpr(:(mult2(x) = 2*x)))
        @test val == [Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.Internal.mult2),Any})]
        @test Revise.firstline(Revise.unwrap(def)).line == 13
        m = @which ReviseTest.Internal.mult2(1)
        @test m.line == 11
        @test whereis(m) == (tmpfile, 13)
        @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def)
        (def, val) = dvs[2]
        @test isequal(Revise.unwrap(def), Revise.RelocatableExpr(:(mult3(x) = 3*x)))
        @test val == [Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.Internal.mult3),Any})]
        m = @which ReviseTest.Internal.mult3(1)
        @test m.line == 14
        @test whereis(m) == (tmpfile, 14)
        @test Revise.RelocatableExpr(definition(m)) == Revise.unwrap(def)

        @test_throws MethodError ReviseTest.Internal.mult4(2)

        function cmpdiff(record, msg; kwargs...)
            record.message == msg
            for (kw, val) in kwargs
                logval = record.kwargs[kw]
                for (v, lv) in zip(val, logval)
                    isa(v, Expr) && (v = Revise.RelocatableExpr(v))
                    isa(lv, Expr) && (lv = Revise.RelocatableExpr(Revise.unwrap(lv)))
                    @test lv == v
                end
            end
            return nothing
        end
        logs = filter(r->r.level==Debug && r.group=="Action", rlogger.logs)
        @test length(logs) == 9
        cmpdiff(logs[1], "DeleteMethod"; deltainfo=(Tuple{typeof(ReviseTest.cube),Any}, MethodSummary(mcube)))
        cmpdiff(logs[2], "DeleteMethod"; deltainfo=(Tuple{typeof(ReviseTest.Internal.mult3),Any}, MethodSummary(mmult3)))
        cmpdiff(logs[3], "DeleteMethod"; deltainfo=(Tuple{typeof(ReviseTest.Internal.mult4),Any}, MethodSummary(delmeth)))
        cmpdiff(logs[4], "Eval"; deltainfo=(ReviseTest, :(cube(x) = x^3)))
        cmpdiff(logs[5], "Eval"; deltainfo=(ReviseTest, :(fourth(x) = x^4)))
        stmpfile = Symbol(tmpfile)
        cmpdiff(logs[6], "LineOffset"; deltainfo=(Any[Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.Internal.mult2),Any})], LineNumberNode(11,stmpfile)=>LineNumberNode(13,stmpfile)))
        cmpdiff(logs[7], "Eval"; deltainfo=(ReviseTest.Internal, :(mult3(x) = 3*x)))
        cmpdiff(logs[8], "LineOffset"; deltainfo=(Any[Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.Internal.unchanged),Any})], LineNumberNode(18,stmpfile)=>LineNumberNode(19,stmpfile)))
        cmpdiff(logs[9], "LineOffset"; deltainfo=(Any[Revise.SigInfo(nothing, Tuple{typeof(ReviseTest.Internal.unchanged2),Any})], LineNumberNode(20,stmpfile)=>LineNumberNode(21,stmpfile)))
        @test length(Revise.actions(rlogger)) == 6  # by default LineOffset is skipped
        @test length(Revise.actions(rlogger; line=true)) == 9
        @test_broken length(Revise.diffs(rlogger)) == 2
        io = PipeBuffer()
        foreach(rec -> (show(io, rec); println(io)), rlogger.logs)
        str = String(take!(io))
        @test occursin("Revise DeleteMethod: mult4(::Any)", str)
        foreach(rec -> (show(IOContext(io, :verbose=>true), rec); println(io)), rlogger.logs)
        str = String(take!(io))
        @test count("Revise.LogRecord", str) > 8
        empty!(rlogger.logs)

        # Backtraces. Note this doesn't test the line-number correction
        # because both of these are revised definitions.
        cp(fl3, tmpfile; force=true)
        mod_exs_infos_old = mod_exs_infos_new
        mod_exs_infos_new = parse_source(tmpfile, Main)
        mod_exs_infos_new = Revise.eval_revised(mod_exs_infos_new, mod_exs_infos_old)
        @latestworld
        try
            ReviseTest.cube(2)
            @test false
        catch err
            @test isa(err, ErrorException) && err.msg == "cube"
            bt = throwing_function(stacktrace(catch_backtrace()))
            @test bt.func === :cube && bt.file == Symbol(tmpfile) && bt.line == 7
        end
        try
            ReviseTest.Internal.mult2(2)
            @test false
        catch err
            @test isa(err, ErrorException) && err.msg == "mult2"
            bt = throwing_function(stacktrace(catch_backtrace()))
            @test bt.func === :mult2 && bt.file == Symbol(tmpfile) && bt.line == 13
        end

        logs = filter(r->r.level==Debug && r.group=="Action", rlogger.logs)
        @test length(logs) == 4
        cmpdiff(logs[3], "Eval"; deltainfo=(ReviseTest, :(cube(x) = error("cube"))))
        cmpdiff(logs[4], "Eval"; deltainfo=(ReviseTest.Internal, :(mult2(x) = error("mult2"))))

        # Turn off future logging
        Revise.debug_logger(; min_level=Info)

        # Gensymmed symbols
        rex1 = Revise.RelocatableExpr(macroexpand(Main, :(t = @elapsed(foo(x)))))
        rex2 = Revise.RelocatableExpr(macroexpand(Main, :(t = @elapsed(foo(x)))))
        @test isequal(rex1, rex2)
        @test hash(rex1) == hash(rex2)
        rex3 = Revise.RelocatableExpr(macroexpand(Main, :(t = @elapsed(bar(x)))))
        @test !isequal(rex1, rex3)
        @test hash(rex1) != hash(rex3)
        sym1, sym2 = gensym(:hello), gensym(:hello)
        rex1 = Revise.RelocatableExpr(:(x = $sym1))
        rex2 = Revise.RelocatableExpr(:(x = $sym2))
        @test isequal(rex1, rex2)
        @test hash(rex1) == hash(rex2)
        # the gensym counter is ignored, but the base name is not
        sym3 = gensym(:world)
        rex3 = Revise.RelocatableExpr(:(x = $sym3))
        @test !isequal(rex1, rex3)
        @test hash(rex1) != hash(rex3)
        # the pairing between gensyms must be consistent across the expression
        @test isequal(Revise.RelocatableExpr(:($sym1 + f($sym1))),
                      Revise.RelocatableExpr(:($sym2 + f($sym2))))
        @test hash(Revise.RelocatableExpr(:($sym1 + f($sym1)))) ==
              hash(Revise.RelocatableExpr(:($sym2 + f($sym2))))
        @test !isequal(Revise.RelocatableExpr(:($sym1 + f($sym1))),
                       Revise.RelocatableExpr(:($sym1 + f($sym2))))
        @test hash(Revise.RelocatableExpr(:($sym1 + f($sym1)))) !=
              hash(Revise.RelocatableExpr(:($sym1 + f($sym2))))
        @test !isequal(Revise.RelocatableExpr(:(g($sym1, $sym2) = $sym1)),
                       Revise.RelocatableExpr(:(g($sym1, $sym2) = $sym2)))
        # counter-free `#` names denote reproducible bindings and compare exactly
        @test !isequal(Revise.RelocatableExpr(:(getval() = var"#x")),
                       Revise.RelocatableExpr(:(getval() = var"#y")))
        @test hash(Revise.RelocatableExpr(:(getval() = var"#x"))) !=
              hash(Revise.RelocatableExpr(:(getval() = var"#y")))
        @test isequal(Revise.RelocatableExpr(:(getval() = var"#x")),
                      Revise.RelocatableExpr(:(getval() = var"#x")))
        # expressions differing only in such names must not collide as
        # `ExprsInfos` keys (generated precompile files contain many
        # directives that differ only in the gensym base name)
        src = """
            module GensymKeyCollision
            precompile(Tuple{typeof(var"#f#1"), Int})
            precompile(Tuple{typeof(var"#g#2"), Int})
            end
            """
        mexs = parse_source!(Revise.ModuleExprsInfos(), src, "gensym_collision.jl", Main)
        @test length(mexs[getfield(Main, :GensymKeyCollision)]) == 2

        # coverage
        rex = convert(Revise.RelocatableExpr, :(a = 1))
        @test Revise.striplines!(rex) isa Revise.RelocatableExpr
        @test copy(rex) !== rex
    end

    do_test("Gensym names in source") && @testset "Gensym names in source" begin
        # An edit confined to var"#..." names must still trigger revision
        testdir = newtestdir()
        fn = joinpath(testdir, "gensymnames.jl")
        write(fn, """
            module GensymNames
            const var"#x" = 1
            const var"#y" = 2
            getval() = var"#x"
            end
            """)
        sleep(mtimedelay)
        includet(fn)
        @latestworld
        @test GensymNames.getval() == 1
        sleep(mtimedelay)
        write(fn, """
            module GensymNames
            const var"#x" = 1
            const var"#y" = 2
            getval() = var"#y"
            end
            """)
        @yry()
        @test GensymNames.getval() == 2

        pop!(LOAD_PATH)
    end

    do_test("Display") && @testset "Display" begin
        io = IOBuffer()
        show(io, Revise.RelocatableExpr(:(@inbounds x[2])))
        str = String(take!(io))
        @test str == ":(@inbounds x[2])"
        mod = private_module()
        file = joinpath(@__DIR__, "revisetest.jl")
        Base.include(mod, file)
        mexs = parse_source(file, mod)
        Revise.instantiate_sigs!(mexs)
        print(IOContext(io, :compact=>true), mexs)
        str = String(take!(io))
        # @test str == "OrderedCollections.OrderedDict($mod$(pair_op_compact)ExprsInfos(<1 expressions>, <0 signatures>), $mod.ReviseTest$(pair_op_compact)ExprsInfos(<2 expressions>, <2 signatures>), $mod.ReviseTest.Internal$(pair_op_compact)ExprsInfos(<6 expressions>, <5 signatures>))"
        exs = mexs[getfield(mod, :ReviseTest)]
        print(IOContext(io, :compact=>true), exs)
        # @test String(take!(io)) == "ExprsInfos(<2 expressions>, <2 signatures>)"
        print(IOContext(io, :compact=>false), exs)
        str = String(take!(io))
        # @test str == "ExprsInfos with the following expressions: \n  :(square(x) = begin\n          x ^ 2\n      end)\n  :(cube(x) = begin\n          x ^ 4\n      end)"

        sleep(0.1)  # wait for EponymTuples to hit the cache
        pkgdata = Revise.pkgdatas[Base.PkgId(EponymTuples)]
        file = first(Revise.srcfiles(pkgdata))
        Revise.maybe_parse_from_cache!(pkgdata, file)
        print(io, pkgdata)
        str = String(take!(io))
        @test occursin("EponymTuples.jl\": FileInfo", str)
        @test occursin(r"with cachefile.*EponymTuples.*ji", str)
        print(IOContext(io, :compact=>true), pkgdata)
        str = String(take!(io))
        @test occursin("1/1 parsed files", str)
    end

    do_test("Non-notifying filesystems (issue #514)") && @testset "Non-notifying filesystems (issue #514)" begin
        # Path-prefix matching must respect path components.
        @test Revise.is_path_prefix("/mnt", "/mnt/c")
        @test Revise.is_path_prefix("/mnt/c", "/mnt/c")
        @test Revise.is_path_prefix("/", "/anything")
        @test !Revise.is_path_prefix("/mnt", "/mnts")
        @test !Revise.is_path_prefix("/mnt/c", "/mnt")

        # `/proc/mounts` escapes spaces, backslashes, etc. as octal.
        @test Revise.unescape_mount("C:\\134") == "C:\\"
        @test Revise.unescape_mount("a\\040b") == "a b"
        @test Revise.unescape_mount("/plain/path") == "/plain/path"

        # The fstype is taken from the most specific (longest) matching mount point.
        # These use Unix-style absolute paths (the only ones `/proc/mounts` describes),
        # so restrict to Unix where `abspath` leaves them unchanged.
        if Sys.isunix()
            mounts = [
                "drivers /usr/lib/wsl/drivers 9p ro 0 0",
                "C:\\134 /mnt/c 9p rw,aname=drvfs 0 0",
                "/dev/sdc / ext4 rw 0 0",
            ]
            @test Revise.fstype_for_path("/mnt/c/Users/foo", mounts) == "9p"
            @test Revise.fstype_for_path("/home/tim/x", mounts) == "ext4"
            @test Revise.fstype_for_path("/mnt/cdrom/x", mounts) == "ext4"  # not under /mnt/c
        end

        # Off WSL, no path is ever treated as non-notifying.
        if !Revise.is_wsl()
            @test !Revise.nonnotifying_path("/mnt/c/whatever")
        end
    end

    do_test("File paths") && @testset "File paths" begin
        testdir = newtestdir()
        for wf in (Revise.watching_files[] ? (true,) : (true, false))
            for (pcflag, fbase) in ((true, "pc"), (false, "npc"),)  # precompiled & not
                modname = uppercase(fbase) * (wf ? "WF" : "WD")
                fbase = fbase * (wf ? "wf" : "wd")
                pcexpr = pcflag ? "" : :(__precompile__(false))
                # Create a package with the following structure:
                #   src/PkgName.jl   # PC.jl = precompiled, NPC.jl = nonprecompiled
                #   src/file2.jl
                #   src/subdir/file3.jl
                #   src/subdir/file4.jl
                # exploring different ways of expressing the `include` statement
                dn = joinpath(testdir, modname, "src")
                mkpath(dn)
                write(joinpath(dn, modname*".jl"), """
                    $pcexpr
                    module $modname

                    export $(fbase)1, $(fbase)2, $(fbase)3, $(fbase)4, $(fbase)5, using_macro_$(fbase)

                    $(fbase)1() = 1

                    include("file2.jl")
                    include("subdir/file3.jl")
                    include(joinpath(@__DIR__, "subdir", "file4.jl"))
                    otherfile = "file5.jl"
                    include(otherfile)

                    # Update order check: modifying `some_macro_` to return -6 doesn't change the
                    # return value of `using_macro_` (issue #20) unless `using_macro_` is also updated,
                    # *in this order*:
                    #   1. update the `@some_macro_` definition
                    #   2. update the `using_macro_` definition
                    macro some_macro_$(fbase)()
                        return 6
                    end
                    using_macro_$(fbase)() = @some_macro_$(fbase)()

                    end
                    """)
                write(joinpath(dn, "file2.jl"), "$(fbase)2() = 2")
                mkdir(joinpath(dn, "subdir"))
                write(joinpath(dn, "subdir", "file3.jl"), "$(fbase)3() = 3")
                write(joinpath(dn, "subdir", "file4.jl"), "$(fbase)4() = 4")
                write(joinpath(dn, "file5.jl"), "$(fbase)5() = 5")

                sleep(mtimedelay)
                @eval using $(Symbol(modname))
                sleep(mtimedelay)
                fn1, fn2 = Symbol("$(fbase)1"), Symbol("$(fbase)2")
                fn3, fn4 = Symbol("$(fbase)3"), Symbol("$(fbase)4")
                fn5 = Symbol("$(fbase)5")
                fn6 = Symbol("using_macro_$(fbase)")
                @eval @test $(fn1)() == 1
                @eval @test $(fn2)() == 2
                @eval @test $(fn3)() == 3
                @eval @test $(fn4)() == 4
                @eval @test $(fn5)() == 5
                @eval @test $(fn6)() == 6
                m = @eval first(methods($fn1))
                rex = Revise.RelocatableExpr(definition(m))
                @test rex == Revise.RelocatableExpr(:( $fn1() = 1 ))
                # Check that definition returns copies
                rex2 = deepcopy(rex)
                rex.ex.args[end].args[end] = 2
                @test Revise.RelocatableExpr(definition(m)) == rex2
                @test Revise.RelocatableExpr(definition(m)) != rex
                # CodeTracking methods
                m3 = first(methods(eval(fn3)))
                m3file = joinpath(dn, "subdir", "file3.jl")
                w = whereis(m3)
                @test samefile(w[1], m3file)
                @test w[2] == 1
                @test signatures_at(m3file, 1) == [Revise.SigInfo(nothing, m3.sig)]
                @test signatures_at(eval(Symbol(modname)), joinpath("src", "subdir", "file3.jl"), 1) == [Revise.SigInfo(nothing, m3.sig)]

                id = Base.PkgId(eval(Symbol(modname)))   # for testing #596
                pkgdata = Revise.pkgdatas[id]

                # Change the definition of function 1 (easiest to just rewrite the whole file)
                write(joinpath(dn, modname*".jl"), """
                    $pcexpr
                    module $modname
                    export $(fbase)1, $(fbase)2, $(fbase)3, $(fbase)4, $(fbase)5, using_macro_$(fbase)
                    $(fbase)1() = -1
                    include("file2.jl")
                    include("subdir/file3.jl")
                    include(joinpath(@__DIR__, "subdir", "file4.jl"))
                    otherfile = "file5.jl"
                    include(otherfile)

                    macro some_macro_$(fbase)()
                        return -6
                    end
                    using_macro_$(fbase)() = @some_macro_$(fbase)()

                    end
                    """)  # just for fun we skipped the whitespace
                Revise.active[] = false
                @yry()
                @eval @test $(fn1)() == 1
                Revise.active[] = true
                @yry()
                fi = pkgdata.fileinfos[1]
                @test fi.extracted[]          # issue 596
                @eval @test $(fn1)() == -1
                @eval @test $(fn2)() == 2
                @eval @test $(fn3)() == 3
                @eval @test $(fn4)() == 4
                @eval @test $(fn5)() == 5
                @eval @test $(fn6)() == 6      # because it hasn't been re-macroexpanded
                @test revise(eval(Symbol(modname)))
                @eval @test $(fn6)() == -6
                # Redefine function 2
                write(joinpath(dn, "file2.jl"), "$(fbase)2() = -2")
                @yry()
                @eval @test $(fn1)() == -1
                @eval @test $(fn2)() == -2
                @eval @test $(fn3)() == 3
                @eval @test $(fn4)() == 4
                @eval @test $(fn5)() == 5
                @eval @test $(fn6)() == -6
                write(joinpath(dn, "subdir", "file3.jl"), "$(fbase)3() = -3")
                @yry()
                @eval @test $(fn1)() == -1
                @eval @test $(fn2)() == -2
                @eval @test $(fn3)() == -3
                @eval @test $(fn4)() == 4
                @eval @test $(fn5)() == 5
                @eval @test $(fn6)() == -6
                write(joinpath(dn, "subdir", "file4.jl"), "$(fbase)4() = -4")
                @yry()
                @eval @test $(fn1)() == -1
                @eval @test $(fn2)() == -2
                @eval @test $(fn3)() == -3
                @eval @test $(fn4)() == -4
                @eval @test $(fn5)() == 5
                @eval @test $(fn6)() == -6
                write(joinpath(dn, "file5.jl"), "$(fbase)5() = -5")
                @yry()
                @eval @test $(fn1)() == -1
                @eval @test $(fn2)() == -2
                @eval @test $(fn3)() == -3
                @eval @test $(fn4)() == -4
                @eval @test $(fn5)() == -5
                @eval @test $(fn6)() == -6
                # Check that the list of files is complete
                pkgdata = Revise.pkgdatas[Base.PkgId(modname)]
                for file = [joinpath("src", modname*".jl"), joinpath("src", "file2.jl"),
                            joinpath("src", "subdir", "file3.jl"),
                            joinpath("src", "subdir", "file4.jl"),
                            joinpath("src", "file5.jl")]
                    @test Revise.hasfile(pkgdata, file)
                end
            end
        end
        # Remove the precompiled file(s)
        rm_precompile("PCWF")
        Revise.watching_files[] || rm_precompile("PCWD")

        # Submodules (issue #142)
        srcdir = joinpath(testdir, "Mysupermodule", "src")
        subdir = joinpath(srcdir, "Mymodule")
        mkpath(subdir)
        write(joinpath(srcdir, "Mysupermodule.jl"), """
            module Mysupermodule
            include("Mymodule/Mymodule.jl")
            end
            """)
        write(joinpath(subdir, "Mymodule.jl"), """
            module Mymodule
            include("filesub.jl")
            end
            """)
        write(joinpath(subdir, "filesub.jl"), "func() = 1")
        sleep(mtimedelay)
        @eval using Mysupermodule
        sleep(mtimedelay)
        @test Mysupermodule.Mymodule.func() == 1
        write(joinpath(subdir, "filesub.jl"), "func() = 2")
        @yry()
        @test Mysupermodule.Mymodule.func() == 2
        rm_precompile("Mymodule")
        rm_precompile("Mysupermodule")

        # Test files paths that can't be statically parsed
        dn = joinpath(testdir, "LoopInclude", "src")
        mkpath(dn)
        write(joinpath(dn, "LoopInclude.jl"), """
            module LoopInclude

            export li_f, li_g

            for fn in ("file1.jl", "file2.jl")
                include(fn)
            end

            end
            """)
        write(joinpath(dn, "file1.jl"), "li_f() = 1")
        write(joinpath(dn, "file2.jl"), "li_g() = 2")
        sleep(mtimedelay)
        @eval using LoopInclude
        sleep(mtimedelay)
        @test li_f() == 1
        @test li_g() == 2
        write(joinpath(dn, "file1.jl"), "li_f() = -1")
        @yry()
        @test li_f() == -1
        rm_precompile("LoopInclude")

        # Multiple packages in the same directory (issue #228)
        write(joinpath(testdir, "A228.jl"), """
            module A228
            using B228
            export f228
            f228(x) = 3 * g228(x)
            end
            """)
        write(joinpath(testdir, "B228.jl"), """
            module B228
            export g228
            g228(x) = 4x + 2
            end
            """)
        sleep(mtimedelay)
        using A228
        sleep(mtimedelay)
        @test f228(3) == 42
        write(joinpath(testdir, "B228.jl"), """
            module B228
            export g228
            g228(x) = 4x + 1
            end
            """)
        @yry()
        @test f228(3) == 39
        rm_precompile("A228")
        rm_precompile("B228")

        # Retracting an export (issue #633): dropping a name from `export`
        # un-exports it, so `using`-ing modules stop resolving it. Requires the
        # `Base.set_binding_visibility!` accessor added in Julia 1.14.
        @static if isdefined(Base, :set_binding_visibility!)
            write(joinpath(testdir, "Export633.jl"), """
                module Export633
                export hello633, g633
                hello633() = 1
                g633() = 2
                end
                """)
            sleep(mtimedelay)
            using Export633
            sleep(mtimedelay)
            @test Base.isexported(Export633, :g633)
            @test @eval(g633()) == 2
            write(joinpath(testdir, "Export633.jl"), """
                module Export633
                export hello633
                hello633() = 1
                g633() = 2
                end
                """)
            @yry()
            @test !Base.isexported(Export633, :g633)
            @test Export633.g633() == 2          # only the export is retracted; the method remains
            @test_throws UndefVarError @eval g633()
            rm_precompile("Export633")
        end

        # uncoupled packages in the same directory (issue #339)
        write(joinpath(testdir, "A339.jl"), """
            module A339
            f() = 1
            end
            """)
        write(joinpath(testdir, "B339.jl"), """
            module B339
            f() = 1
            end
            """)
        sleep(mtimedelay)
        using A339, B339
        sleep(mtimedelay)
        @test A339.f() == 1
        @test B339.f() == 1
        sleep(mtimedelay)
        write(joinpath(testdir, "A339.jl"), """
                        module A339
                        f() = 2
                        end
                        """)
        @yry()
        @test A339.f() == 2
        @test B339.f() == 1
        sleep(mtimedelay)
        write(joinpath(testdir, "B339.jl"), """
                        module B339
                        f() = 2
                        end
                        """)
        @yry()
        @test A339.f() == 2
        @test B339.f() == 2
        rm_precompile("A339")
        rm_precompile("B339")

        # Combining `include` with empty functions (issue #758)
        write(joinpath(testdir, "Issue758.jl"), """
            module Issue758
            global gvar = true
            function f end
            include("Issue758helperfile.jl")
            end
            """)
        write(joinpath(testdir, "Issue758helperfile.jl"), "")
        sleep(mtimedelay)
        using Issue758
        sleep(mtimedelay)
        @test_throws MethodError Issue758.f()
        sleep(mtimedelay)
        write(joinpath(testdir, "Issue758.jl"), """
            module Issue758
            global gvar = true
            function f end
            f() = 1
            include("Issue758helperfile.jl")
            end
            """)
        @yry()
        @test Issue758.f() == 1
        rm_precompile("Issue758")

        pop!(LOAD_PATH)
    end

    # https://github.com/timholy/Revise.jl/issues/1033
    do_test("Source from cache") && @testset "Source from cache" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "CacheLookup1033", "src")
        mkpath(dn)
        write(joinpath(dn, "CacheLookup1033.jl"), """
            module CacheLookup1033
            include("sub.jl")
            end
            """)
        write(joinpath(dn, "sub.jl"), "f1033() = 1")
        sleep(mtimedelay)
        @eval using CacheLookup1033
        sleep(mtimedelay)
        pkgdata = Revise.pkgdatas[Base.PkgId(CacheLookup1033)]
        # Baseline: the source of a precompiled package is read back from its `.ji` cache.
        for file in Revise.srcfiles(pkgdata)
            @test occursin("1033", Revise.read_from_cache(pkgdata, file))
        end
        # The cache is indexed by the path recorded at precompile time. Reading it must
        # not depend on reconstructing that path from `basedir`, which can take a
        # different form than the cache's (e.g. across symlinks). Simulate the
        # divergence by pointing `basedir` elsewhere; the source should still be found.
        saved = pkgdata.info.basedir
        pkgdata.info.basedir = joinpath(saved, "does", "not", "match")
        try
            for file in Revise.srcfiles(pkgdata)
                @test occursin("1033", Revise.read_from_cache(pkgdata, file))
            end
        finally
            pkgdata.info.basedir = saved
        end
        rm_precompile("CacheLookup1033")
    end

    # issue #131
    do_test("Base & stdlib file paths") && @testset "Base & stdlib file paths" begin
        @test isfile(Revise.basesrccache)
        targetfn = Base.Filesystem.path_separator * joinpath("good", "path", "mydir", "myfile.jl")
        @test Revise.fixpath("/some/bad/path/mydir/myfile.jl"; badpath="/some/bad/path", goodpath="/good/path") == targetfn
        @test Revise.fixpath("/some/bad/path/mydir/myfile.jl"; badpath="/some/bad/path/", goodpath="/good/path") == targetfn
        @test isfile(Revise.fixpath(Base.find_source_file("array.jl")))
        failedfiles = Tuple{String,String}[]
        for (_,file) = Base._included_files
            fixedfile = Revise.fixpath(file)
            if !isfile(fixedfile)
                push!(failedfiles, (file, fixedfile))
            end
        end
        if !isempty(failedfiles)
            display(failedfiles)
        end
        @test isempty(failedfiles)
    end

    do_test("Namespace") && @testset "Namespace" begin
        # Issues #579, #239, and #627
        testdir = newtestdir()
        dn = joinpath(testdir, "Namespace", "src")
        mkpath(dn)
        write(joinpath(dn, "Namespace.jl"), """
            module Namespace
            struct X end
            cos(::X) = 20
            end
            """)
        sleep(mtimedelay)
        @eval using Namespace
        @test Namespace.cos(Namespace.X()) == 20
        @test_throws MethodError Base.cos(Namespace.X())
        sleep(mtimedelay)
        write(joinpath(dn, "Namespace.jl"), """
            module Namespace
            struct X end
            sin(::Int) = 10
            Base.cos(::X) = 20
            # From #627
            module Foos
                struct Foo end
            end
            using .Foos: Foo
            end
            """)
        @yry()
        @test Namespace.sin(0) == 10
        @test Base.sin(0) == 0
        @test Base.cos(Namespace.X()) == 20
        if Base.VERSION >= v"1.12.0-DEV.2047"
            # The orphaned local `cos` is re-imported from `Base` (issue #239), matching what
            # a fresh load of the revised source would give (`Namespace.cos === Base.cos`).
            @test Namespace.cos(Namespace.X()) == 20
        else
            @test_throws MethodError Namespace.cos(Namespace.X())
        end

        rm_precompile("Namespace")
        pop!(LOAD_PATH)
    end

    do_test("Multiple definitions") && @testset "Multiple definitions" begin
        # This simulates a copy/paste/save "error" from one file to another
        # ref https://github.com/timholy/CodeTracking.jl/issues/55
        testdir = newtestdir()
        dn = joinpath(testdir, "Multidef", "src")
        mkpath(dn)
        write(joinpath(dn, "Multidef.jl"), """
            module Multidef
            include("utils.jl")
            end
            """)
        write(joinpath(dn, "utils.jl"), "repeated(x) = x+1")
        sleep(mtimedelay)
        @eval using Multidef
        @test Multidef.repeated(3) == 4
        sleep(mtimedelay)
        write(joinpath(dn, "Multidef.jl"), """
            module Multidef
            include("utils.jl")
            repeated(x) = x+1
            end
            """)
        @yry()
        @test Multidef.repeated(3) == 4
        sleep(mtimedelay)
        write(joinpath(dn, "utils.jl"), "\n")
        @yry()
        @test Multidef.repeated(3) == 4

        rm_precompile("Multidef")
        pop!(LOAD_PATH)
    end

    do_test("Multimodule include") && @testset "Multimodule include" begin
        # A single file `include`d into more than one module has one `FileInfo`
        # per inclusion; a revision must update all of them, not just the first
        # (issue #730).
        testdir = newtestdir()
        dn = joinpath(testdir, "Multimod730", "src")
        mkpath(dn)
        write(joinpath(dn, "Multimod730.jl"), """
            module Multimod730
            module A
            include("multimod730_inc.jl")
            end
            module B
            include("multimod730_inc.jl")
            end
            end
            """)
        write(joinpath(dn, "multimod730_inc.jl"), "f730(x) = 1")
        sleep(mtimedelay)
        @eval using Multimod730
        @test Multimod730.A.f730(0) == 1
        @test Multimod730.B.f730(0) == 1
        # An edit must propagate to both modules
        sleep(mtimedelay)
        write(joinpath(dn, "multimod730_inc.jl"), "f730(x) = 2")
        @yry()
        @test Multimod730.A.f730(0) == 2
        @test Multimod730.B.f730(0) == 2
        # Method deletion must cascade to both modules
        sleep(mtimedelay)
        write(joinpath(dn, "multimod730_inc.jl"), "g730(x) = 3")
        @yry()
        @test_throws MethodError Multimod730.A.f730(0)
        @test_throws MethodError Multimod730.B.f730(0)
        @test Multimod730.A.g730(0) == 3
        @test Multimod730.B.g730(0) == 3

        rm_precompile("Multimod730")
        pop!(LOAD_PATH)
    end

    do_test("include with mapexpr") && @testset "include with mapexpr" begin
        # The transform passed to `include(mapexpr, file)`/`includet(mapexpr, file)` must be
        # re-applied whenever the file is revised (issues #634, #820). The test transform
        # bumps the integer literal body of a short-form function definition by a fixed
        # offset, so `f() = 2` behaves as `f() = 42`: return values reveal whether the
        # transform was applied, and which one.
        function mkbump(n)
            return function (ex)
                if Meta.isexpr(ex, :(=), 2)
                    rhs = ex.args[2]
                    if rhs isa Expr && rhs.head === :block && rhs.args[end] isa Int
                        rhs.args[end] += n
                    end
                end
                return ex
            end
        end
        bump40 = mkbump(40)
        testdir = newtestdir()

        # includet(mapexpr, file)
        script = joinpath(testdir, "mapexpr_script.jl")
        write(script, "fmap634() = 2\n")
        sleep(mtimedelay)
        includet(bump40, script)
        @latestworld
        @test fmap634() == 42
        sleep(mtimedelay)
        write(script, "fmap634() = 3\n")
        @yry()
        @test fmap634() == 43
        @test occursin("with mapexpr", sprint(show, Revise.FileInfo(Main; mapexpr=bump40)))

        # An error thrown by the transform is reported against the statement it was applied to
        badscript = joinpath(testdir, "mapexpr_bad.jl")
        write(badscript, "# a comment\nfmap634bad() = 2\n")
        sleep(mtimedelay)
        badmap(_) = error("bad transform")
        err = try
            Revise.track(badmap, badscript)
        catch err
            err
        end
        @test err isa Revise.ReviseEvalException
        @test occursin("mapexpr_bad.jl:2", err.loc)
        @test occursin("bad transform", sprint(showerror, err))

        # track(mapexpr, file) on an already-loaded script
        script2 = joinpath(testdir, "mapexpr_script2.jl")
        write(script2, "fmap634b() = 2\n")
        include(bump40, script2)
        @latestworld
        @test fmap634b() == 42
        sleep(mtimedelay)
        Revise.track(bump40, script2)
        sleep(mtimedelay)
        write(script2, "fmap634b() = 3\n")
        @yry()
        @test fmap634b() == 43

        # A revision that introduces `include(mapexpr, file)` into a package starts
        # tracking the file with its transform. This path discovers the transform from
        # the revised code itself, so it works on all supported Julia versions.
        dn = joinpath(testdir, "MapExprRT", "src")
        mkpath(dn)
        top_rt(inc) = """
            module MapExprRT
            function bump(ex)
                if Meta.isexpr(ex, :(=), 2)
                    rhs = ex.args[2]
                    rhs isa Expr && rhs.head === :block && rhs.args[end] isa Int && (rhs.args[end] += 40)
                end
                return ex
            end
            f() = 1
            $inc
            end
            """
        write(joinpath(dn, "MapExprRT.jl"), top_rt(""))
        sleep(mtimedelay)
        @eval using MapExprRT
        @test MapExprRT.f() == 1
        sleep(mtimedelay)
        write(joinpath(dn, "b_rt.jl"), "g() = 2\n")
        write(joinpath(dn, "MapExprRT.jl"), top_rt("include(bump, \"b_rt.jl\")"))
        @yry()
        @test MapExprRT.g() == 42
        # Subsequent revisions of the included file re-apply the transform
        sleep(mtimedelay)
        write(joinpath(dn, "b_rt.jl"), "g() = 3\n")
        @yry()
        @test MapExprRT.g() == 43
        # Method deletion works through the transform
        sleep(mtimedelay)
        write(joinpath(dn, "b_rt.jl"), "h() = 4\n")
        @yry()
        @test MapExprRT.h() == 44
        @test_throws MethodError MapExprRT.g()
        # The three-argument `include(mapexpr, mod, path)` targets another module
        sleep(mtimedelay)
        write(joinpath(dn, "c_rt.jl"), "k() = 5\n")
        write(joinpath(dn, "MapExprRT.jl"),
              top_rt("include(bump, \"b_rt.jl\")\nmodule SubRT end\nBase.include(bump, SubRT, \"c_rt.jl\")"))
        @yry()
        @test MapExprRT.SubRT.k() == 45
        sleep(mtimedelay)
        write(joinpath(dn, "c_rt.jl"), "k() = 6\n")
        @yry()
        @test MapExprRT.SubRT.k() == 46
        rm_precompile("MapExprRT")

        # For `include(mapexpr, file)` executed while a package loads (including from its
        # precompile cache), the transform is discovered from the record kept by
        # `Base.include_mapexprs` (Julia ≥ 1.14).
        if isdefined(Base, :include_mapexprs)
            dn = joinpath(testdir, "MapExprPC", "src")
            mkpath(dn)
            top_pc(f) = """
                module MapExprPC
                function mkbump(n)
                    return function (ex)
                        if Meta.isexpr(ex, :(=), 2)
                            rhs = ex.args[2]
                            if rhs isa Expr && rhs.head === :block && rhs.args[end] isa Int
                                rhs.args[end] += n
                            end
                        end
                        return ex
                    end
                end
                const pcbump40 = mkbump(40)   # closures capturing load-time state
                const pcbump50 = mkbump(50)
                include($f, "b_pc.jl")
                module Sub end
                Base.include(pcbump50, Sub, "c_pc.jl")
                include(pcbump40, "c_pc.jl")
                end
                """
            write(joinpath(dn, "MapExprPC.jl"), top_pc("pcbump40"))
            write(joinpath(dn, "b_pc.jl"), "g() = 2\n")
            write(joinpath(dn, "c_pc.jl"), "h() = 2\n")
            sleep(mtimedelay)
            @eval using MapExprPC
            @test MapExprPC.g() == 42        # pcbump40 applied at load (Base behavior)
            @test MapExprPC.Sub.h() == 52    # include(mapexpr, mod, path) into Sub
            @test MapExprPC.h() == 42        # same file, different transform, into the root
            # Revise recorded each inclusion's transform
            pkgdata = Revise.pkgdatas[Base.PkgId(MapExprPC)]
            @test Revise.fileinfo(pkgdata, "src/b_pc.jl").mapexpr === MapExprPC.pcbump40
            @test Revise.fileinfo(pkgdata, "src/MapExprPC.jl").mapexpr === identity
            # A file included twice with different transforms revises under each
            sleep(mtimedelay)
            write(joinpath(dn, "c_pc.jl"), "h() = 3\n")
            @yry()
            @test MapExprPC.Sub.h() == 53
            @test MapExprPC.h() == 43
            # Editing the `include(mapexpr, ...)` statement itself re-diffs the file under
            # the new transform, replacing (not duplicating) the tracked entry
            sleep(mtimedelay)
            write(joinpath(dn, "MapExprPC.jl"), top_pc("pcbump50"))
            @yry()
            @test MapExprPC.g() == 52
            idxs = Revise.fileindices(pkgdata, "src/b_pc.jl")
            @test length(idxs) == 1
            @test pkgdata.fileinfos[only(idxs)].mapexpr === MapExprPC.pcbump50
            # ...and later revisions of the file use the new transform
            sleep(mtimedelay)
            write(joinpath(dn, "b_pc.jl"), "g() = 4\n")
            @yry()
            @test MapExprPC.g() == 54
            rm_precompile("MapExprPC")
        end

        pop!(LOAD_PATH)
    end

    do_test("IJulia preexecute hook") && @testset "IJulia preexecute hook" begin
        # IJulia reloads code by registering `Revise.revise` as a preexecute
        # hook (IJulia.push_preexecute_hook) and invoking every hook before each
        # cell runs. This reproduces that mechanism in-process (no Jupyter
        # server) so CI guards the Revise side of the path exercised in issue
        # #325: an edit to a tracked file between cells must take effect on the
        # next cell, triggered solely by the hook and without an explicit
        # `revise()` in the cell body.
        testdir = newtestdir()
        dn = joinpath(testdir, "JupyterRevise", "src")
        mkpath(dn)
        write(joinpath(dn, "JupyterRevise.jl"), """
            module JupyterRevise
            greet() = "hello"
            end
            """)
        sleep(mtimedelay)

        # The kernel's preexecute hooks, populated exactly as IJulia does.
        preexecute_hooks = Function[]
        push_preexecute_hook(f) = push!(preexecute_hooks, f)
        # Run a "cell": fire every preexecute hook, then evaluate the body.
        # Time passes between cells in a real notebook, so by the time the user
        # runs the next cell the file-watcher task has already pushed the change
        # onto `revision_queue`; wait for that here so it is the hook (not the
        # test) that applies the revision.
        function run_cell(thunk)
            timedwait(() -> !isempty(Revise.revision_queue), event_timeout; pollint=0.02)
            sleep(0.02)
            for hook in preexecute_hooks
                Base.invokelatest(hook)
            end
            return Base.invokelatest(thunk)
        end

        # Load Revise's IJulia integration the way IJulia does, then `using`.
        push_preexecute_hook(Revise.revise)
        @eval using JupyterRevise
        @test JupyterRevise.greet() == "hello"

        sleep(mtimedelay)
        write(joinpath(dn, "JupyterRevise.jl"), """
            module JupyterRevise
            greet() = "world"
            end
            """)
        # The preexecute hook fires `revise()` automatically before the cell.
        @test run_cell(() -> JupyterRevise.greet()) == "world"

        sleep(mtimedelay)
        write(joinpath(dn, "JupyterRevise.jl"), """
            module JupyterRevise
            greet() = "again"
            end
            """)
        # Without firing the hook the cell sees stale code; firing it reloads.
        # This confirms the preexecute hook is what drives the reload.
        timedwait(() -> !isempty(Revise.revision_queue), event_timeout; pollint=0.02)
        sleep(0.02)
        @test Base.invokelatest(() -> JupyterRevise.greet()) == "world"
        @test run_cell(() -> JupyterRevise.greet()) == "again"

        rm_precompile("JupyterRevise")
        pop!(LOAD_PATH)
    end

    do_test("Recursive types (issues #417 and #883)") && @testset "Recursive types (issues #417 and #883)" begin
        testdir = newtestdir()
        fn = joinpath(testdir, "recursive.jl")
        write(fn, """
            module RecursiveTypes
            # issue #417
            struct Foo
                x::Vector{Foo}

                Foo() = new(Foo[])
            end

            # issue #883
            @static if Base.VERSION >= v"1.12-"
                struct NestedDict{K, V} <: AbstractDict{K, Union{V, NestedDict}} end
            end

            end
            """)
        sleep(mtimedelay)
        includet(fn)
        @latestworld
        @test isa(RecursiveTypes.Foo().x, Vector{RecursiveTypes.Foo})
        if Base.VERSION >= v"1.12-"
            @test isa(RecursiveTypes.NestedDict{Int, String}(), AbstractDict{Int, Union{String, RecursiveTypes.NestedDict}})
        end

        pop!(LOAD_PATH)
    end

    # issue #318
    do_test("Cross-module extension") && @testset "Cross-module extension" begin
        testdir = newtestdir()
        dnA = joinpath(testdir, "CrossModA", "src")
        mkpath(dnA)
        write(joinpath(dnA, "CrossModA.jl"), """
            module CrossModA
            foo(x) = "default"
            end
            """)
        dnB = joinpath(testdir, "CrossModB", "src")
        mkpath(dnB)
        write(joinpath(dnB, "CrossModB.jl"), """
            module CrossModB
            import CrossModA
            CrossModA.foo(x::Int) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using CrossModA, CrossModB
        @test CrossModA.foo("") == "default"
        @test CrossModA.foo(0) == 1
        sleep(mtimedelay)
        write(joinpath(dnB, "CrossModB.jl"), """
            module CrossModB
            import CrossModA
            CrossModA.foo(x::Int) = 2
            end
            """)
        @yry()
        @test CrossModA.foo("") == "default"
        @test CrossModA.foo(0) == 2
        write(joinpath(dnB, "CrossModB.jl"), """
            module CrossModB
            import CrossModA
            CrossModA.foo(x::Int) = 3
            end
            """)
        @yry()
        @test CrossModA.foo("") == "default"
        @test CrossModA.foo(0) == 3

        rm_precompile("CrossModA")
        rm_precompile("CrossModB")
        pop!(LOAD_PATH)
    end

    # issue #36
    do_test("@__FILE__") && @testset "@__FILE__" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "ModFILE", "src")
        mkpath(dn)
        write(joinpath(dn, "ModFILE.jl"), """
            module ModFILE

            mf() = @__FILE__, 1

            end
            """)
        sleep(mtimedelay)
        @eval using ModFILE
        sleep(mtimedelay)
        @test ModFILE.mf() == (joinpath(dn, "ModFILE.jl"), 1)
        write(joinpath(dn, "ModFILE.jl"), """
            module ModFILE

            mf() = @__FILE__, 2

            end
            """)
        @yry()
        @test ModFILE.mf() == (joinpath(dn, "ModFILE.jl"), 2)
        rm_precompile("ModFILE")
        pop!(LOAD_PATH)
    end

    do_test("Revision order") && @testset "Revision order" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "Order1", "src")
        mkpath(dn)
        write(joinpath(dn, "Order1.jl"), """
            module Order1
            include("file1.jl")
            include("file2.jl")
            end
            """)
        write(joinpath(dn, "file1.jl"), "# a comment")
        write(joinpath(dn, "file2.jl"), "# a comment")
        sleep(mtimedelay)
        @eval using Order1
        sleep(mtimedelay)
        # we want Revise to process files the order file1.jl, file2.jl, but let's save them in the opposite order
        write(joinpath(dn, "file2.jl"), "f(::Ord1) = 1")
        sleep(mtimedelay)
        write(joinpath(dn, "file1.jl"), "struct Ord1 end")
        @yry()
        @test Order1.f(Order1.Ord1()) == 1

        # A case in which order cannot be determined solely from file order
        dn = joinpath(testdir, "Order2", "src")
        mkpath(dn)
        write(joinpath(dn, "Order2.jl"), """
            module Order2
            include("file.jl")
            end
            """)
        write(joinpath(dn, "file.jl"), "# a comment")
        sleep(mtimedelay)
        @eval using Order2
        sleep(mtimedelay)
        write(joinpath(dn, "Order2.jl"), """
            module Order2
            include("file.jl")
            f(::Ord2) = 1
            end
            """)
        sleep(mtimedelay)
        write(joinpath(dn, "file.jl"), "struct Ord2 end")
        # TODO: remove also the log messages check when this test is fixed
        @test_logs (:error, r"Failed to revise") (:warn, r"The running code does not match the saved version") yry()
        @latestworld
        @test_broken Order2.f(Order2.Ord2()) == 1
        # Resolve it with retry
        Revise.retry()
        @latestworld
        @test Order2.f(Order2.Ord2()) == 1

        # Cross-module dependencies
        dn3 = joinpath(testdir, "Order3", "src")
        mkpath(dn3)
        write(joinpath(dn3, "Order3.jl"), """
            module Order3
            using Order2
            end
            """)
        sleep(mtimedelay)
        @eval using Order3
        sleep(mtimedelay)
        write(joinpath(dn3, "Order3.jl"), """
            module Order3
            using Order2
            g(::Order2.Ord2a) = 1
            end
            """)
        sleep(mtimedelay)
        write(joinpath(dn, "file.jl"), """
            struct Ord2 end
            struct Ord2a end
            """)
        @yry()
        @test Order3.g(Order2.Ord2a()) == 1

        rm_precompile("Order1")
        rm_precompile("Order2")
        pop!(LOAD_PATH)
    end

    # issue #8 and #197
    do_test("Module docstring") && @testset "Module docstring" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "ModDocstring", "src")
        mkpath(dn)
        write(joinpath(dn, "ModDocstring.jl"), """
            " Ahoy! "
            module ModDocstring

            include("dependency.jl")

            f() = 1

            end
            """)
        write(joinpath(dn, "dependency.jl"), "")
        sleep(mtimedelay)
        @eval using ModDocstring
        sleep(mtimedelay)
        @test ModDocstring.f() == 1
        ds = @doc(ModDocstring)
        @test get_docstring(ds) == "Ahoy! "

        write(joinpath(dn, "ModDocstring.jl"), """
            " Ahoy! "
            module ModDocstring

            include("dependency.jl")

            f() = 2

            end
            """)
        @yry()
        @test ModDocstring.f() == 2
        ds = @doc(ModDocstring)
        @test get_docstring(ds) == "Ahoy! "

        write(joinpath(dn, "ModDocstring.jl"), """
            " Hello! "
            module ModDocstring

            include("dependency.jl")

            f() = 3

            end
            """)
        @yry()
        @test ModDocstring.f() == 3
        ds = @doc(ModDocstring)
        @test get_docstring(ds) == "Hello! "
        rm_precompile("ModDocstring")

        # issue #197
        dn = joinpath(testdir, "ModDocstring2", "src")
        mkpath(dn)
        write(joinpath(dn, "ModDocstring2.jl"), """
            "docstring"
            module ModDocstring2
                "docstring for .Sub"
                module Sub
                end
            end
            """)
        sleep(mtimedelay)
        @eval using ModDocstring2
        sleep(mtimedelay)
        ds = @doc(ModDocstring2)
        @test get_docstring(ds) == "docstring"
        ds = @doc(ModDocstring2.Sub)
        @test get_docstring(ds) == "docstring for .Sub"
        write(joinpath(dn, "ModDocstring2.jl"), """
            "updated docstring"
            module ModDocstring2
                "updated docstring for .Sub"
                module Sub
                end
            end
            """)
        @yry()
        ds = @doc(ModDocstring2)
        @test get_docstring(ds) == "updated docstring"
        ds = @doc(ModDocstring2.Sub)
        @test get_docstring(ds) == "updated docstring for .Sub"
        rm_precompile("ModDocstring2")

        pop!(LOAD_PATH)
    end

    do_test("Changing docstring") && @testset "Changing docstring" begin
        # Compiled mode covers most docstring changes, so we have to go to
        # special effort to test the older interpreter-based solution.
        testdir = newtestdir()
        dn = joinpath(testdir, "ChangeDocstring", "src")
        mkpath(dn)
        write(joinpath(dn, "ChangeDocstring.jl"), """
            module ChangeDocstring
            "f" f() = 1
            g() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using ChangeDocstring
        sleep(mtimedelay)
        @test ChangeDocstring.f() == 1
        ds = @doc(ChangeDocstring.f)
        @test get_docstring(ds) == "f"
        @test ChangeDocstring.g() == 1
        ds = @doc(ChangeDocstring.g)
        @test get_docstring(ds) in (
            "No documentation found.",
            "No documentation found for private symbol.",
            "No documentation found for private binding ")
        # Ordinary route
        write(joinpath(dn, "ChangeDocstring.jl"), """
            module ChangeDocstring
            "h" f() = 1
            "g" g() = 1
            end
            """)
        @yry()
        ds = @doc(ChangeDocstring.f)
        @test get_docstring(ds) == "h"
        ds = @doc(ChangeDocstring.g)
        @test get_docstring(ds) == "g"

        # Now manually change the docstring
        ex = quote "g" f() = 1 end
        lwr = Meta.lower(ChangeDocstring, ex)
        frame = Frame(ChangeDocstring, lwr.args[1])
        exinfo = Revise.ExInfo(ex)
        ret = Revise._methods_by_execution!(JuliaInterpreter.RecursiveInterpreter(), exinfo,
                                            frame, trues(length(frame.framecode.src.code)); mode=:sigs)
        ds = @doc(ChangeDocstring.f)
        @test get_docstring(ds) == "g"

        rm_precompile("ChangeDocstring")

        # Test for #583
        dn = joinpath(testdir, "FirstDocstring", "src")
        mkpath(dn)
        write(joinpath(dn, "FirstDocstring.jl"), """
            module FirstDocstring
            g() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using FirstDocstring
        sleep(mtimedelay)
        @test FirstDocstring.g() == 1
        ds = @doc(FirstDocstring.g)
        @test get_docstring(ds) in (
            "No documentation found.",
            "No documentation found for private symbol.",
            "No documentation found for private binding ")
        write(joinpath(dn, "FirstDocstring.jl"), """
            module FirstDocstring
            "g" g() = 1
            end
            """)
        @yry()
        ds = @doc(FirstDocstring.g)
        @test get_docstring(ds) == "g"

        rm_precompile("FirstDocstring")
        pop!(LOAD_PATH)
    end

    do_test("Forced revision docstrings (issue #975)") && @testset "Forced revision docstrings (issue #975)" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "ForceDocstring", "src")
        mkpath(dn)
        write(joinpath(dn, "ForceDocstring.jl"), """
            module ForceDocstring
            "f" f() = 1
            "g" g() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using ForceDocstring
        sleep(mtimedelay)
        @test ForceDocstring.f() == 1
        # `revise(mod; force=true)` re-evaluates every definition, which rewrites
        # each docstring; `Base.Docs` warns on each rewrite unless we suppress it.
        logs, _ = Test.collect_test_logs() do
            revise(ForceDocstring)
        end
        @latestworld
        @test !any(r -> occursin("Replacing docs", r.message), logs)
        @test ForceDocstring.f() == 1
        @test ForceDocstring.g() == 1

        rm_precompile("ForceDocstring")
        pop!(LOAD_PATH)
    end

    do_test("New documented binding") && @testset "New documented binding" begin
        # A revision that introduces a brand-new documented binding creates that
        # binding in a world newer than the one Revise's machinery runs in (the
        # frozen world of issue #552). Revise's `doc!` interception must read the
        # binding back at the latest world; a plain access would be a
        # backdated-const read, which Julia 1.12 reports with
        # "access to binding ... in a world prior to its definition world"
        # (and will turn into an error in a future release).
        testdir = newtestdir()
        dn = joinpath(testdir, "NewDocBinding", "src")
        mkpath(dn)
        write(joinpath(dn, "NewDocBinding.jl"), """
            module NewDocBinding
            g() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using NewDocBinding
        sleep(mtimedelay)
        @test NewDocBinding.g() == 1
        write(joinpath(dn, "NewDocBinding.jl"), """
            module NewDocBinding
            g() = 1
            "h" h() = 1
            end
            """)
        # The backdated-const admonition is printed by the runtime (not via the
        # logging system), so capture stderr at the stream level to detect it.
        errfile, errio = mktemp()
        try
            redirect_stderr(errio) do
                yry()
            end
        finally
            close(errio)
        end
        @latestworld
        stderrtxt = read(errfile, String)
        rm(errfile; force=true)
        @test !occursin("world prior to its definition world", stderrtxt)
        @test NewDocBinding.h() == 1
        ds = @doc(NewDocBinding.h)
        @test get_docstring(ds) == "h"

        rm_precompile("NewDocBinding")
        pop!(LOAD_PATH)
    end

    do_test("doc expr signature") && @testset "Docstring attached to signatures" begin
        md = Revise.ModuleExprsInfos(Main)
        parse_source!(md, """
            module DocstringSigsOnly
            function f end
            "basecase" f(x)
            "basecase with type" f(x::Int)
            "basecase no varname" f(::Float64)
            "where" f(x::T) where T <: Int8
            "where no varname" f(::T) where T <: String
            end
            """, "test2", Main)
        # Simply test that the "bodies" of the doc exprs are not included as
        # standalone expressions.
        @test length(md[Main.DocstringSigsOnly]) == 6 # 1 func + 5 doc exprs
    end

    do_test("Undef in docstrings") && @testset "Undef in docstrings" begin
        fn = Base.find_source_file("abstractset.jl")   # has lots of examples of """str""" func1, func2
        mod_exs_infos_old = parse_source(fn, Base)
        mod_exs_infos_new = parse_source(fn, Base)
        odict = mod_exs_infos_old[Base]
        ndict = mod_exs_infos_new[Base]
        for (k, _) in odict
            @test haskey(ndict, k)
        end
    end

    do_test("Macro docstrings (issue #309)") && @testset "Macro docstrings (issue #309)" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "MacDocstring", "src")
        mkpath(dn)
        write(joinpath(dn, "MacDocstring.jl"), """
            module MacDocstring

            macro myconst(name, val)
                quote
                    \"\"\"
                        mydoc
                    \"\"\"
                    const \$(esc(name)) = \$val
                end
            end

            @myconst c 1.2
            f() = 1

            end # module
            """)
        sleep(mtimedelay)
        @eval using MacDocstring
        sleep(mtimedelay)
        @test MacDocstring.f() == 1
        ds = @doc(MacDocstring.c)
        @test strip(get_docstring(ds)) == "mydoc"

        write(joinpath(dn, "MacDocstring.jl"), """
            module MacDocstring

            macro myconst(name, val)
                quote
                    \"\"\"
                        mydoc
                    \"\"\"
                    const \$(esc(name)) = \$val
                end
            end

            @myconst c 1.2
            f() = 2

            end # module
            """)
        @yry()
        @test MacDocstring.f() == 2
        ds = @doc(MacDocstring.c)
        @test strip(get_docstring(ds)) == "mydoc"

        rm_precompile("MacDocstring")
        pop!(LOAD_PATH)
    end

    # issue #165
    do_test("Changing @inline annotations") && @testset "Changing @inline annotations" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "PerfAnnotations", "src")
        mkpath(dn)
        write(joinpath(dn, "PerfAnnotations.jl"), """
            module PerfAnnotations

            @inline hasinline(x) = x
            check_hasinline(x) = hasinline(x)

            @noinline hasnoinline(x) = x
            check_hasnoinline(x) = hasnoinline(x)

            notannot1(x) = x
            check_notannot1(x) = notannot1(x)

            notannot2(x) = x
            check_notannot2(x) = notannot2(x)

            end
            """)
        sleep(mtimedelay)
        @eval using PerfAnnotations
        sleep(mtimedelay)
        @test PerfAnnotations.check_hasinline(3) == 3
        @test PerfAnnotations.check_hasnoinline(3) == 3
        @test PerfAnnotations.check_notannot1(3) == 3
        @test PerfAnnotations.check_notannot2(3) == 3
        code = get_code(PerfAnnotations.check_hasinline, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        code = get_code(PerfAnnotations.check_hasnoinline, Tuple{Int})
        @test length(code) == 2 && code[1].head === :invoke
        code = get_code(PerfAnnotations.check_notannot1, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        code = get_code(PerfAnnotations.check_notannot2, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        write(joinpath(dn, "PerfAnnotations.jl"), """
            module PerfAnnotations

            hasinline(x) = x
            check_hasinline(x) = hasinline(x)

            hasnoinline(x) = x
            check_hasnoinline(x) = hasnoinline(x)

            @inline notannot1(x) = x
            check_notannot1(x) = notannot1(x)

            @noinline notannot2(x) = x
            check_notannot2(x) = notannot2(x)

            end
            """)
        @yry()
        @test PerfAnnotations.check_hasinline(3) == 3
        @test PerfAnnotations.check_hasnoinline(3) == 3
        @test PerfAnnotations.check_notannot1(3) == 3
        @test PerfAnnotations.check_notannot2(3) == 3
        code = get_code(PerfAnnotations.check_hasinline, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        code = get_code(PerfAnnotations.check_hasnoinline, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        code = get_code(PerfAnnotations.check_notannot1, Tuple{Int})
        @test length(code) == 1 && isreturning_slot(code[1], 2)
        code = get_code(PerfAnnotations.check_notannot2, Tuple{Int})
        @test length(code) == 2 && code[1].head === :invoke
        rm_precompile("PerfAnnotations")

        pop!(LOAD_PATH)
    end

    do_test("Revising macros") && @testset "Revising macros" begin
        # issue #174
        testdir = newtestdir()
        dn = joinpath(testdir, "MacroRevision", "src")
        mkpath(dn)
        write(joinpath(dn, "MacroRevision.jl"), """
            module MacroRevision
            macro change(foodef)
                foodef.args[2].args[2] = 1
                esc(foodef)
            end
            @change foo(x) = 0
            end
            """)
        sleep(mtimedelay)
        @eval using MacroRevision
        sleep(mtimedelay)
        @test MacroRevision.foo("hello") == 1

        write(joinpath(dn, "MacroRevision.jl"), """
            module MacroRevision
            macro change(foodef)
                foodef.args[2].args[2] = 2
                esc(foodef)
            end
            @change foo(x) = 0
            end
            """)
        @yry()
        @test MacroRevision.foo("hello") == 1
        revise(MacroRevision)
        @latestworld
        @test MacroRevision.foo("hello") == 2

        write(joinpath(dn, "MacroRevision.jl"), """
            module MacroRevision
            macro change(foodef)
                foodef.args[2].args[2] = 3
                esc(foodef)
            end
            @change foo(x) = 0
            end
            """)
        @yry()
        @test MacroRevision.foo("hello") == 2
        revise(MacroRevision)
        @latestworld
        @test MacroRevision.foo("hello") == 3
        rm_precompile("MacroRevision")

        # issue #435
        dn = joinpath(testdir, "MacroSigs", "src")
        mkpath(dn)
        write(joinpath(dn, "MacroSigs.jl"), """
            module MacroSigs
            end
            """)
        sleep(mtimedelay)
        @eval using MacroSigs
        sleep(mtimedelay)
        write(joinpath(dn, "MacroSigs.jl"), """
            module MacroSigs
            macro testmac(fname)
                esc(quote
                    function some_fun end
                    \$fname() = 1
                    end)
            end

            @testmac blah
            end
            """)
        @yry()
        @test MacroSigs.blah() == 1
        @test haskey(CodeTracking.method_info, MethodInfoKey(@which MacroSigs.blah()))
        rm_precompile("MacroSigs")

        # Issue #568 (a macro *execution* bug)
        dn = joinpath(testdir, "MacroLineNos568", "src")
        mkpath(dn)
        write(joinpath(dn, "MacroLineNos568.jl"), """
            module MacroLineNos568
            using MacroTools: @q

            function my_fun end

            macro some_macro(value)
                return esc(@q \$MacroLineNos568.my_fun() = \$value)
            end

            @some_macro 20
            end
            """)
        sleep(mtimedelay)
        @eval using MacroLineNos568
        sleep(mtimedelay)
        @test MacroLineNos568.my_fun() == 20
        write(joinpath(dn, "MacroLineNos568.jl"), """
            module MacroLineNos568
            using MacroTools: @q

            function my_fun end

            macro some_macro(value)
                return esc(@q \$MacroLineNos568.my_fun() = \$value)
            end

            @some_macro 30
            end
            """)
        @yry()
        @test MacroLineNos568.my_fun() == 30
        rm_precompile("MacroLineNos568")

        # Macros that create empty functions (another macro *execution* bug, issue #792)
        file = tempname()
        write(file, "@empty_function issue792f1\n")
        sleep(mtimedelay)
        includet(ReviseTestPrivate, file)
        sleep(mtimedelay)
        @test isempty(methods(ReviseTestPrivate.issue792f1))
        open(file, "a") do f
            println(f, "@empty_function issue792f2")
        end
        @yry()
        @test isempty(methods(ReviseTestPrivate.issue792f2))
        rm(file)

        pop!(LOAD_PATH)
    end

    do_test("More arg-modifying macros") && @testset "More arg-modifying macros" begin
        # issue #183
        testdir = newtestdir()
        dn = joinpath(testdir, "ArgModMacros", "src")
        mkpath(dn)
        write(joinpath(dn, "ArgModMacros.jl"), """
            module ArgModMacros

            using EponymTuples

            const revision = Ref(0)

            function hyper_loglikelihood(@eponymargs(μ, σ, LΩ), @eponymargs(w̃s, α̃s, β̃s))
                revision[] = 1
                loglikelihood_normal(@eponymtuple(μ, σ, LΩ), vcat(w̃s, α̃s, β̃s))
            end

            loglikelihood_normal(@eponymargs(μ, σ, LΩ), stuff) = stuff

            end
            """)
        sleep(mtimedelay)
        @eval using ArgModMacros
        sleep(mtimedelay)
        @test ArgModMacros.hyper_loglikelihood((μ=1, σ=2, LΩ=3), (w̃s=4, α̃s=5, β̃s=6)) == [4,5,6]
        @test ArgModMacros.revision[] == 1
        write(joinpath(dn, "ArgModMacros.jl"), """
            module ArgModMacros

            using EponymTuples

            const revision = Ref(0)

            function hyper_loglikelihood(@eponymargs(μ, σ, LΩ), @eponymargs(w̃s, α̃s, β̃s))
                revision[] = 2
                loglikelihood_normal(@eponymtuple(μ, σ, LΩ), vcat(w̃s, α̃s, β̃s))
            end

            loglikelihood_normal(@eponymargs(μ, σ, LΩ), stuff) = stuff

            end
            """)
        @yry()
        @test ArgModMacros.hyper_loglikelihood((μ=1, σ=2, LΩ=3), (w̃s=4, α̃s=5, β̃s=6)) == [4,5,6]
        @test ArgModMacros.revision[] == 2
        rm_precompile("ArgModMacros")
        pop!(LOAD_PATH)
    end

    do_test("Line numbers") && @testset "Line numbers" begin
        # issue #27
        testdir = newtestdir()
        modname = "LineNumberMod"
        dn = joinpath(testdir, modname, "src")
        mkpath(dn)
        write(joinpath(dn, modname*".jl"), "module $modname include(\"incl.jl\") end")
        write(joinpath(dn, "incl.jl"), """
            0
            0
            1
            2
            3
            4
            5
            6
            7
            8

            foo(x) = x+5

            foo(y::Int) = y-51
            """)
        sleep(mtimedelay)
        @eval using LineNumberMod
        sleep(mtimedelay)
        lines = Int[]
        files = String[]
        for m in methods(LineNumberMod.foo)
            push!(files, String(m.file))
            push!(lines, m.line)
        end
        @test all(f->endswith(string(f), "incl.jl"), files)
        write(joinpath(dn, "incl.jl"), """
            0
            0
            1
            2
            3
            4
            5
            6
            7
            8

            foo(x) = x+6

            foo(y::Int) = y-51
            """)
        @yry()
        for m in methods(LineNumberMod.foo)
            @test endswith(string(m.file), "incl.jl")
            @test m.line ∈ lines
        end
        rm_precompile("LineNumberMod")
        pop!(LOAD_PATH)
    end

    do_test("Line numbers in backtraces and warnings") && @testset "Line numbers in backtraces and warnings" begin
        filename = randtmp() * ".jl"
        write(filename, """
            function triggered(iserr::Bool, iswarn::Bool)
                iserr && error("error")
                iswarn && @warn "Information"
                return nothing
            end
            """)
        sleep(mtimedelay)
        includet(filename)
        sleep(mtimedelay)
        @latestworld
        try
            triggered(true, false)
            @test false
        catch
            st = stacktrace(catch_backtrace())
            Revise.update_stacktrace_lineno!(st)
            bt = throwing_function(st)
            @test bt.file == Symbol(filename) && bt.line == 2
        end
        io = IOBuffer()
        if isdefined(Base, :methodloc_callback)
            print(io, methods(triggered))
            @test occursin(filename * ":1", String(take!(io)))
        end
        write(filename, """
            # A comment to change the line numbers
            function triggered(iserr::Bool, iswarn::Bool)
                iserr && error("error")
                iswarn && @warn "Information"
                return nothing
            end
            """)
        @yry()
        try
            triggered(true, false)
            @test false
        catch
            bt = throwing_function(Revise.update_stacktrace_lineno!(stacktrace(catch_backtrace())))
            @test bt.file == Symbol(filename) && bt.line == 3
        end
        st = try
            triggered(true, false)
            @test false
        catch
            stacktrace(catch_backtrace())
        end
        targetstr = basename(filename * ":3")
        Base.show_backtrace(io, st)
        @test occursin(targetstr, String(take!(io)))
        # Long stacktraces take a different path, test this too
        while length(st) < 100
            st = vcat(st, st)
        end
        Base.show_backtrace(io, st)
        @test occursin(targetstr, String(take!(io)))
        if isdefined(Base, :methodloc_callback)
            print(io, methods(triggered))
            @test occursin(basename(filename * ":2"), String(take!(io)))
        end

        push!(to_remove, filename)
    end

    # Issue #43
    do_test("New submodules") && @testset "New submodules" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "Submodules", "src")
        mkpath(dn)
        write(joinpath(dn, "Submodules.jl"), """
            module Submodules
            f() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using Submodules
        sleep(mtimedelay)
        @test Submodules.f() == 1
        write(joinpath(dn, "Submodules.jl"), """
            module Submodules
            f() = 1
            module Sub
            g() = 2
            end
            end
            """)
        @yry()
        @test Submodules.f() == 1
        @test Submodules.Sub.g() == 2
        rm_precompile("Submodules")
        pop!(LOAD_PATH)
    end

    do_test("Submodule in same file (#718)") && @testset "Submodule in same file (#718)" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "TestPkg718", "src")
        mkpath(dn)
        write(joinpath(dn, "TestPkg718.jl"), """
            module TestPkg718

            module TestModule718
                export _VARIABLE_UNASSIGNED
                global _VARIABLE_UNASSIGNED = -84.0
            end

            using .TestModule718

            end
            """)
        sleep(mtimedelay)
        @eval using TestPkg718
        sleep(mtimedelay)
        @test TestPkg718._VARIABLE_UNASSIGNED == -84.0
        write(joinpath(dn, "TestPkg718.jl"), """
            module TestPkg718

            module TestModule718
                export _VARIABLE_UNASSIGNED
                global _VARIABLE_UNASSIGNED = -83.0
            end

            using .TestModule718

            end
            """)
        @yry()
        @test TestPkg718._VARIABLE_UNASSIGNED == -83.0

        rm_precompile("TestPkg718")
        pop!(LOAD_PATH)
    end

    do_test("Timing (issue #341)") && @testset "Timing (issue #341)" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "Timing", "src")
        mkpath(dn)
        write(joinpath(dn, "Timing.jl"), """
            module Timing
            f(x) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using Timing
        sleep(mtimedelay)
        @test Timing.f(nothing) == 1
        tmpfile = joinpath(dn, "Timing_temp.jl")
        write(tmpfile, """
            module Timing
            f(x) = 2
            end
            """)
        @yry(expect_revision=false)   # tmpfile is not the tracked path; no revision yet
        @test Timing.f(nothing) == 1
        mv(tmpfile, pathof(Timing), force=true)
        @yry()
        @test Timing.f(nothing) == 2

        rm_precompile("Timing")
    end

    # Regression test for issue #1025: PR #1003 (TOCTOU fix) broke the #341 fix.
    # An editor save typically generates a burst of directory events: creating a temp file,
    # renaming it over the tracked file, etc. With #1003, every spurious event advanced
    # wl.timestamp so the tracked file's ctime could fall behind it and be missed.
    #
    # The fix was to track per-file ctimes instead of a directory timestamp.
    # A file is detected when its ctime changes, not when it exceeds a watermark.
    # Spurious events touch no tracked files, so no file_ctimes entry changes and
    # nothing is falsely reported as changed.
    do_test("Spurious events don't prevent detection (issue #1025)") &&
    @testset "Spurious events don't prevent detection (issue #1025)" begin
        testdir = mktempdir()
        push!(to_remove, testdir)
        tracked_file = joinpath(testdir, "tracked1025.jl")

        write(tracked_file, "tracked1025_f() = 1")
        sleep(0.1)  # let ctime settle before creating WatchList

        # Set up a WatchList as init_watching would (baseline ctime recorded at push time)
        pkgid = Base.PkgId(UUIDs.uuid4(), "tracked1025_test")
        @lock Revise.revise_lock begin
            wl = Revise.WatchList()
            push!(wl, basename(tracked_file) => pkgid)
            wl.file_ctimes[basename(tracked_file)] = stat(tracked_file).ctime
            Revise.watched_files[testdir] = wl
        end

        try
            # --- Phase 1: spurious event; no tracked file changed ---
            # Start the watcher *before* any modification (naturalistic ordering).
            t1 = @async Revise.watch_files_via_dir(testdir)
            sleep(0.05)  # give t1 time to block on wait_changed
            write(joinpath(testdir, "spurious1025a.tmp"), "")
            wait(t1)
            rm(joinpath(testdir, "spurious1025a.tmp"), force=true)

            # Invariant check (deterministic on all platforms):
            # a spurious event must not change any tracked file's recorded ctime.
            @test Revise.watched_files[testdir].file_ctimes[basename(tracked_file)] == stat(tracked_file).ctime

            # --- Phase 2: edit the tracked file ---
            write(tracked_file, "tracked1025_f() = 2")

            # --- Phase 3: detection event; tracked file must be found ---
            detected = Ref{Vector{String}}(String[])
            t2 = @async begin
                latestfiles, _ = Revise.watch_files_via_dir(testdir)
                detected[] = [first(p) for p in latestfiles]
            end
            sleep(0.05)
            write(joinpath(testdir, "spurious1025b.tmp"), "")
            wait(t2)
            rm(joinpath(testdir, "spurious1025b.tmp"), force=true)

            @test basename(tracked_file) ∈ detected[]
        finally
            @lock Revise.revise_lock begin
                delete!(Revise.watched_files, testdir)
            end
        end
    end

    do_test("DO NOT PARSE") && @testset "DO NOT PARSE" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "DoNotParse", "src")
        mkpath(dn)
        write(joinpath(dn, "DoNotParse.jl"), """
            # REVISE: DO NOT PARSE
            module DoNotParse
            f(x) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using DoNotParse
        sleep(mtimedelay)
        @test DoNotParse.f(1) == 1
        write(joinpath(dn, "DoNotParse.jl"), """
            # REVISE: DO NOT PARSE
            module DoNotParse
            f(x) = 2
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        @test DoNotParse.f(1) == 1
        @test isempty(logs)
        rm_precompile("DoNotParse")
        pop!(LOAD_PATH)
    end

    do_test("Method deletion") && @testset "Method deletion" begin
        Core.eval(Base, :(revisefoo(x::Float64) = 1)) # to test cross-module method scoping
        testdir = newtestdir()
        dn = joinpath(testdir, "MethDel", "src")
        mkpath(dn)
        write(joinpath(dn, "MethDel.jl"), """
            __precompile__(false)   # "clean" Base doesn't have :revisefoo
            module MethDel
            f(x) = 1
            f(x::Int) = 2
            g(x::Vector{T}, y::T) where T = 1
            g(x::Array{T,N}, y::T) where N where T = 2
            g(::Array, ::Any) = 3
            h(x::Array{T}, y::T) where T = g(x, y)
            k(::Int; badchoice=1) = badchoice
            Base.revisefoo(x::Int) = 2
            struct Private end
            Base.revisefoo(::Private) = 3

            dfltargs(x::Int8, y::Int=0, z::Float32=1.0f0) = x+y+z

            hasmacro1(@nospecialize(x)) = x
            hasmacro2(@nospecialize(x::Int)) = x
            hasmacro3(@nospecialize(x::Int), y::Float64) = x

            hasdestructure1(x, (count, name)) = name^count
            hasdestructure2(x, (count, name)::Tuple{Int,Any}) = name^count

            struct A end
            struct B end

            checkunion(a::Union{Nothing, A}) = 1

            methgensym(::Vector{<:Integer}) = 1

            mapf(fs, x) = (fs[1](x), mapf(Base.tail(fs), x)...)
            mapf(::Tuple{}, x) = ()

            for T in (Int, Float64, String)
                @eval mytypeof(x::\$T) = \$T
            end

            @generated function firstparam(A::AbstractArray)
                T = A.parameters[1]
                return :(\$T)
            end

            end
            """)
        sleep(mtimedelay)
        @eval using MethDel
        sleep(mtimedelay)
        @test MethDel.f(1.0) == 1
        @test MethDel.f(1) == 2
        @test MethDel.g(rand(3), 1.0) == 1
        @test MethDel.g(rand(3, 3), 1.0) == 2
        @test MethDel.g(Int[], 1.0) == 3
        @test MethDel.h(rand(3), 1.0) == 1
        @test MethDel.k(1) == 1
        @test MethDel.k(1; badchoice=2) == 2
        @test MethDel.hasmacro1(1) == 1
        @test MethDel.hasmacro2(1) == 1
        @test MethDel.hasmacro3(1, 0.0) == 1
        @test MethDel.hasdestructure1(0, (3, "hi")) == "hihihi"
        @test MethDel.hasdestructure2(0, (3, "hi")) == "hihihi"
        @test Base.revisefoo(1.0) == 1
        @test Base.revisefoo(1) == 2
        @test Base.revisefoo(MethDel.Private()) == 3
        @test MethDel.dfltargs(Int8(2)) == 3.0f0
        @test MethDel.dfltargs(Int8(2), 5) == 8.0f0
        @test MethDel.dfltargs(Int8(2), 5, -17.0f0) == -10.0f0
        @test MethDel.checkunion(nothing) == 1
        @test MethDel.methgensym([1]) == 1
        @test_throws MethodError MethDel.methgensym([1.0])
        @test MethDel.mapf((x->x+1, x->x+0.1), 3) == (4, 3.1)
        @test MethDel.mytypeof(1) === Int
        @test MethDel.mytypeof(1.0) === Float64
        @test MethDel.mytypeof("hi") === String
        @test MethDel.firstparam(rand(2,2)) === Float64
        write(joinpath(dn, "MethDel.jl"), """
            module MethDel
            f(x) = 1
            g(x::Array{T,N}, y::T) where N where T = 2
            h(x::Array{T}, y::T) where T = g(x, y)
            k(::Int; goodchoice=-1) = goodchoice
            dfltargs(x::Int8, yz::Tuple{Int,Float32}=(0,1.0f0)) = x+yz[1]+yz[2]

            struct A end
            struct B end

            checkunion(a::Union{Nothing, B}) = 2

            methgensym(::Vector{<:Real}) = 1

            mapf(fs::F, x) where F = (fs[1](x), mapf(Base.tail(fs), x)...)
            mapf(::Tuple{}, x) = ()

            for T in (Int, String)
                @eval mytypeof(x::\$T) = \$T
            end

            end
            """)
        @yry()
        @test MethDel.f(1.0) == 1
        @test MethDel.f(1) == 1
        @test MethDel.g(rand(3), 1.0) == 2
        @test MethDel.g(rand(3, 3), 1.0) == 2
        @test_throws MethodError MethDel.g(Int[], 1.0)
        @test MethDel.h(rand(3), 1.0) == 2
        @test_throws MethodError MethDel.k(1; badchoice=2)
        @test MethDel.k(1) == -1
        @test MethDel.k(1; goodchoice=10) == 10
        @test_throws MethodError MethDel.hasmacro1(1)
        @test_throws MethodError MethDel.hasmacro2(1)
        @test_throws MethodError MethDel.hasmacro3(1, 0.0)
        @test_throws MethodError MethDel.hasdestructure1(0, (3, "hi"))
        @test_throws MethodError MethDel.hasdestructure2(0, (3, "hi"))
        @test Base.revisefoo(1.0) == 1
        @test_throws MethodError Base.revisefoo(1)
        @test_throws MethodError Base.revisefoo(MethDel.Private())
        @test MethDel.dfltargs(Int8(2)) == 3.0f0
        @test MethDel.dfltargs(Int8(2), (5,-17.0f0)) == -10.0f0
        @test_throws MethodError MethDel.dfltargs(Int8(2), 5) == 8.0f0
        @test_throws MethodError MethDel.dfltargs(Int8(2), 5, -17.0f0) == -10.0f0
        @test MethDel.checkunion(nothing) == 2
        @test MethDel.methgensym([1]) == 1
        @test MethDel.methgensym([1.0]) == 1
        @test length(methods(MethDel.methgensym)) == 1
        @test MethDel.mapf((x->x+1, x->x+0.1), 3) == (4, 3.1)
        @test length(methods(MethDel.mapf)) == 2
        @test MethDel.mytypeof(1) === Int
        @test_throws MethodError MethDel.mytypeof(1.0)
        @test MethDel.mytypeof("hi") === String
        @test_throws MethodError MethDel.firstparam(rand(2,2))

        Base.delete_method(first(methods(Base.revisefoo)))
    end

    do_test("Method deletion specificity") && @testset "Method deletion specificity" begin
        ex1 = :(methspecificity(x::Int) = 1)
        ex2 = :(methspecificity(x::Integer) = 2)
        Core.eval(ReviseTestPrivate, ex1)
        Core.eval(ReviseTestPrivate, ex2)
        exsig1 = Revise.RelocatableExpr(ex1) => [Revise.SigInfo(nothing, Tuple{typeof(ReviseTestPrivate.methspecificity),Int})]
        exsig2 = Revise.RelocatableExpr(ex2) => [Revise.SigInfo(nothing, Tuple{typeof(ReviseTestPrivate.methspecificity),Integer})]
        f_old, f_new = Revise.ExprsInfos(exsig1, exsig2), Revise.ExprsInfos(exsig2)
        let reeval_list = Base.IdSet{Union{Method,Type}}()
            handled_types = Base.IdSet{Type}()
            world = Base.get_world_counter()
            Revise.delete_missing!(f_old, f_new, reeval_list, handled_types, world)
            m = @which ReviseTestPrivate.methspecificity(1)
            @test m.sig.parameters[2] === Integer
        end
        let reeval_list = Base.IdSet{Union{Method,Type}}()
            handled_types = Base.IdSet{Type}()
            world = Base.get_world_counter()
            Revise.delete_missing!(f_old, f_new, reeval_list, handled_types, world)
            m = @which ReviseTestPrivate.methspecificity(1)
            @test m.sig.parameters[2] === Integer
        end
    end

    # Repairing an orphaned binding requires binding partitions (`delete_binding`, 1.12+).
    do_test("Orphaned binding realias (issue #239)") && Base.VERSION >= v"1.12.0-DEV.2047" &&
            @testset "Orphaned binding realias (issue #239)" begin
        # An accidental `iterate(x::Foo)=...` defines a module-local `iterate` that shadows
        # `Base.iterate`, and `caller` binds to it. Correcting it to `Base.iterate(x::Foo)=...`
        # must leave the unqualified reference in `caller` resolving to `Base.iterate`, without
        # a session restart.
        testdir = newtestdir()
        dn = joinpath(testdir, "Orphan239", "src"); mkpath(dn)
        fn = joinpath(dn, "Orphan239.jl")
        write(fn, """
            module Orphan239
            struct Foo end
            caller() = iterate(Foo())
            iterate(x::Foo) = (42, nothing)   # accidental shadow of Base.iterate
            end
            """)
        sleep(mtimedelay)
        @eval using Orphan239
        @test Orphan239.caller() == (42, nothing)
        @test Orphan239.iterate !== Base.iterate
        sleep(mtimedelay)
        write(fn, """
            module Orphan239
            struct Foo end
            caller() = iterate(Foo())
            Base.iterate(x::Foo) = (42, nothing)   # fixed: extend Base.iterate
            end
            """)
        @yry()
        @test Orphan239.iterate === Base.iterate     # orphaned binding re-imported from Base
        @test Orphan239.caller() == (42, nothing)    # `caller` works again, no restart

        rm_precompile("Orphan239")

        # The bug is not specific to `Base`: it arises for any name brought into implicit scope
        # by a bare `using`. Here `flatten` (not a `Base` export) is shadowed via
        # `using Base.Iterators`, and the repair must re-import it from `Base.Iterators`.
        dn = joinpath(testdir, "Orphan239c", "src"); mkpath(dn)
        fn = joinpath(dn, "Orphan239c.jl")
        write(fn, """
            module Orphan239c
            using Base.Iterators
            struct Foo end
            caller() = flatten(Foo())
            flatten(x::Foo) = :shadow   # accidental shadow of Base.Iterators.flatten
            end
            """)
        sleep(mtimedelay)
        @eval using Orphan239c
        @test Orphan239c.caller() == :shadow
        @test Orphan239c.flatten !== Base.Iterators.flatten
        sleep(mtimedelay)
        write(fn, """
            module Orphan239c
            using Base.Iterators
            struct Foo end
            caller() = flatten(Foo())
            Base.Iterators.flatten(x::Foo) = :fixed   # fixed: extend Base.Iterators.flatten
            end
            """)
        @yry()
        @test Orphan239c.flatten === Base.Iterators.flatten   # re-imported from Base.Iterators
        @test Orphan239c.caller() == :fixed                   # `caller` works again, no restart

        rm_precompile("Orphan239c")

        # The repair must be narrow: deleting a method that is NOT replaced by a same-named
        # function leaves the (now empty) binding alone rather than re-importing it.
        dn = joinpath(testdir, "Orphan239b", "src"); mkpath(dn)
        fn = joinpath(dn, "Orphan239b.jl")
        write(fn, """
            module Orphan239b
            gone(x::Int) = 1
            keep() = 2
            end
            """)
        sleep(mtimedelay)
        @eval using Orphan239b
        @test Orphan239b.gone(0) == 1
        sleep(mtimedelay)
        write(fn, """
            module Orphan239b
            function fwd end    # genuine forward declaration, must stay empty
            keep() = 3
            end
            """)
        @yry()
        @test Orphan239b.keep() == 3
        @test isempty(methods(Orphan239b.gone))             # emptied, not re-imported
        @test isdefined(Orphan239b, :fwd) && isempty(methods(Orphan239b.fwd))  # left untouched

        rm_precompile("Orphan239b")
    end

    do_test("Duplicate macro-defined methods") && @testset "Duplicate macro-defined methods" begin
        # issue #668: two definitions sharing one signature, where the method's recorded
        # location comes from a macro (a different file than the source). Deleting one of
        # them must not fail with `AssertionError: ld[idx] < typemax(eltype(ld))`.
        testdir = newtestdir()
        dn = joinpath(testdir, "Dup668", "src"); mkpath(dn)
        fn = joinpath(dn, "Dup668.jl")
        # `@deffoo` stamps the method body with a LineNumberNode from a phantom file,
        # mimicking what macros like `@views`/`@timing` do to a wrapped function.
        write(fn, """
            __precompile__(false)
            module Dup668
            macro deffoo(val)
                esc(Expr(:function, :(foo()),
                         Expr(:block, LineNumberNode(99, Symbol("phantom.jl")), val)))
            end
            @deffoo 1
            @deffoo 2
            end""")
        sleep(mtimedelay)
        using Dup668
        sleep(mtimedelay)
        @test Dup668.foo() == 2
        key = Revise.MethodInfoKey(nothing, first(methods(Dup668.foo)).sig)
        # Remove the first (duplicate) definition, leaving a single method.
        write(fn, """
            __precompile__(false)
            module Dup668
            macro deffoo(val)
                esc(Expr(:function, :(foo()),
                         Expr(:block, LineNumberNode(99, Symbol("phantom.jl")), val)))
            end
            @deffoo 2
            end""")
        @yry()
        @test isempty(Revise.queue_errors)
        @test Dup668.foo() == 2
        @test length(CodeTracking.method_info[key]) == 1

        rm_precompile("Dup668")
    end

    do_test("Duplicate method warning") && @testset "Duplicate method warning" begin
        # issue #889: a duplicated method signature in a precompilable package works in the
        # running session but fails the next precompilation. Revise warns and tracks it in
        # `duplicated_signatures`, clearing the state once the duplicate is removed.
        testdir = newtestdir()
        dn = joinpath(testdir, "DupWarn", "src"); mkpath(dn)
        fn = joinpath(dn, "DupWarn.jl")
        write(fn, "module DupWarn\nfoo(x::Int) = 1\nend\n")
        sleep(mtimedelay)
        @eval using DupWarn
        @test DupWarn.foo(1) == 1
        @yry()  # populate `method_info` for the original definition
        # Track this signature specifically: other test packages may leave unrelated
        # duplicates in the global `duplicated_signatures`.
        key = Revise.MethodInfoKey(nothing, first(methods(DupWarn.foo)).sig)
        @test !haskey(Revise.duplicated_signatures, key)

        # Add a second definition of the same signature.
        write(fn, "module DupWarn\nfoo(x::Int) = 1\nfoo(x::Int) = 2\nend\n")
        sleep(mtimedelay)
        @test_logs (:warn, r"defined in more than one location") match_mode=:any yry()
        @latestworld
        @test DupWarn.foo(3) == 2                       # still works in this session
        @test haskey(Revise.duplicated_signatures, key)
        # `duplicate_methods()` re-reports the tracked duplicates on demand.
        @test_logs (:warn, r"defined in more than one location") match_mode=:any Revise.duplicate_methods()

        # Removing the duplicate clears the tracked state.
        write(fn, "module DupWarn\nfoo(x::Int) = 99\nend\n")
        sleep(mtimedelay)
        @yry()
        @test DupWarn.foo(3) == 99
        @test !haskey(Revise.duplicated_signatures, key)

        rm_precompile("DupWarn")
    end

    do_test("Duplicate method warning exclusions") && @testset "Duplicate method warning exclusions" begin
        # The warning is scoped to precompilable packages: `includet`/`Main` scripts and
        # `__precompile__(false)` packages never precompile, so duplicate methods there are
        # harmless (and sometimes deliberate) and must not be flagged (issue #889).
        testdir = newtestdir()

        # `includet` script: a duplicated method must not be tracked.
        script = joinpath(testdir, "dupscript.jl")
        write(script, "dupscriptfn(x::Int) = 1\n")
        sleep(mtimedelay)
        includet(script)
        write(script, "dupscriptfn(x::Int) = 1\ndupscriptfn(x::Int) = 2\n")
        sleep(mtimedelay)
        @yry()
        scriptkey = Revise.MethodInfoKey(nothing, first(methods(Main.dupscriptfn)).sig)
        @test !haskey(Revise.duplicated_signatures, scriptkey)

        # `__precompile__(false)` package: likewise not tracked.
        dn = joinpath(testdir, "NoPCDup", "src"); mkpath(dn)
        fn = joinpath(dn, "NoPCDup.jl")
        write(fn, "__precompile__(false)\nmodule NoPCDup\nbar(x::Int) = 1\nend\n")
        sleep(mtimedelay)
        @eval using NoPCDup
        @test NoPCDup.bar(1) == 1
        write(fn, "__precompile__(false)\nmodule NoPCDup\nbar(x::Int) = 1\nbar(x::Int) = 2\nend\n")
        sleep(mtimedelay)
        @yry()
        # Assert only the exclusion, not that the revision landed: the extra
        # filewatching CI pass can drop the event, and the duplicate would then simply
        # never be created. Either way this signature must not be flagged.
        barkey = Revise.MethodInfoKey(nothing, first(methods(NoPCDup.bar)).sig)
        @test !haskey(Revise.duplicated_signatures, barkey)

        rm_precompile("NoPCDup")
    end

    do_test("revise_file_now") && @testset "revise_file_now" begin
        # Very rarely this is used for debugging
        testdir = newtestdir()
        dn = joinpath(testdir, "ReviseFileNow", "src")
        mkpath(dn)
        fn = joinpath(dn, "ReviseFileNow.jl")
        write(fn, """
            module ReviseFileNow
            f(x) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using ReviseFileNow
        @test ReviseFileNow.f(0) == 1
        sleep(mtimedelay)
        pkgdata = Revise.pkgdatas[Base.PkgId(ReviseFileNow)]
        write(fn, """
            module ReviseFileNow
            f(x) = 2
            end
            """)
        try
            Revise.revise_file_now(pkgdata, "foo")
        catch err
            @test isa(err, ErrorException)
            @test occursin("not currently being tracked", err.msg)
        end
        Revise.revise_file_now(pkgdata, relpath(fn, pkgdata))
        @latestworld
        @test ReviseFileNow.f(0) == 2

        rm_precompile("ReviseFileNow")
    end

    do_test("Evaled toplevel") && @testset "Evaled toplevel" begin
        testdir = newtestdir()
        dnA = joinpath(testdir, "ToplevelA", "src"); mkpath(dnA)
        dnB = joinpath(testdir, "ToplevelB", "src"); mkpath(dnB)
        dnC = joinpath(testdir, "ToplevelC", "src"); mkpath(dnC)
        write(joinpath(dnA, "ToplevelA.jl"), """
            module ToplevelA
            @eval using ToplevelB
            g() = 2
            end""")
        write(joinpath(dnB, "ToplevelB.jl"), """
            module ToplevelB
            using ToplevelC
            end""")
        write(joinpath(dnC, "ToplevelC.jl"), """
            module ToplevelC
            export f
            f() = 1
            end""")
        sleep(mtimedelay)
        using ToplevelA
        sleep(mtimedelay)
        @test ToplevelA.ToplevelB.f() == 1
        @test ToplevelA.g() == 2
        write(joinpath(dnA, "ToplevelA.jl"), """
            module ToplevelA
            @eval using ToplevelB
            g() = 3
            end""")
        @yry()
        @test ToplevelA.ToplevelB.f() == 1
        @test ToplevelA.g() == 3

        rm_precompile("ToplevelA")
        rm_precompile("ToplevelB")
        rm_precompile("ToplevelC")
    end

    do_test("struct inner functions") && @testset "struct inner functions" begin
        # issue #599
        testdir = newtestdir()
        dn = joinpath(testdir, "StructInnerFuncs", "src"); mkpath(dn)
        write(joinpath(dn, "StructInnerFuncs.jl"), """
            module StructInnerFuncs
            mutable struct A
                x::Int

                A(x) = new(f(x))
                f(x) = x^2
            end
            g(x) = 1
            end""")
        sleep(mtimedelay)
        using StructInnerFuncs
        sleep(mtimedelay)
        @test StructInnerFuncs.A(2).x == 4
        @test StructInnerFuncs.g(3) == 1
        write(joinpath(dn, "StructInnerFuncs.jl"), """
            module StructInnerFuncs
            mutable struct A
                x::Int

                A(x) = new(f(x))
                f(x) = x^2
            end
            g(x) = 2
            end""")
        @yry()
        @test StructInnerFuncs.A(2).x == 4
        @test StructInnerFuncs.g(3) == 2

        rm_precompile("StructInnerFuncs")
    end

    do_test("Issue 606") && @testset "Issue 606" begin
        # issue #606
        testdir = newtestdir()
        dn = joinpath(testdir, "Issue606", "src"); mkpath(dn)
        write(joinpath(dn, "Issue606.jl"), """
            module Issue606
            function convert_output_relations()
                function add_default_zero!(dict::Dict{K, V})::Dict{K, V} where
                        {K <: Tuple, V}
                    if K == Tuple{} && isempty(dict)
                        dict[()] = 0.0
                    end
                    return dict
                end

                function convert_to_sorteddict(
                    relation::Union{Dict{K, Tuple{Float64}}}
                ) where K <: Tuple
                    return add_default_zero!(Dict{K, Float64}((k, v[1]) for (k, v) in relation))
                end

                function convert_to_sorteddict(relation::Dict{<:Tuple, Float64})
                    return add_default_zero!(relation)
                end

                return "HELLO"
            end
            end""")
        sleep(mtimedelay)
        using Issue606
        sleep(mtimedelay)
        @test Issue606.convert_output_relations() == "HELLO"
        write(joinpath(dn, "Issue606.jl"), """
            module Issue606
            function convert_output_relations()
                function add_default_zero!(dict::Dict{K, V})::Dict{K, V} where
                        {K <: Tuple, V}
                    if K == Tuple{} && isempty(dict)
                        dict[()] = 0.0
                    end
                    return dict
                end

                function convert_to_sorteddict(
                    relation::Union{Dict{K, Tuple{Float64}}}
                ) where K <: Tuple
                    return add_default_zero!(Dict{K, Float64}((k, v[1]) for (k, v) in relation))
                end

                function convert_to_sorteddict(relation::Dict{<:Tuple, Float64})
                    return add_default_zero!(relation)
                end

                return "HELLO2"
            end
            end""")
        @yry()
        @test Issue606.convert_output_relations() == "HELLO2"

        rm_precompile("Issue606")
    end

    do_test("Revision errors") && @testset "Revision errors" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "RevisionErrors", "src")
        mkpath(dn)
        fn = joinpath(dn, "RevisionErrors.jl")
        write(fn, """
            module RevisionErrors
            f(x) = 1
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using RevisionErrors
        sleep(mtimedelay)
        @test RevisionErrors.f(0) == 1
        write(fn, """
            module RevisionErrors
            f{x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 1
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end

        function check_revision_error(rec, ErrorType, msg, line)
            @test rec.message == "Failed to revise $fn"
            exc, bt = rec.kwargs[:exception]
            if exc isa Revise.ReviseEvalException
                @test bt === nothing
                st = exc.stacktrace
                io = IOBuffer()
                let exc=exc
                    with_logger(ConsoleLogger(io)) do
                        @error rec.message exception=(exc, nothing)
                    end
                end
                rendered = String(take!(io))
                @test occursin("Stacktrace:", rendered)
                frame, _ = only(st)
                @test occursin("RevisionErrors.jl:$(frame.line)", rendered)
                exc = exc.exc
            else
                st = stacktrace(bt)
            end
            @test exc isa ErrorType
            if ErrorType === LoadError
                @test exc.file == fn
                @test exc.line == line
                @test occursin(msg, errmsg(exc.error))
            elseif ErrorType === Base.Meta.ParseError
                @test occursin(msg, exc.msg)
            elseif ErrorType === UndefVarError
                @test msg == exc.var
            end
            @test length(st) == 1
        end

        # test errors are reported the the first time
        check_revision_error(logs[1], Base.VERSION < v"1.10" ? LoadError : Base.Meta.ParseError,
                             Base.VERSION < v"1.10" ? "missing comma or }" : "Expected `}`", 2 + (Base.VERSION >= v"1.10"))
        # Check that there's an informative warning
        rec = logs[2]
        @test startswith(rec.message, "The running code does not match")
        @test occursin("RevisionErrors.jl", rec.message)

        # test errors are not re-reported
        logs, _ = Test.collect_test_logs() do
            yry(expect_revision=false)   # nothing new to revise
        end
        @test isempty(logs)

        # test error re-reporting
        logs,_ = Test.collect_test_logs() do
            Revise.errors()
        end
        check_revision_error(logs[1], Base.VERSION < v"1.10" ? LoadError : Base.Meta.ParseError,
                             Base.VERSION < v"1.10" ? "missing comma or }" : "Expected `}`", 2 + (Base.VERSION >= v"1.10"))

        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 1
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        @latestworld
        @test isempty(logs)
        @test RevisionErrors.f(0) == 2

        # issue #421
        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            function g(x) = 1
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        delim = Base.VERSION < v"1.10" ? '"' : '`'
        check_revision_error(logs[1], Base.VERSION < v"1.10" ? LoadError : Base.Meta.ParseError,
                             "unexpected $delim=$delim", 6 + (Base.VERSION >= v"1.10")*2)

        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 1
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        @test isempty(logs)

        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 1
            foo(::Vector{T}) = 3
            end
            """)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        check_revision_error(logs[1], UndefVarError, :T, 6)

        # issue #541
        sleep(mtimedelay)
        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x} = 2
            end
            """)
        sleep(mtimedelay)
        @test try
            revise(throw=true)
            false
        catch err
            if Base.VERSION < v"1.10"
                isa(err, LoadError) && occursin("""unexpected "}" """, errmsg(err.error))
            else
                isa(err, Base.Meta.ParseError) && occursin("Expected `)`", err.msg)
            end
        end
        sleep(mtimedelay)
        write(joinpath(dn, "RevisionErrors.jl"), """
            module RevisionErrors
            f(x) = 2
            struct Vec{N, T <: Union{Float32,Float64}}
                data::NTuple{N, T}
            end
            g(x) = 2
            end
            """)
        @yry()
        @test RevisionErrors.g(0) == 2

        rm_precompile("RevisionErrors")
        empty!(Revise.queue_errors)

        testfile = joinpath(testdir, "Test301.jl")
        write(testfile, """
            module Test301
            mutable struct Struct301
                x::Int
                unset

                Struct301(x::Integer) = new(x)
            end
            f(s) = s.unset
            const s = Struct301(1)
            if f(s)
                g() = 1
            else
                g() = 2
            end
            end
            """)
        logfile = joinpath(tempdir(), randtmp()*".log")
        open(logfile, "w") do io
            redirect_stderr(io) do
                includet(testfile)
            end
        end
        sleep(mtimedelay)
        lines = readlines(logfile)
        @test lines[1] == "ERROR: UndefRefError: access to undefined reference"
        @test any(str -> occursin(r"f\(.*Test301\.Struct301\)", str), lines)
        @test any(str -> endswith(str, "Test301.jl:10"), lines)

        logfile = joinpath(tempdir(), randtmp()*".log")
        open(logfile, "w") do io
            redirect_stderr(io) do
                includet("callee_error.jl")
            end
        end
        sleep(mtimedelay)
        lines = readlines(logfile)
        @test lines[1] == "ERROR: BoundsError: attempt to access 3-element $(Vector{Int}) at index [4]"
        @test any(str -> endswith(str, "callee_error.jl:12"), lines)
        @test_throws UndefVarError CalleeError.foo(0.1f0)

        # Issue #877 (lowering errors)
        file = joinpath(testdir, "goodbadfile.jl")
        write(file, """
            function f877()
                for i in 1:10
                    # 1=2   #uncomment to trigger error
                end
            end
            """)
        includet(file)
        @test !isempty(methods(f877))
        sleep(mtimedelay)
        write(file, """
            function f877()
                for i in 1:10
                    1=2   #uncomment to trigger error
                end
            end
            """)
        sleep(mtimedelay)
        logs, _ = Test.collect_test_logs() do
            yry()
        end
        @test isempty(methods(f877))
        sleep(mtimedelay)
        write(file, """
            function f877()
                for i in 1:10
                    # 1=2   #uncomment to trigger error
                end
            end
            """)
        sleep(mtimedelay)
        yry()
        @test !isempty(methods(f877))

        # Issue #954: methods in a statically-untaken branch must not have their
        # signatures resolved, because the names they reference may not exist (e.g.
        # a type behind a `VERSION`/`Sys` check). The signature walk follows control
        # flow, so the dead branch is never visited.
        function includet_quiet(file)
            logfile = joinpath(tempdir(), randtmp()*".log")
            open(logfile, "w") do io
                redirect_stderr(io) do
                    includet(file)
                end
            end
            return read(logfile, String)
        end

        # Literal `if true`: live `then`, dead `else` references an undefined name.
        file = joinpath(testdir, "deadbranch954.jl")
        write(file, """
            if true
                f954(::Int) = 1
            else
                f954(::UNDEFINED_954) = 2
            end
            """)
        log = includet_quiet(file)
        @test f954(1) == 1
        @test !any(m -> occursin("UNDEFINED_954", string(m.sig)), methods(f954))
        @test !occursin("UNDEFINED_954", log)

        # `if false`: the dead branch is now the `then` branch, live is `else`.
        file = joinpath(testdir, "deadbranch954b.jl")
        write(file, """
            if false
                g954(::UNDEFINED_954B) = 1
            else
                g954(::Int) = 2
            end
            """)
        log = includet_quiet(file)
        @test g954(1) == 2
        @test !any(m -> occursin("UNDEFINED_954B", string(m.sig)), methods(g954))
        @test !occursin("UNDEFINED_954B", log)

        # Non-literal condition: the condition is evaluated and the dead branch
        # skipped. This is the case a literal-only reachability fold cannot handle.
        file = joinpath(testdir, "deadbranch954c.jl")
        write(file, """
            if Sys.WORD_SIZE == $(Sys.WORD_SIZE)
                h954(::Int) = 1
            else
                h954(::UNDEFINED_954C) = 2
            end
            """)
        log = includet_quiet(file)
        @test h954(1) == 1
        @test !any(m -> occursin("UNDEFINED_954C", string(m.sig)), methods(h954))
        @test !occursin("UNDEFINED_954C", log)

        # Code after the `if` must still be tracked (the live branch's jump to the
        # merge point is followed).
        file = joinpath(testdir, "deadbranch954d.jl")
        write(file, """
            if true
                p954(::Int) = 1
            else
                p954(::UNDEFINED_954D) = 2
            end
            q954(::Float64) = 3
            """)
        log = includet_quiet(file)
        @test p954(1) == 1
        @test q954(1.0) == 3
        @test !occursin("UNDEFINED_954D", log)

        # A toplevel loop with no tracked body must not hang (we do not follow
        # unconditional backedges), and methods after it are still tracked.
        file = joinpath(testdir, "deadbranch954e.jl")
        write(file, """
            loop954 = Int[]
            for i in 1:3
                push!(loop954, i)
            end
            r954(::Int) = 1
            """)
        includet_quiet(file)
        @test r954(1) == 1
    end

    do_test("Retry on InterruptException") && @testset "Retry on InterruptException" begin
        function check_revision_interrupt(logs)
            rec = logs[1]
            @test rec.message == "Failed to revise $fn"
            exc = rec.kwargs[:exception]
            if exc isa Revise.ReviseEvalException
                exc = exc.exc
            else
                exc, _ = exc
            end
            @test exc isa InterruptException
            if length(logs) > 1
                rec = logs[2]
                @test startswith(rec.message, "The running code does not match")
            end
        end

        testdir = newtestdir()
        dn = joinpath(testdir, "RevisionInterrupt", "src")
        mkpath(dn)
        fn = joinpath(dn, "RevisionInterrupt.jl")
        write(fn, """
            module RevisionInterrupt
            f(x) = 1
            end
            """)
        sleep(mtimedelay)
        @eval using RevisionInterrupt
        sleep(mtimedelay)
        @test RevisionInterrupt.f(0) == 1

        # Interpreted & compiled mode
        n = 1
        for errthrow in ("throw(InterruptException())", """
                         eval(quote  # this forces interpreted mode
                             throw(InterruptException())
                         end)""")
            n += 1
            write(fn, """
                module RevisionInterrupt
                $errthrow
                f(x) = $n
                end
                """)
            logs, _ = Test.collect_test_logs() do
                yry()
            end
            check_revision_interrupt(logs)
            # This method gets deleted because it's redefined to f(x) = 2,
            # but the error prevents it from getting that far.
            # @test RevisionInterrupt.f(0) == 1
            # Check that InterruptException triggers a retry (issue #418)
            logs, _ = Test.collect_test_logs() do
                yry()
            end
            check_revision_interrupt(logs)
            # @test RevisionInterrupt.f(0) == 1
            write(fn, """
                module RevisionInterrupt
                f(x) = $n
                end
                """)
            logs, _ = Test.collect_test_logs() do
                yry()
            end
            @latestworld
            @test isempty(logs)
            @test RevisionInterrupt.f(0) == n
        end
    end

    do_test("Modify @enum") && @testset "Modify @enum" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "ModifyEnum", "src")
        mkpath(dn)
        write(joinpath(dn, "ModifyEnum.jl"), """
            module ModifyEnum
            @enum Fruit apple=1 orange=2
            end
            """)
        sleep(mtimedelay)
        @eval using ModifyEnum
        sleep(mtimedelay)
        @test Int(ModifyEnum.apple) == 1
        @test ModifyEnum.apple isa ModifyEnum.Fruit
        @test_throws UndefVarError Int(ModifyEnum.kiwi)
        write(joinpath(dn, "ModifyEnum.jl"), """
            module ModifyEnum
            @enum Fruit apple=1 orange=2 kiwi=3
            end
            """)
        @yry()
        @test Int(ModifyEnum.kiwi) == 3
        @test Base.instances(ModifyEnum.Fruit) === (ModifyEnum.apple, ModifyEnum.orange, ModifyEnum.kiwi)
        rm_precompile("ModifyEnum")
        pop!(LOAD_PATH)
    end

    do_test("revise_structs preference") && if Base.VERSION >= v"1.12.0-DEV.2047"
        @testset "revise_structs preference" begin
            # The preference is read in __init__, so we have to test it via subprocesses.
            # Set the value explicitly in the active project's preferences file: a
            # preference there takes precedence over one inherited from the default
            # environment, so the test reflects the `revise_structs` plumbing rather than
            # whatever the host machine happens to set globally. NOTE: when the tests run
            # in a `Pkg.test` sandbox (e.g. testing Revise as a dependency of another
            # project), Pkg materializes the ambient preferences as
            # `JuliaLocalPreferences.toml`, which shadows any `LocalPreferences.toml` in
            # the same directory — so write to the file that actually has priority.
            test_proj_dir = dirname(Base.active_project())
            prefs_file = let candidates = joinpath.(test_proj_dir, Base.preferences_names)
                i = findfirst(isfile, candidates)
                i === nothing ? joinpath(test_proj_dir, last(Base.preferences_names)) : candidates[i]
            end
            backup = isfile(prefs_file) ? read(prefs_file, String) : nothing
            julia = Base.julia_cmd()
            check_bpart = "using Revise; print(Revise.__bpart__[])"
            try
                # With revise_structs = false, __bpart__ should be false
                write(prefs_file, "[Revise]\nrevise_structs = false\n")
                @test read(`$julia --project=$test_proj_dir -e $check_bpart`, String) == "false"
                # With revise_structs = true, __bpart__ should be true
                write(prefs_file, "[Revise]\nrevise_structs = true\n")
                @test read(`$julia --project=$test_proj_dir -e $check_bpart`, String) == "true"
            finally
                if backup === nothing
                    rm(prefs_file; force=true)
                else
                    write(prefs_file, backup)
                end
            end
        end
    end

    # Struct revision tests require __bpart__[] = true. Enable it based on the Julia version
    # alone: the LocalPreferences setting (which defaults to false for end users) should not
    # suppress these tests on CI.
    if Base.VERSION >= v"1.12.0-DEV.2047"
        Revise.__bpart__[] = true
    end

    Revise.__bpart__[] && do_test("Type info tracking") && @testset "Type info tracking" begin
        let exinfo = lower_and_track(:(abstract type ABC end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ABC)).name
        end

        let exinfo = lower_and_track(:(abstract type ABCNumber <: Number end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ABCNumber)).name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_Int
                x::Int
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Int)).name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_Int_String
                x::Int
                y::String
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Int_String)).name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_Vector_Int
                x::Vector{Int}
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Vector_Int)).name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_T_Integer{T<:Integer}
                x::T
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_T_Integer)).body.name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_Vector_AbstractString
                x::Vector{<:AbstractString}
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_Vector_AbstractString)).name
        end

        let exinfo = lower_and_track(:(struct ConcreteType_T_AbstractString_Vector_T{T<:AbstractString}
                x::Vector{<:T}
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :ConcreteType_T_AbstractString_Vector_T)).body.name
        end

        let exinfo = lower_and_track(:(struct Subtype <: ABC
                x::Int
            end))
            typeinfo = only(exinfo.typeinfos)
            @test typeinfo.typname == @invokelatest(getglobal(TypeInfoTracking, :Subtype)).name
        end
    end

    Revise.__bpart__[] && do_test("visit") && @testset "visit" include("test_visit.jl")

    if Revise.__bpart__[] && do_test("struct revision (simple)")   # can we revise types and constants?
        @testset "struct revision (simple)" begin
            testdir = newtestdir()
            try
                # Example from https://github.com/timholy/Revise.jl/pull/894#issuecomment-2824111764
                dn = joinpath(testdir, "StructExample", "src")
                mkpath(dn)
                pkg_code_v1 = """
                    module StructExample
                        export hello, Hello
                        struct Hello; who::String; end
                        hello(x::Hello) = hello(x.who)
                        hello(who::String) = "Hello, \$who"
                    end
                    """
                write(joinpath(dn, "StructExample.jl"), pkg_code_v1)
                sleep(mtimedelay)
                @eval using StructExample
                sleep(mtimedelay)
                @test StructExample.hello(StructExample.Hello("World")) == "Hello, World"
                # Revision 2: rename field and update method
                pkg_code_v2 = replace(pkg_code_v1,
                    "struct Hello; who::String; end" =>
                    "struct Hello; who2::String; end")
                pkg_code_v2 = replace(pkg_code_v2, "hello(x.who)" => "hello(x.who2 * \" (changed)\")")
                write(joinpath(dn, "StructExample.jl"), pkg_code_v2)
                @yry()
                @test StructExample.hello(StructExample.Hello("World")) == "Hello, World (changed)"
            finally
                rm_precompile("StructExample")
                pop!(LOAD_PATH)
            end
        end
    end

    if Revise.__bpart__[] && do_test("type scan world-age (issue #993)")
        @testset "type scan world-age (issue #993)" begin
            # Enumerating the type tree for a struct revision must read bindings
            # at the current world. The earlier recursive `subtypes`-per-type
            # walk ran long enough that concurrent world advances left it
            # reading freshly-defined bindings at a stale world, emitting
            # "Detected access to binding ... prior to its definition world".
            # `all_named_types()` is a single current-world pass: it must
            # see types defined after Revise loaded (as runtime `eval` from
            # Requires produces) and emit no such warning.
            # Heavy reproducer: Revise#993 / JuliaLang/julia#61804.
            @eval module Late993
                abstract type LateAbs end
                struct LateConcrete{T} <: LateAbs; x::T; end
                struct LateUser; y::LateConcrete{Int}; end
                module Inner
                    struct LateNested; z::Int; end
                end
            end
            Revise.all_named_types()  # warm up before capturing stderr
            # redirect_stderr needs a real fd (the warning is a C-level write),
            # so capture through a temp file rather than an IOBuffer.
            errfile = tempname()
            allt = open(errfile, "w") do io
                redirect_stderr(io) do
                    Revise.all_named_types()
                end
            end
            errstr = read(errfile, String)
            rm(errfile; force=true)
            # Types defined after load are found at the current world
            @test Late993.LateAbs in allt
            @test Late993.LateConcrete in allt
            @test Late993.LateUser in allt
            @test Late993.Inner.LateNested in allt
            # No world-age warning emitted by the scan
            @test !occursin("definition world", errstr)
            @test !occursin("Detected access", errstr)
            # The flat pass must not lose coverage relative to a recursive
            # `subtypes` walk. Compare by TypeName over non-anonymous types (the
            # flat pass stores raw bindings, the recursion parent-intersected
            # forms, so identity differs but the TypeName set must not shrink).
            # Take the flat scan last so types created by JIT-compiling the
            # reference walk cannot make it spuriously smaller than the reference.
            function recursive_subtypes!(types, parent)
                for Ty in InteractiveUtils.subtypes(parent)
                    Ty in types && continue
                    push!(types, Ty)
                    recursive_subtypes!(types, Ty)
                end
                return types
            end
            ref = recursive_subtypes!(Base.IdSet{Type}(), Any)
            flat = Revise.all_named_types()
            realnames(s) = Set(dt.name for t in s
                               for dt in (Base.unwrap_unionall(t),)
                               if dt isa DataType && dt !== Any && !startswith(string(dt.name.name), "#"))
            @test realnames(ref) ⊆ realnames(flat)
        end
    end

    if Revise.__bpart__[] && do_test("struct revision (retry)")
        @testset "struct revision (retry)" begin
            testdir = newtestdir()
            try
                # Full-circle parametric removal and restoration
                # Start with a parametric struct and a method depending on its type parameter.
                # Switch the struct to non-parametric (with its own method), then switch back
                # to the original parametric definition and ensure calls work again.
                dn4 = joinpath(testdir, "StructParamFullCircle", "src")
                mkpath(dn4)
                fn4 = joinpath(dn4, "StructParamFullCircle.jl")
                pkg_code_v1 = """
                    module StructParamFullCircle
                        struct Foo{T}; x::T; end
                        bar(::Foo{T}) where {T} = "parametric with \$T"
                    end
                """
                write(fn4, pkg_code_v1)
                sleep(mtimedelay)
                @eval using StructParamFullCircle
                sleep(mtimedelay)
                foo1 = StructParamFullCircle.Foo(1)
                @test StructParamFullCircle.bar(foo1) == "parametric with $Int"

                # Revision 2: change Foo to be non-parametric
                # N.B. This would cause revision error when trying to redefine `bar(::Foo{T}) where {T} = "parametric with $T"`,
                #      which uses type parameter of `Foo` that no longer exists
                pkg_code_v2 = replace(pkg_code_v1,
                    "struct Foo{T}; x::T; end" =>
                    "struct Foo; x::Int; end")
                write(fn4, pkg_code_v2)
                @test_logs (:error, r"Failed to revise") (:warn, r"The running code does not match the saved version") yry()
                foo2 = @invokelatest(StructParamFullCircle.Foo(1))
                @test_throws MethodError @invokelatest(StructParamFullCircle.bar(foo2))

                # Revision 3: change Foo back to its original parametric definition
                # N.B. This makes revision of `bar` possible again, so Revise should redefine it once more
                pkg_code_v3 = replace(pkg_code_v2,
                    "struct Foo; x::Int; end" =>
                    "struct Foo{T}; x::T; end")
                write(fn4, pkg_code_v3)
                @yry()
                foo3 = @invokelatest(StructParamFullCircle.Foo(1))
                @test @invokelatest(StructParamFullCircle.bar(foo3)) == "parametric with $Int"
            finally
                rm_precompile("StructParamFullCircle")
                pop!(LOAD_PATH)
            end
        end
    end

    if Revise.__bpart__[] && do_test("struct revision (dependency)")   # can we revise types and constants?
        @testset "struct revision (dependency)" begin
            testdir = newtestdir()
            try
                dn = joinpath(testdir, "StructConst", "src")
                mkpath(dn)
                pkg_code_v1 = """
                    module StructConst
                    const __hash__ = 0x71716e828e2d6093
                    struct Fixed; x::Int; end
                    Base.hash(f::Fixed, h::UInt) = hash(__hash__, hash(f.x, h))
                    struct Point; x::Float64; end
                    # Three methods that won't need to be explicitly redefined (but will need re-evaluation for new type)
                    firstval(p::Point) = p.x
                    firstvalP(p::P) where P<:Point = p.x
                    returnsconst(::Point) = 1
                    # Method that will need to be explicitly redefined
                    mynorm(p::Point) = sqrt(p.x^2)
                    # Method that uses `Point` without it being in the signature
                    hiddenconstructor(x) = Point(ntuple(_ -> x, length(fieldnames(Point)))...)
                    # Change of field that has no parameters (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3271461024)
                    struct ChangePrimitiveType; x::Int; end
                    useprimitivetype(::ChangePrimitiveType) = 1
                    # Additional constructors (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3274102493)
                    abstract type AbstractMoreConstructors end
                    struct MoreConstructors; x::Int; end
                    MoreConstructors() = MoreConstructors(1)
                    end
                    """
                write(joinpath(dn, "StructConst.jl"), pkg_code_v1)
                # Also create another package that uses it
                dn2 = joinpath(testdir, "StructConstUser", "src")
                mkpath(dn2)
                write(joinpath(dn2, "StructConstUser.jl"), """
                    module StructConstUser
                    using StructConst: StructConst, Point
                    struct PointWrapper; p::Point; end
                    scuf(f::StructConst.Fixed) = 33 * f.x
                    scup(p::Point) = 44 * p.x
                    scup(pw::PointWrapper) = 55 * pw.p.x
                    end
                    """)
                # ...and one that uses that. This is to check whether the propagation of
                # signature extraction works correctly.
                dn3 = joinpath(testdir, "StructConstUserUser", "src")
                mkpath(dn3)
                write(joinpath(dn3, "StructConstUserUser.jl"), """
                    module StructConstUserUser
                    using StructConstUser
                    struct PointWrapperWrapper; pw::StructConstUser.PointWrapper; end
                    StructConstUser.scup(pw::PointWrapperWrapper) = 2 * StructConstUser.scup(pw.pw)
                    end
                    """)
                sleep(mtimedelay)
                @eval using StructConst
                @eval using StructConstUser
                @eval using StructConstUserUser
                sleep(mtimedelay)
                f = StructConst.Fixed(5)
                v1 = hash(f)
                p = StructConst.Point(5.0)
                hp = StructConst.hiddenconstructor(5)
                @test isa(hp, StructConst.Point) && hp.x === 5.0
                pw = StructConstUser.PointWrapper(p)
                pww = StructConstUserUser.PointWrapperWrapper(pw)
                @test StructConst.firstval(p) == StructConst.firstvalP(p) === 5.0
                @test StructConst.returnsconst(p) === 1
                @test StructConst.mynorm(p) == 5.0
                @test StructConstUser.scuf(f) == 33 * 5.0
                @test StructConstUser.scup(p) == 44 * 5.0
                @test StructConstUser.scup(pw) == 55 * 5.0
                @test StructConstUser.scup(pww) == 2 * 55 * 5.0
                spt = StructConst.ChangePrimitiveType(3)
                @test StructConst.useprimitivetype(spt) === 1
                mc = StructConst.MoreConstructors()
                @test mc.x == 1 && supertype(typeof(mc)) === Any

                # Revision 2: change const, add field to Point, change field type, add supertype
                pkg_code_v2 = replace(pkg_code_v1,
                    "const __hash__ = 0x71716e828e2d6093" =>
                    "const __hash__ = 0xddaab158621d200c")
                pkg_code_v2 = replace(pkg_code_v2,
                    "struct Point; x::Float64; end" =>
                    "struct Point; x::Float64; y::Float64; end")
                pkg_code_v2 = replace(pkg_code_v2,
                    "mynorm(p::Point) = sqrt(p.x^2)" =>
                    "mynorm(p::Point) = sqrt(p.x^2 + p.y^2)")
                pkg_code_v2 = replace(pkg_code_v2,
                    "struct ChangePrimitiveType; x::Int; end" =>
                    "struct ChangePrimitiveType; x::Float64; end")
                pkg_code_v2 = replace(pkg_code_v2,
                    "struct MoreConstructors; x::Int; end" =>
                    "struct MoreConstructors <: AbstractMoreConstructors; x::Int; end")
                write(joinpath(dn, "StructConst.jl"), pkg_code_v2)
                @yry()
                @test StructConst.__hash__ == 0xddaab158621d200c
                v2 = hash(f)
                @test v1 != v2
                # Call with old objects---ensure we deleted all the outdated methods to reduce user confusion
                @test_throws MethodError @invokelatest(StructConst.firstval(p))
                @test_throws MethodError @invokelatest(StructConst.firstvalP(p))
                @test_throws MethodError @invokelatest(StructConst.returnsconst(p))
                @test_throws MethodError @invokelatest(StructConst.mynorm(p))
                @test @invokelatest(StructConstUser.scuf(f)) == 33 * 5.0
                @test_throws MethodError @invokelatest(StructConstUser.scup(p))
                @test_throws MethodError @invokelatest(StructConstUser.scup(pw))
                @test_throws MethodError @invokelatest(StructConstUser.scup(pww))
                @test_throws MethodError StructConst.useprimitivetype(spt)
                # Call with new objects
                p2 = @invokelatest(StructConst.Point(3.0, 4.0))
                hp = @invokelatest(StructConst.hiddenconstructor(5))
                @test isa(hp, StructConst.Point) && hp.x === 5.0 && hp.y === 5.0
                pw2 = @invokelatest(StructConstUser.PointWrapper(p2))
                pww2 = @invokelatest(StructConstUserUser.PointWrapperWrapper(pw2))
                @test @invokelatest(StructConst.firstval(p2)) == @invokelatest(StructConst.firstvalP(p2)) === 3.0
                @test StructConst.returnsconst(p2) === 1
                @test @invokelatest(StructConst.mynorm(p2)) == 5.0
                @test @invokelatest(StructConstUser.scup(p2)) == 44 * 3.0
                @test @invokelatest(StructConstUser.scup(pw2)) == 55 * 3.0
                @test @invokelatest(StructConstUser.scup(pww2)) == 2 * 55 * 3.0
                spt2 = StructConst.ChangePrimitiveType(3.0)
                @test StructConst.useprimitivetype(spt2) === 1
                mc = StructConst.MoreConstructors()
                @test mc.x == 1 && supertype(typeof(mc)) === StructConst.AbstractMoreConstructors

                # Revision 3: revert const, make Point parametric with supertype, remove ChangePrimitiveType and MoreConstructors
                pkg_code_v3 = replace(pkg_code_v2,
                    "const __hash__ = 0xddaab158621d200c" =>
                    "const __hash__ = 0x71716e828e2d6093")
                pkg_code_v3 = replace(pkg_code_v3,
                    "struct Point; x::Float64; y::Float64; end" =>
                    "struct Point{T<:Real} <: AbstractVector{T}; x::T; y::T; end")
                pkg_code_v3 = replace(pkg_code_v3,
                    "# Change of field that has no parameters (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3271461024)\n" => "",
                    "struct ChangePrimitiveType; x::Float64; end\n" => "",
                    "useprimitivetype(::ChangePrimitiveType) = 1\n" => "",
                    "# Additional constructors (https://github.com/timholy/Revise.jl/pull/894#issuecomment-3274102493)\n" => "",
                    "abstract type AbstractMoreConstructors end\n" => "",
                    "struct MoreConstructors <: AbstractMoreConstructors; x::Int; end\n" => "",
                    "MoreConstructors() = MoreConstructors(1)\n" => "")
                write(joinpath(dn, "StructConst.jl"), pkg_code_v3)
                @yry()
                @test StructConst.__hash__ == 0x71716e828e2d6093
                v3 = hash(f)
                @test v1 == v3
                p3 = @invokelatest(StructConst.Point(3.0, 4.0))
                hp = @invokelatest(StructConst.hiddenconstructor(5))
                @test isa(hp, StructConst.Point) && hp.x === 5 && hp.y === 5
                pw3 = @invokelatest(StructConstUser.PointWrapper(p3))
                pww3 = @invokelatest(StructConstUserUser.PointWrapperWrapper(pw3))
                @test @invokelatest(StructConst.firstval(p3)) == @invokelatest(StructConst.firstvalP(p3)) === 3.0
                @test @invokelatest(StructConst.mynorm(p3)) == 5.0
                @test @invokelatest(StructConstUser.scup(p3)) == 44 * 3.0
                @test @invokelatest(StructConstUser.scup(pw3)) == 55 * 3.0
                @test @invokelatest(StructConstUser.scup(pww3)) == 2 * 55 * 3.0

            finally
                rm_precompile("StructConst")
                rm_precompile("StructConstUser")
                rm_precompile("StructConstUserUser")
                pop!(LOAD_PATH)
            end
        end
    end

    if Revise.__bpart__[] && do_test("struct revision (round-trip duplicates)")
        @testset "struct revision (round-trip duplicates)" begin
            # A field rename that is later reverted. Methods with keyword or optional
            # arguments taking the revised type live in a file that never changes, so
            # they are re-evaluated only through `redefine_bindings!`. That used to
            # evaluate their defining expression once per method it defines, stacking
            # shadowed duplicate definitions; the next revision then deleted only the
            # dispatchable copy, resurrecting a method whose signature referenced the
            # previous generation of the type.
            testdir = newtestdir()
            try
                dn = joinpath(testdir, "StructRoundTrip", "src")
                mkpath(dn)
                pkg_code_v1 = """
                    module StructRoundTrip
                    struct State; cache::Dict{Symbol,Int}; end
                    State() = State(Dict{Symbol,Int}())
                    getcache(s::State) = s.cache
                    include("methods.jl")
                    end
                    """
                write(joinpath(dn, "StructRoundTrip.jl"), pkg_code_v1)
                write(joinpath(dn, "methods.jl"), """
                    kwfun(s::State, x::Int; skip::Bool=false) = length(getcache(s)) + x
                    optfun(s::State, x::Int=0) = length(getcache(s)) + x
                    # Deliberately self-contained: these bodies never dispatch on `State`,
                    # so a leftover duplicate of them stays executable end-to-end. Calling
                    # one with an outdated object then silently returns a stale result
                    # instead of raising the MethodError that flags stale data.
                    kwtag(s::State; prefix::Symbol=:field) = (prefix, fieldnames(typeof(s))[1])
                    opttag(s::State, x::Int=1) = (fieldnames(typeof(s))[1], x)
                    """)
                sleep(mtimedelay)
                @eval using StructRoundTrip
                sleep(mtimedelay)
                s1 = StructRoundTrip.State()
                @test StructRoundTrip.kwfun(s1, 1) == 1
                @test StructRoundTrip.optfun(s1) == 0
                @test StructRoundTrip.kwtag(s1) == (:field, :cache)
                @test StructRoundTrip.opttag(s1) == (:cache, 1)

                # Revision 2: rename the field (methods.jl stays untouched)
                pkg_code_v2 = replace(pkg_code_v1,
                    "cache::Dict" => "cache2::Dict",
                    "s.cache" => "s.cache2")
                write(joinpath(dn, "StructRoundTrip.jl"), pkg_code_v2)
                @yry()
                s2 = @invokelatest(StructRoundTrip.State())
                @test @invokelatest(StructRoundTrip.kwfun(s2, 1)) == 1
                @test @invokelatest(StructRoundTrip.optfun(s2)) == 0
                @test @invokelatest(StructRoundTrip.kwtag(s2)) == (:field, :cache2)
                @test @invokelatest(StructRoundTrip.opttag(s2)) == (:cache2, 1)
                @test_throws MethodError @invokelatest(StructRoundTrip.kwfun(s1, 1))
                @test_throws MethodError @invokelatest(StructRoundTrip.kwfun(s1, 1; skip=true))
                @test_throws MethodError @invokelatest(StructRoundTrip.optfun(s1))
                @test_throws MethodError @invokelatest(StructRoundTrip.optfun(s1, 1))
                @test_throws MethodError @invokelatest(StructRoundTrip.kwtag(s1))
                @test_throws MethodError @invokelatest(StructRoundTrip.opttag(s1))
                @test length(methods(StructRoundTrip.kwfun)) == 1
                @test length(methods(StructRoundTrip.optfun)) == 2
                @test length(methods(StructRoundTrip.kwtag)) == 1
                @test length(methods(StructRoundTrip.opttag)) == 2

                # Revision 3: revert to the original definition
                write(joinpath(dn, "StructRoundTrip.jl"), pkg_code_v1)
                @yry()
                s3 = @invokelatest(StructRoundTrip.State())
                @test @invokelatest(StructRoundTrip.kwfun(s3, 1; skip=true)) == 1
                @test @invokelatest(StructRoundTrip.optfun(s3, 1)) == 1
                @test @invokelatest(StructRoundTrip.kwtag(s3)) == (:field, :cache)
                @test @invokelatest(StructRoundTrip.opttag(s3)) == (:cache, 1)
                # The round-1 generation must be fully retired: no duplicate
                # definition may survive to serve the outdated type. A survivor of
                # the self-contained methods would not throw here but silently run
                # stale code, returning `:cache2` for a field that no longer exists
                @test_throws MethodError @invokelatest(StructRoundTrip.kwtag(s2))
                @test_throws MethodError @invokelatest(StructRoundTrip.kwtag(s2; prefix=:x))
                @test_throws MethodError @invokelatest(StructRoundTrip.opttag(s2))
                @test_throws MethodError @invokelatest(StructRoundTrip.opttag(s2, 2))
                @test_throws MethodError @invokelatest(StructRoundTrip.kwfun(s2, 1))
                @test_throws MethodError @invokelatest(StructRoundTrip.kwfun(s2, 1; skip=true))
                @test_throws MethodError @invokelatest(StructRoundTrip.optfun(s2))
                @test_throws MethodError @invokelatest(StructRoundTrip.optfun(s2, 1))
                @test length(methods(StructRoundTrip.kwfun)) == 1
                @test length(methods(StructRoundTrip.optfun)) == 2
                @test length(methods(StructRoundTrip.kwtag)) == 1
                @test length(methods(StructRoundTrip.opttag)) == 2
            finally
                rm_precompile("StructRoundTrip")
                pop!(LOAD_PATH)
            end
        end
    end

    if Revise.__bpart__[] && do_test("struct revision (issue #1022)")
        @testset "struct revision (issue #1022)" begin
            # Editing only the default value of a `@kwdef` struct (or any other change
            # that re-creates a type with identical structure) must not trigger the
            # expensive struct-revision path: no `DeleteType` walk runs and the
            # binding is preserved. Genuine changes to fields, types, or mutability
            # still go through full revision.
            testdir = newtestdir()
            try
                rlogger = Revise.debug_logger()
                deletetype_logs() = filter(r -> r.level == Debug && r.group == "Action" &&
                                                r.message == "DeleteType", rlogger.logs)
                dn = joinpath(testdir, "Revise1022", "src")
                mkpath(dn)
                fn = joinpath(dn, "Revise1022.jl")
                pkg_code_v1 = """
                    module Revise1022
                    Base.@kwdef struct Blah
                        x = 4
                    end
                    # Dispatches on Blah but is otherwise untouched by the revisions below;
                    # it keeps working as long as the Blah binding is preserved.
                    useblah(b::Blah) = b.x + 1
                    # The `if` block keeps the abstract type and the struct in a single
                    # expression, so revision interprets (rather than `Core.eval`s) it and
                    # registers a `TypeInfo` for the abstract type too. Editing the default
                    # then exercises preservation prediction for both.
                    if true
                        abstract type AbstractWrapped end
                        Base.@kwdef struct Wrapped <: AbstractWrapped
                            w::Int = 1
                        end
                    end
                    end
                    """
                write(fn, pkg_code_v1)
                sleep(mtimedelay)
                @eval using Revise1022
                sleep(mtimedelay)
                T_v1 = Revise1022.Blah
                W_v1 = Revise1022.Wrapped
                AW_v1 = Revise1022.AbstractWrapped
                @test Revise1022.Blah().x == 4
                @test Revise1022.useblah(Revise1022.Blah()) == 5

                # Revision 1: change only default values — bindings must be preserved,
                # and no DeleteType walk may run
                empty!(rlogger.logs)
                pkg_code_v2 = replace(pkg_code_v1, "x = 4" => "x = 5", "w::Int = 1" => "w::Int = 2")
                write(fn, pkg_code_v2)
                @yry()
                @test @invokelatest(getfield(Revise1022, :Blah)) === T_v1
                @test @invokelatest(getfield(Revise1022, :Wrapped)) === W_v1
                @test @invokelatest(Revise1022.Blah()).x == 5
                @test @invokelatest(Revise1022.Wrapped()).w == 2
                @test isempty(deletetype_logs())
                @test @invokelatest(Revise1022.useblah(Revise1022.Blah())) == 6

                # Revision 2: another default-only edit. The `if` block was interpreted in
                # `:eval` mode by revision 1, so the abstract type now has a `TypeInfo` on
                # record and its preservation must also be predicted (via `_equiv_typedef`).
                empty!(rlogger.logs)
                pkg_code_v3 = replace(pkg_code_v2, "w::Int = 2" => "w::Int = 3")
                write(fn, pkg_code_v3)
                @yry()
                @test @invokelatest(getfield(Revise1022, :Wrapped)) === W_v1
                @test @invokelatest(getfield(Revise1022, :AbstractWrapped)) === AW_v1
                @test @invokelatest(Revise1022.Wrapped()).w == 3
                @test isempty(deletetype_logs())

                # Revision 3: add a field — the full struct-revision path must run
                empty!(rlogger.logs)
                pkg_code_v4 = replace(pkg_code_v3,
                    "x = 5" => "x = 5\n        y::Int = 7",
                    "useblah(b::Blah) = b.x + 1" => "useblah(b::Blah) = b.x + b.y")
                write(fn, pkg_code_v4)
                @yry()
                T_v4 = @invokelatest getfield(Revise1022, :Blah)
                @test T_v4 !== T_v1
                @test :y in fieldnames(T_v4)
                @test @invokelatest(Revise1022.useblah(Revise1022.Blah())) == 12  # 5 + 7
                @test !isempty(deletetype_logs())

                # Revision 4: change a field type — the full struct-revision path must run
                empty!(rlogger.logs)
                pkg_code_v5 = replace(pkg_code_v4, "y::Int = 7" => "y::Float64 = 7.0")
                write(fn, pkg_code_v5)
                @yry()
                T_v5 = @invokelatest getfield(Revise1022, :Blah)
                @test T_v5 !== T_v4
                @test fieldtype(T_v5, :y) === Float64
                @test !isempty(deletetype_logs())
            finally
                rm_precompile("Revise1022")
                pop!(LOAD_PATH)
            end

            # Changing a field's `const` or `@atomic` annotation changes the type even
            # though the field names and types are identical; preservation must not be
            # predicted (`Core._equiv_typedef` compares these flags, so delegating the
            # equivalence check to the runtime gets them right).
            testdir = newtestdir()
            try
                dn = joinpath(testdir, "ReviseFieldFlags", "src")
                mkpath(dn)
                fn = joinpath(dn, "ReviseFieldFlags.jl")
                pkg_code_v1 = """
                    module ReviseFieldFlags
                    mutable struct MC
                        x::Int
                    end
                    MC() = MC(0)
                    mutable struct MA
                        y::Int
                    end
                    MA() = MA(0)
                    end
                    """
                write(fn, pkg_code_v1)
                sleep(mtimedelay)
                @eval using ReviseFieldFlags
                sleep(mtimedelay)
                MC_v1, MA_v1 = ReviseFieldFlags.MC, ReviseFieldFlags.MA
                @test !isconst(MC_v1, 1)
                @test !Base.isfieldatomic(MA_v1, 1)

                pkg_code_v2 = replace(pkg_code_v1, "x::Int" => "const x::Int",
                                                   "y::Int" => "@atomic y::Int")
                write(fn, pkg_code_v2)
                @yry()
                MC_v2 = @invokelatest getfield(ReviseFieldFlags, :MC)
                @test MC_v2 !== MC_v1
                @test isconst(MC_v2, 1)
                MA_v2 = @invokelatest getfield(ReviseFieldFlags, :MA)
                @test MA_v2 !== MA_v1
                @test Base.isfieldatomic(MA_v2, 1)
            finally
                rm_precompile("ReviseFieldFlags")
                pop!(LOAD_PATH)
            end

            # Preservation predictions run before any of the queued changes are applied,
            # so a same-revision change to a binding the struct's structure depends on
            # (here, a `const` field-type alias) makes the prediction stale: evaluation
            # redefines the struct after all. The post-evaluation check must catch this
            # and run the deletion walk late, so that dependent methods (`dist`) are
            # still re-evaluated for the new type.
            testdir = newtestdir()
            try
                dn = joinpath(testdir, "ReviseStale", "src")
                mkpath(dn)
                fn = joinpath(dn, "ReviseStale.jl")
                pkg_code_v1 = """
                    module ReviseStale
                    const Coord = Float32
                    Base.@kwdef struct Pt
                        x::Coord = 0
                    end
                    dist(p::Pt) = Float64(p.x)
                    end
                    """
                write(fn, pkg_code_v1)
                sleep(mtimedelay)
                @eval using ReviseStale
                sleep(mtimedelay)
                P_v1 = ReviseStale.Pt
                @test ReviseStale.dist(ReviseStale.Pt()) === 0.0

                pkg_code_v2 = replace(pkg_code_v1, "const Coord = Float32" => "const Coord = Float64",
                                                   "x::Coord = 0" => "x::Coord = 1")
                write(fn, pkg_code_v2)
                @yry()
                P_v2 = @invokelatest getfield(ReviseStale, :Pt)
                @test P_v2 !== P_v1
                @test fieldtype(P_v2, :x) === Float64
                @test @invokelatest(ReviseStale.dist(@invokelatest(ReviseStale.Pt()))) === 1.0
            finally
                rm_precompile("ReviseStale")
                pop!(LOAD_PATH)
            end

            # Moving an unchanged struct between files deletes it from one file's exprs
            # and re-creates it from another's. Predictions span all queued files, so
            # the move is recognized as preserving and skips the deletion walk.
            testdir = newtestdir()
            try
                rlogger = Revise.debug_logger()
                dn = joinpath(testdir, "ReviseMove", "src")
                mkpath(dn)
                write(joinpath(dn, "ReviseMove.jl"), """
                    module ReviseMove
                    include("types.jl")
                    include("methods.jl")
                    end
                    """)
                write(joinpath(dn, "types.jl"), """
                    struct Movable
                        x::Int
                    end
                    """)
                write(joinpath(dn, "methods.jl"), """
                    usemovable(m::Movable) = m.x + 1
                    """)
                sleep(mtimedelay)
                @eval using ReviseMove
                sleep(mtimedelay)
                M_v1 = ReviseMove.Movable
                @test ReviseMove.usemovable(ReviseMove.Movable(1)) == 2

                empty!(rlogger.logs)
                write(joinpath(dn, "types.jl"), "\n")
                write(joinpath(dn, "methods.jl"), """
                    struct Movable
                        x::Int
                    end
                    usemovable(m::Movable) = m.x + 1
                    """)
                @yry()
                @test @invokelatest(getfield(ReviseMove, :Movable)) === M_v1
                @test @invokelatest(ReviseMove.usemovable(@invokelatest(ReviseMove.Movable(1)))) == 2
                @test isempty(filter(r -> r.level == Debug && r.group == "Action" &&
                                          r.message == "DeleteType", rlogger.logs))
            finally
                rm_precompile("ReviseMove")
                pop!(LOAD_PATH)
            end
        end
    end

    do_test("Rewritten precompile cache") && @testset "Rewritten precompile cache" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "RewrittenCache", "src")
        mkpath(dn)
        write(joinpath(dn, "RewrittenCache.jl"), """
            module RewrittenCache
            include("a.jl")
            include("b.jl")
            include("c.jl")
            end
            """)
        write(joinpath(dn, "a.jl"), "afun() = 1\n")
        write(joinpath(dn, "b.jl"), "bfun() = 1\n")
        write(joinpath(dn, "c.jl"), "cfun() = 1\n")
        sleep(mtimedelay)
        @eval using RewrittenCache
        @latestworld
        M = RewrittenCache
        id = Base.PkgId(M)
        pkgdata = Revise.pkgdatas[id]
        @test pkgdata.cachebuildid != 0
        @test Revise.cache_snapshot_is_valid(pkgdata)

        # Where a handle can be held on the cache, it keeps the snapshot readable across a
        # rebuild and the rebuild is a non-event. Drop it afterwards to exercise the
        # platforms that cannot hold one.
        held = pkgdata.cacheio
        @test (held !== nothing) == Revise.can_hold_cache()
        if held !== nothing
            sleep(mtimedelay)
            write(joinpath(dn, "a.jl"), "afun() = 2\n")
            sleep(mtimedelay)
            # Stands in for a `using` or `Pkg.precompile` in a separate process: the cache
            # path depends on the project and compile flags, not on the source, so this
            # replaces the file Revise recorded.
            Base.compilecache(id)
            @yry()
            @test Revise.cache_snapshot_is_valid(pkgdata)   # read through the held handle
            @test M.afun() == 2
            @test isempty(Revise.rewritten_caches)
            pkgdata.cacheio = nothing
        end

        # A rebuild from unchanged source carries the same snapshot: still a non-event.
        sleep(mtimedelay)
        Base.compilecache(id)
        @test !Revise.cache_snapshot_is_valid(pkgdata)      # the build id did change
        @test Revise.cached_source_is_current(pkgdata, Revise.fileinfo(pkgdata, joinpath("src", "b.jl")))
        sleep(mtimedelay)
        write(joinpath(dn, "b.jl"), "bfun() = 2\n")
        @yry()
        @test M.bfun() == 2
        @test isempty(Revise.rewritten_caches)
        @test isempty(Revise.queue_errors)

        # An edit that a rebuild sweeps into the cache leaves no baseline for that file:
        # no revision is attempted, and the loss is reported rather than guessed at.
        # (A file already revised once holds its baseline in memory and is unaffected;
        # c.jl has not been revised, so the cache is still its only baseline.)
        sleep(mtimedelay)
        write(joinpath(dn, "c.jl"), "cfun() = 2\n")
        sleep(mtimedelay)
        Base.compilecache(id)
        @test !Revise.cached_source_is_current(pkgdata, Revise.fileinfo(pkgdata, joinpath("src", "c.jl")))
        @test_logs (:warn, r"source it held for one or more edited files is gone") match_mode=:any yry()
        @latestworld
        @test M.cfun() == 1                                 # left as the session had it
        @test id in Revise.rewritten_caches
        err, _ = Revise.queue_errors[(pkgdata, joinpath("src", "c.jl"))]
        @test err isa Revise.StaleCacheError
        @test occursin("cannot be revised", sprint(showerror, err))
        # a.jl and b.jl kept their baselines, so they still revise normally
        sleep(mtimedelay)
        write(joinpath(dn, "a.jl"), "afun() = 42\n")
        @yry()
        @test M.afun() == 42
        empty!(Revise.rewritten_caches)
        empty!(Revise.queue_errors)
        rm_precompile("RewrittenCache")
        pop!(LOAD_PATH)
    end

    do_test("Queueing when the snapshot is gone") && @testset "Queueing when the snapshot is gone" begin
        # `stale_load` decides what to revise by comparing each file against the source
        # snapshot in the cache. A snapshot that is no longer the one the session loaded
        # says nothing about what needs revising, so the file is queued rather than passed
        # over for matching a cache the session never ran.
        testdir = newtestdir()
        dn = joinpath(testdir, "QueueStale", "src")
        mkpath(dn)
        write(joinpath(dn, "QueueStale.jl"), "module QueueStale\nqfun() = 1\nend\n")
        sleep(mtimedelay)
        @eval using QueueStale
        @latestworld
        id = Base.PkgId(QueueStale)
        pkgdata = Revise.pkgdatas[id]
        pkgdata.cacheio = nothing        # as on a platform that cannot hold a handle
        file = joinpath("src", "QueueStale.jl")
        try
            @test !Revise.queue_changed_files!(id)     # nothing differs from the snapshot
            sleep(mtimedelay)
            write(joinpath(dn, "QueueStale.jl"), "module QueueStale\nqfun() = 2\nend\n")
            sleep(mtimedelay)
            Base.compilecache(id)                      # sweeps the edit into the cache
            filter!(pr -> first(pr) !== pkgdata, Revise.revision_queue)
            # The source on disk now matches the cache, so comparing against it would find
            # nothing to do...
            @test read(joinpath(dn, "QueueStale.jl"), String) == Revise.read_from_cache(pkgdata, file)
            # ...but that snapshot is not the one this session loaded, so the file is queued
            @test Revise.queue_changed_files!(id)
            @test (pkgdata, file) ∈ Revise.revision_queue
        finally
            filter!(pr -> first(pr) !== pkgdata, Revise.revision_queue)
            rm_precompile("QueueStale")
            pop!(LOAD_PATH)
        end
    end

    # `Revise.hold_cache!` keeps a precompile cache's source snapshot readable by holding a
    # handle open on the file. That is sound only where replacing the file by rename both
    # succeeds while the handle is open — otherwise Revise would break the *other* process's
    # precompilation — and leaves the handle reading the original bytes.
    # `Revise.can_hold_cache()` records where those two things are known to hold; this
    # measures them. Where it says no, they are `@test_broken`: an "Unexpected Pass" in CI
    # means the platform supports the handle after all and the test in `can_hold_cache` can
    # be relaxed to admit it.
    do_test("Cache handle across a rebuild") && @testset "Cache handle across a rebuild" begin
        if VERSION >= v"1.11.0-DEV.683"   # older Julia cannot read a cache through a handle at all
            testdir = newtestdir()
            dn = joinpath(testdir, "CacheHandle", "src")
            mkpath(dn)
            write(joinpath(dn, "CacheHandle.jl"), "module CacheHandle\nchfun() = 1\nend\n")
            id = Base.identify_package("CacheHandle")
            ret = Base.compilecache(id)
            cachefile = ret isa Tuple ? ret[1] : ret
            io = open(cachefile, "r")
            local rebuilt, preserved
            try
                srcname = first(Revise.pkg_fileinfo(id, cachefile, io)[2]).filename
                readsrc() = (seekstart(io); Base.isvalid_cache_header(io);
                             Base.read_dependency_src(io, cachefile, srcname))
                before = readsrc()
                sleep(mtimedelay)
                write(joinpath(dn, "CacheHandle.jl"), "module CacheHandle\nchfun() = 2\nend\n")
                sleep(mtimedelay)
                rebuilt = try
                    Base.compilecache(id)          # the replacing rename, with our handle open
                    true
                catch
                    false
                end
                preserved = rebuilt && (try readsrc() == before catch; false end)
            finally
                close(io)
            end
            @info "cache handle across a rebuild" Sys.KERNEL rebuilt preserved Revise.can_hold_cache()
            if Revise.can_hold_cache()
                @test rebuilt
                @test preserved
            else
                @test_broken rebuilt && preserved
            end
            rm_precompile("CacheHandle")
            pop!(LOAD_PATH)
        end
    end

    # issue #738
    do_test("stale_load") && @testset "stale_load" begin
        testdir = newtestdir()

        # Methods changed, added, and deleted between cache build and load
        dn = joinpath(testdir, "StaleLoadLeaf", "src")
        mkpath(dn)
        write(joinpath(dn, "StaleLoadLeaf.jl"), """
            module StaleLoadLeaf
            f() = 1
            h(x) = x + 1
            end
            """)
        id = Base.identify_package("StaleLoadLeaf")
        ret = Base.compilecache(id)
        cachefile = ret isa Tuple ? ret[1] : ret
        sleep(mtimedelay)
        write(joinpath(dn, "StaleLoadLeaf.jl"), """
            module StaleLoadLeaf
            f() = 2
            g() = 42
            end
            """)
        caches = Set(Base.find_all_in_cache_path(id))
        m = Revise.stale_load("StaleLoadLeaf")
        @latestworld
        @test m isa Module
        @test Base.root_module(id) === m
        # The stale cache was loaded, and no new cache was built
        @test Base.samefile(Base.pkgorigins[id].cachepath, cachefile)
        @test Set(Base.find_all_in_cache_path(id)) == caches
        # ...and revision brought the loaded code up to date
        @test m.f() == 2
        @test m.g() == 42
        @test !isdefined(m, :h) || isempty(methods(m.h))
        # The package is watched, so subsequent edits revise normally
        sleep(mtimedelay)
        write(joinpath(dn, "StaleLoadLeaf.jl"), """
            module StaleLoadLeaf
            f() = 3
            g() = 42
            end
            """)
        @yry()
        @test m.f() == 3
        # `using` binds the already-loaded module; a second `stale_load` warns
        @eval using StaleLoadLeaf
        @test StaleLoadLeaf === m
        @test_logs (:warn, r"already loaded") Revise.stale_load("StaleLoadLeaf")
        rm_precompile("StaleLoadLeaf")

        @static if VERSION >= v"1.11"
            # An edited dependency is stale-loaded and revised along with the top package
            depuuid, topuuid = string(uuid4()), string(uuid4())
            for (name, uuid, extra) in (("StaleLoadDep", depuuid, ""),
                                        ("StaleLoadTop", topuuid, "\n[deps]\nStaleLoadDep = \"$depuuid\""))
                mkpath(joinpath(testdir, name, "src"))
                write(joinpath(testdir, name, "Project.toml"), """
                    name = "$name"
                    uuid = "$uuid"
                    version = "0.1.0"
                    $extra
                    """)
            end
            write(joinpath(testdir, "StaleLoadDep", "src", "StaleLoadDep.jl"), """
                module StaleLoadDep
                dval() = 1
                end
                """)
            write(joinpath(testdir, "StaleLoadTop", "src", "StaleLoadTop.jl"), """
                module StaleLoadTop
                using StaleLoadDep
                tval() = StaleLoadDep.dval() + 10
                end
                """)
            topid = Base.identify_package("StaleLoadTop")
            depid = Base.identify_package(topid, "StaleLoadDep")
            Base.compilecache(topid)   # also builds the dependency's cache
            sleep(mtimedelay)
            write(joinpath(testdir, "StaleLoadDep", "src", "StaleLoadDep.jl"), """
                module StaleLoadDep
                dval() = 2
                end
                """)
            write(joinpath(testdir, "StaleLoadTop", "src", "StaleLoadTop.jl"), """
                module StaleLoadTop
                using StaleLoadDep
                tval() = StaleLoadDep.dval() + 100
                end
                """)
            mtop = Revise.stale_load(topid)
            @latestworld
            @test mtop.tval() == 102
            @test Base.root_module_exists(depid)
            @test haskey(Revise.pkgdatas, depid)
            rm_precompile("StaleLoadTop")
            rm_precompile("StaleLoadDep")
        end

        if Revise.__bpart__[]
            # A `struct` redefined between cache build and load
            dn = joinpath(testdir, "StaleLoadStruct", "src")
            mkpath(dn)
            write(joinpath(dn, "StaleLoadStruct.jl"), """
                module StaleLoadStruct
                struct Point
                    x::Int
                end
                coord(p::Point) = (p.x,)
                end
                """)
            sid = Base.identify_package("StaleLoadStruct")
            Base.compilecache(sid)
            sleep(mtimedelay)
            write(joinpath(dn, "StaleLoadStruct.jl"), """
                module StaleLoadStruct
                struct Point
                    x::Int
                    y::Int
                end
                coord(p::Point) = (p.x, p.y)
                end
                """)
            ms = Revise.stale_load(sid)
            @latestworld
            @test fieldnames(ms.Point) == (:x, :y)
            @test ms.coord(ms.Point(3, 4)) == (3, 4)
            rm_precompile("StaleLoadStruct")
        end

        # A package whose cache is up-to-date loads and revises nothing
        dn = joinpath(testdir, "StaleLoadFresh", "src")
        mkpath(dn)
        write(joinpath(dn, "StaleLoadFresh.jl"), """
            module StaleLoadFresh
            fresh() = 1
            end
            """)
        Base.compilecache(Base.identify_package("StaleLoadFresh"))
        mf = Revise.stale_load(:StaleLoadFresh)
        @latestworld
        @test mf.fresh() == 1
        rm_precompile("StaleLoadFresh")

        # Failure modes
        @test_throws "could not identify a package" Revise.stale_load("NotAPackageAnywhereZZZ")
        dn = joinpath(testdir, "StaleLoadNoCache", "src")
        mkpath(dn)
        write(joinpath(dn, "StaleLoadNoCache.jl"), """
            module StaleLoadNoCache
            end
            """)
        @test_throws "no loadable precompile cache" Revise.stale_load("StaleLoadNoCache")
    end

    do_test("get_def") && @testset "get_def" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "GetDef", "src")
        mkpath(dn)
        write(joinpath(dn, "GetDef.jl"), """
            module GetDef

            f(x) = 1
            f(v::AbstractVector) = 2
            f(v::AbstractVector{<:Integer}) = 3

            foo(x::T, y::Integer=1; kw1="hello", kwargs...) where T<:Number = error("stop")
            bar(x) = foo(x; kw1="world")

            end
            """)
        sleep(mtimedelay)
        @eval using GetDef
        sleep(mtimedelay)
        @test GetDef.f(1.0) == 1
        @test GetDef.f([1.0]) == 2
        @test GetDef.f([1]) == 3
        m = @which GetDef.f([1])
        ex = Revise.RelocatableExpr(definition(m))
        @test ex isa Revise.RelocatableExpr
        @test isequal(ex, Revise.RelocatableExpr(:(f(v::AbstractVector{<:Integer}) = 3)))

        st = try GetDef.bar(5.0) catch _ stacktrace(catch_backtrace()) end
        linfo = st[2].linfo
        m = isa(linfo, Core.CodeInstance) ? linfo.def.def : linfo.def
        def = Revise.RelocatableExpr(definition(m))
        @test def == Revise.RelocatableExpr(:(foo(x::T, y::Integer=1; kw1="hello", kwargs...) where T<:Number = error("stop")))

        rm_precompile("GetDef")

        # This method identifies itself as originating from @irrational, defined in Base, but
        # the module of the method is listed as Base.MathConstants.
        m = @which Float32(π)
        @test definition(m) isa Expr
    end

    do_test("Pkg exclusion") && @testset "Pkg exclusion" begin
        # `dont_watch`/`silence` mutate global state and persist to LocalPreferences.toml;
        # snapshot both so the testset leaves nothing behind.
        prefs_file = joinpath(dirname(Base.active_project()), "LocalPreferences.toml")
        prefs_backup = isfile(prefs_file) ? read(prefs_file, String) : nothing
        dont_watch_backup = copy(Revise.dont_watch_pkgs)
        silence_backup = copy(Revise.silence_pkgs)
        try
            Revise.dont_watch(:Example)
            Revise.silence(:Example)
            @eval import Example
            id = Base.PkgId(Example)
            @test !haskey(Revise.pkgdatas, id)
            # Ensure that dont_watch/allow_watch works
            Revise.dont_watch(:GSL)
            @test :GSL in Revise.dont_watch_pkgs
            Revise.allow_watch(:GSL)
            @test !(:GSL in Revise.dont_watch_pkgs)
            # Ensure that silencing works
            Revise.silence(:GSL)
            @test "GSL" in Revise.silence_pkgs
            Revise.unsilence(:GSL)
            @test !("GSL" in Revise.silence_pkgs)
        finally
            empty!(Revise.dont_watch_pkgs); union!(Revise.dont_watch_pkgs, dont_watch_backup)
            empty!(Revise.silence_pkgs); union!(Revise.silence_pkgs, silence_backup)
            if prefs_backup === nothing
                rm(prefs_file; force=true)
            else
                write(prefs_file, prefs_backup)
            end
        end
        pop!(LOAD_PATH)
    end

    do_test("Manual track") && @testset "Manual track" begin
        srcfile = joinpath(tempdir(), randtmp()*".jl")
        write(srcfile, "revise_f(x) = 1")
        sleep(mtimedelay)
        # issue #783: `includet` returns the value of the last evaluated expression
        ret = includet(srcfile)
        sleep(mtimedelay)
        @latestworld
        @test ret === revise_f
        @test revise_f(10) == 1
        @test length(signatures_at(srcfile, 1)) == 1
        write(srcfile, "revise_f(x) = 2")
        @yry()
        @test revise_f(10) == 2
        push!(to_remove, srcfile)

        # Do it again with a relative path
        curdir = pwd()
        cd(tempdir())
        srcfile = randtmp()*".jl"
        write(srcfile, "revise_floc(x) = 1")
        sleep(mtimedelay)
        include(joinpath(pwd(), srcfile))
        @test revise_floc(10) == 1
        Revise.track(srcfile)
        sleep(mtimedelay)
        write(srcfile, "revise_floc(x) = 2")
        @yry()
        @test revise_floc(10) == 2
        # Call track again & make sure it doesn't track twice
        Revise.track(srcfile)
        id = Base.PkgId(Main)
        pkgdata = Revise.pkgdatas[id]
        @test count(isequal(srcfile), pkgdata.info.files) == 1
        push!(to_remove, joinpath(tempdir(), srcfile))
        cd(curdir)

        # Empty files (issue #253)
        srcfile = joinpath(tempdir(), randtmp()*".jl")
        write(srcfile, "\n")
        sleep(mtimedelay)
        # issue #783: an empty file has no last expression, so `includet` returns `nothing`
        @test includet(srcfile) === nothing
        sleep(mtimedelay)
        @test basename(srcfile) ∈ Revise.watched_files[dirname(srcfile)]
        push!(to_remove, srcfile)

        # Double-execution (issue #263)
        srcfile = joinpath(tempdir(), randtmp()*".jl")
        write(srcfile, "println(\"executed\")")
        sleep(mtimedelay)
        logfile = joinpath(tempdir(), randtmp()*".log")
        open(logfile, "w") do io
            redirect_stdout(io) do
                includet(srcfile)
            end
        end
        sleep(mtimedelay)
        lines = readlines(logfile)
        @test length(lines) == 1 && chomp(lines[1]) == "executed"
        # In older versions of Revise, it would do the work again when the file
        # changed. Starting with 3.0, Revise modifies methods and docstrings but
        # does not "do work."
        write(srcfile, "println(\"executed again\")")
        open(logfile, "w") do io
            redirect_stdout(io) do
                yry()
            end
        end
        lines = readlines(logfile)
        @test isempty(lines)

        # tls path (issue #264)
        srcdir = joinpath(tempdir(), randtmp())
        mkpath(srcdir)
        push!(to_remove, srcdir)
        srcfile1 = joinpath(srcdir, randtmp()*".jl")
        srcfile2 = joinpath(srcdir, randtmp()*".jl")
        write(srcfile1, "includet(\"$(basename(srcfile2))\")")
        write(srcfile2, "f264() = 1")
        sleep(mtimedelay)
        include(srcfile1)
        sleep(mtimedelay)
        @latestworld
        @test f264() == 1
        write(srcfile2, "f264() = 2")
        @yry()
        @test f264() == 2

        # recursive `includet`s (issue #302)
        testdir = newtestdir()
        srcfile1 = joinpath(testdir, "Test302.jl")
        write(srcfile1, """
            module Test302
            struct Parameters{T}
                control::T
            end
            function Parameters(control = nothing; kw...)
                Parameters(control)
            end
            function (p::Parameters)(; kw...)
                p
            end
            end
            """)
        srcfile2 = joinpath(testdir, "test2.jl")
        write(srcfile2, """
            includet(joinpath(@__DIR__, "Test302.jl"))
            using .Test302
            """)
        sleep(mtimedelay)
        includet(srcfile2)
        sleep(mtimedelay)
        @latestworld
        p = Test302.Parameters{Int}(3)
        @test p() == p
        write(srcfile1, """
            module Test302
            struct Parameters{T}
                control::T
            end
            function Parameters(control = nothing; kw...)
                Parameters(control)
            end
            function (p::Parameters)(; kw...)
                0
            end
            end
            """)
        @yry()
        @test p() == 0

        # Double-execution prevention (issue #639)
        empty!(issue639report)
        srcfile1 = joinpath(testdir, "file1.jl")
        srcfile2 = joinpath(testdir, "file2.jl")
        write(srcfile1, """
            include(joinpath(@__DIR__, "file2.jl"))
            push!($(@__MODULE__).issue639report, '1')
            """)
        write(srcfile2, "push!($(@__MODULE__).issue639report, '2')")
        sleep(mtimedelay)
        includet(srcfile1)
        @test issue639report == ['2', '1']

        # Non-included dependency (issue #316)
        testdir = newtestdir()
        dn = joinpath(testdir, "LikePlots", "src"); mkpath(dn)
        write(joinpath(dn, "LikePlots.jl"), """
            module LikePlots
            plot() = 0
            backend() = include(joinpath(@__DIR__, "backends/backend.jl"))
            end
            """)
        sd = joinpath(dn, "backends"); mkpath(sd)
        write(joinpath(sd, "backend.jl"), "f() = 1")
        sleep(mtimedelay)
        @eval using LikePlots
        @test LikePlots.plot() == 0
        @test_throws UndefVarError LikePlots.f()
        sleep(mtimedelay)
        Revise.track(LikePlots, joinpath(sd, "backend.jl"))
        LikePlots.backend()
        @latestworld
        @test LikePlots.f() == 1
        sleep(2*mtimedelay)
        write(joinpath(sd, "backend.jl"), "f() = 2")
        @yry()
        @test LikePlots.f() == 2
        pkgdata = Revise.pkgdatas[Base.PkgId(LikePlots)]
        @test joinpath("src", "backends", "backend.jl") ∈ Revise.srcfiles(pkgdata)
        # No duplications from Revise.track with either relative or absolute paths
        Revise.track(LikePlots, joinpath(sd, "backend.jl"))
        @test length(Revise.srcfiles(pkgdata)) == 2
        cd(dn) do
            Revise.track(LikePlots, joinpath("backends", "backend.jl"))
            @test length(Revise.srcfiles(pkgdata)) == 2
        end

        rm_precompile("LikePlots")

        # Issue #475
        srcfile = joinpath(tempdir(), randtmp()*".jl")
        write(srcfile, """
            a475 = 0.8
            a475 = 0.7
            a475 = 0.8
            """)
        includet(srcfile)
        @test a475 == 0.8

    end

    do_test("Track sysimage package") && @testset "Track sysimage package" begin
        # issue #685: a package baked into a system image (e.g. with PackageCompiler.jl)
        # is already loaded at startup, so the `using` callback never fires and Revise
        # has no record of it. `Revise.track` should start watching it and apply any
        # edits made since it was precompiled. We reproduce the sysimage state by
        # loading the package normally and then dropping Revise's record of it, leaving
        # a valid `Base.pkgorigins` cache entry whose mtime predates the source edit.
        testdir = newtestdir()
        dn = joinpath(testdir, "TrackSysimg685", "src")
        mkpath(dn)
        write(joinpath(dn, "TrackSysimg685.jl"), """
            module TrackSysimg685
            f685() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using TrackSysimg685
        @latestworld
        @test TrackSysimg685.f685() == 1
        id = Base.PkgId(TrackSysimg685)
        old_pkgdata = @lock Revise.revise_lock begin
            pd = Revise.pkgdatas[id]
            delete!(Revise.pkgdatas, id)
            pd
        end
        sleep(mtimedelay)
        fn685 = joinpath(dn, "TrackSysimg685.jl")
        write(fn685, """
            module TrackSysimg685
            f685() = 2
            end
            """)
        # In file-watching mode, the watcher task still holds the dropped record and
        # queues it when the file changes; `track` then queues the replacement record
        # for the same file. Each record carries its own copy of the file's old
        # signatures, so processing both would delete `f685` twice. Push the stale
        # entry explicitly so the duplicate pair is exercised deterministically
        # rather than only when the watcher wins the race.
        @lock Revise.revise_lock push!(Revise.revision_queue,
                                       (old_pkgdata, relpath(fn685, old_pkgdata)))
        Revise.track(TrackSysimg685)
        @latestworld
        @test TrackSysimg685.f685() == 2

        # A package with no precompile cache cannot be tracked this way; fail with a
        # clear message rather than a `KeyError` (PR #688 review). Drive `_track`
        # directly with a synthetic, never-loaded package id: a module defined here
        # would resolve to `Main`, which the testsets above have already tracked.
        fakeid = Base.PkgId(Base.UUID("00000000-0000-0000-0000-000000000685"), "NotALoadedPkg685")
        @test_throws "no Revise.track recipe" Revise._track(fakeid, :NotALoadedPkg685)

        rm_precompile("TrackSysimg685")
        pop!(LOAD_PATH)
    end

    do_test("Auto-track user scripts") && @testset "Auto-track user scripts" begin
        srcfile = joinpath(tempdir(), randtmp()*".jl")
        push!(to_remove, srcfile)
        write(srcfile, "revise_g() = 1")
        sleep(mtimedelay)
        # By default user scripts are not tracked
        # issue #358: but if the user is tracking all includes...
        user_track_includes = Revise.tracking_Main_includes[]
        Revise.tracking_Main_includes[] = false
        include(srcfile)
        @yry(expect_revision=false)   # user scripts are not tracked by default
        @test revise_g() == 1
        write(srcfile, "revise_g() = 2")
        @yry(expect_revision=false)   # still untracked, so the edit is not picked up
        @test revise_g() == 1
        # Turn on tracking of user scripts
        empty!(Revise.included_files)  # don't track files already loaded (like this one)
        Revise.tracking_Main_includes[] = true
        try
            srcfile = joinpath(tempdir(), randtmp()*".jl")
            push!(to_remove, srcfile)
            write(srcfile, "revise_g() = 1")
            sleep(mtimedelay)
            include(srcfile)
            @yry(expect_revision=false)   # include just ran; no edit to revise yet
            @test revise_g() == 1
            write(srcfile, "revise_g() = 2")
            @yry()
            @test revise_g() == 2

            # issue #257
            _logs, _ = Test.collect_test_logs() do  # just to prevent noisy warning
                try include("nonexistent1.jl") catch end
                yry(expect_revision=false)   # include of a missing file revises nothing
                try include("nonexistent2.jl") catch end
                yry(expect_revision=false)
            end
        finally
            Revise.tracking_Main_includes[] = user_track_includes  # restore old behavior
        end
    end

    do_test("Distributed") && @testset "Distributed" begin
        # The d31474 test below is from
        # https://discourse.julialang.org/t/how-do-i-make-revise-jl-work-in-multiple-workers-environment/31474
        newprocs = addprocs(2)
        newproc = newprocs[end]
        Revise.init_worker.(newprocs)
        allworkers = [myid(); newprocs]
        dirname = randtmp()
        mkdir(dirname)
        @everywhere push_LOAD_PATH!(dirname) = push!(LOAD_PATH, dirname)  # Don't want to share this LOAD_PATH
        for p in allworkers
            remotecall_wait(push_LOAD_PATH!, p, dirname)
        end
        push!(to_remove, dirname)
        modname = "ReviseDistributed"
        dn = joinpath(dirname, modname, "src")
        mkpath(dn)
        s31474 = """
        function d31474()
            r = @spawnat $newproc sqrt(4)
            fetch(r)
        end
        """
        write(joinpath(dn, modname*".jl"), """
            module ReviseDistributed
            using Distributed

            f() = π
            g(::Int) = 0
            $s31474

            end
            """)
        sleep(mtimedelay)
        using ReviseDistributed
        sleep(mtimedelay)
        @everywhere using ReviseDistributed
        for p in allworkers
            @test remotecall_fetch(ReviseDistributed.f, p)    == π
            @test remotecall_fetch(ReviseDistributed.g, p, 1) == 0
        end
        @test ReviseDistributed.d31474() == 2.0
        s31474 = """
        function d31474()
            r = @spawnat $newproc sqrt(9)
            fetch(r)
        end
        """
        write(joinpath(dn, modname*".jl"), """
            module ReviseDistributed

            f() = 3.0
            $s31474

            end
            """)
        @yry()
        @test_throws MethodError ReviseDistributed.g(1)
        for p in allworkers
            @test remotecall_fetch(ReviseDistributed.f, p) == 3.0
            @test_throws RemoteException remotecall_fetch(ReviseDistributed.g, p, 1)
        end
        @test ReviseDistributed.d31474() == 3.0
        rmprocs(allworkers[2:3]...; waitfor=10)
        rm_precompile("ReviseDistributed")
        pop!(LOAD_PATH)
    end

    do_test("Distributed on worker") && @testset "Distributed on worker" begin
        # https://github.com/timholy/Revise.jl/pull/527
        favorite_proc, boring_proc = addprocs(2)

        Distributed.remotecall_eval(Main, [favorite_proc, boring_proc], :(ENV["JULIA_REVISE_WORKER_ONLY"] = "1"))

        dirname = randtmp()
        mkdir(dirname)
        push!(to_remove, dirname)

        @everywhere push_LOAD_PATH!(dirname) = push!(LOAD_PATH, dirname)  # Don't want to share this LOAD_PATH
        remotecall_wait(push_LOAD_PATH!, favorite_proc, dirname)

        modname = "ReviseDistributedOnWorker"
        dn = joinpath(dirname, modname, "src")
        mkpath(dn)

        s527_old = """
        module ReviseDistributedOnWorker

        f() = π
        g(::Int) = 0

        end
        """
        write(joinpath(dn, modname*".jl"), s527_old)

        # In the first tests, we only load Revise on our favorite process. The other (boring) process should be unaffected by the upcoming tests.
        Distributed.remotecall_eval(Main, [favorite_proc], :(using Revise))
        sleep(mtimedelay)
        Distributed.remotecall_eval(Main, [favorite_proc], :(using ReviseDistributedOnWorker))
        sleep(mtimedelay)

        @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.f())) == π
        @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.g(1))) == 0

        # we only loaded ReviseDistributedOnWorker on our favorite process
        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.f()))
        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.g(1)))

        s527_new = """
        module ReviseDistributedOnWorker

        f() = 3.0

        end
        """
        write(joinpath(dn, modname*".jl"), s527_new)
        sleep(mtimedelay)
        Distributed.remotecall_eval(Main, [favorite_proc], :(Revise.revise()))
        sleep(mtimedelay)

        @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.f())) == 3.0
        @test_throws RemoteException Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.g(1)))

        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.f()))
        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.g(1)))

        # In the second part, we'll also load Revise on the boring process, which should have no effect.
        Distributed.remotecall_eval(Main, [boring_proc], :(using Revise))

        write(joinpath(dn, modname*".jl"), s527_old)

        sleep(mtimedelay)
        @test !Distributed.remotecall_eval(Main, favorite_proc, :(Revise.revision_queue |> isempty))
        @test Distributed.remotecall_eval(Main, boring_proc, :(Revise.revision_queue |> isempty))

        Distributed.remotecall_eval(Main, [favorite_proc, boring_proc], :(Revise.revise()))
        sleep(mtimedelay)


        @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.f())) == π
        @test Distributed.remotecall_eval(Main, favorite_proc, :(ReviseDistributedOnWorker.g(1))) == 0

        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.f()))
        @test_throws RemoteException Distributed.remotecall_eval(Main, boring_proc, :(ReviseDistributedOnWorker.g(1)))

        rmprocs(favorite_proc, boring_proc; waitfor=10)
    end

    do_test("Distributed late worker") && @testset "Distributed late worker" begin
        # A worker added *after* an in-session revision loads the package fresh
        # from disk and so lacks the freshly-gensym'd closures the master now
        # produces. Serializing such a closure to it (as `@distributed` bodies do)
        # would throw `UndefVarError` on deserialization. `Revise.init_worker`
        # must replay the session's revisions onto the new worker. (issue #637)
        dirname = randtmp()
        mkdir(dirname)
        push!(to_remove, dirname)
        push!(LOAD_PATH, dirname)
        modname = "ReviseDist637"
        dn = joinpath(dirname, modname, "src")
        mkpath(dn)
        file = joinpath(dn, modname * ".jl")
        # `adder` returns an anonymous closure; revising its body re-lowers it and
        # assigns it a new gensym type name on the master.
        write(file, "module $modname\nadder() = (x -> x + 1)\nend\n")
        sleep(mtimedelay)
        @eval using ReviseDist637
        sleep(mtimedelay)
        @test ReviseDist637.adder()(5) == 6

        write(file, "module $modname\nadder() = (x -> x + 10)\nend\n")
        @yry()
        @test ReviseDist637.adder()(5) == 15

        # Add a worker only now, after the revision, then bring it up to date.
        newproc = only(addprocs(1))
        try
            Distributed.remotecall_eval(Main, [newproc], :(push!(LOAD_PATH, $dirname)))
            Distributed.remotecall_eval(Main, [newproc], :(using ReviseDist637))
            Revise.init_worker(newproc)
            # The master-created closure must deserialize on the new worker.
            cl = ReviseDist637.adder()
            @test remotecall_fetch(cl, newproc, 5) == 15
        finally
            rmprocs(newproc; waitfor=10)
        end
        rm_precompile(modname)
        pop!(LOAD_PATH)
    end

    do_test("Git") && @testset "Git" begin
        loc = Base.find_package("Revise")
        if occursin("dev", loc)
            repo, path = Revise.git_repo(loc)
            @test repo != nothing
            files = Revise.git_files(repo)
            @test "README.md" ∈ files
            src = Revise.git_source(loc, "946d588328c2eb5fe5a56a21b4395379e41092e0")
            @test startswith(src, "__precompile__")
            src = Revise.git_source(loc, "eae5e000097000472280e6183973a665c4243b94") # 2nd commit in Revise's history
            @test src == "module Revise\n\n# package code goes here\n\nend # module\n"
        else
            @warn "skipping git tests because Revise is not under development"
        end
        # Issue #135
        if !Sys.iswindows()
            randdir = randtmp()
            modname = "ModuleWithNewFile"
            push!(to_remove, randdir)
            push!(LOAD_PATH, randdir)
            randdir = joinpath(randdir, modname)
            mkpath(joinpath(randdir, "src"))
            mainjl = joinpath(randdir, "src", modname*".jl")
            LibGit2.with(LibGit2.init(randdir)) do repo
                write(mainjl, """
                    module $modname
                    end
                    """)
                LibGit2.add!(repo, joinpath("src", modname*".jl"))
                test_sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(); digits=0), 0)
                LibGit2.commit(repo, "New file test"; author=test_sig, committer=test_sig)
            end
            sleep(mtimedelay)
            @eval using $(Symbol(modname))
            sleep(mtimedelay)
            mod = @eval $(Symbol(modname))
            id = Base.PkgId(mod)
            extrajl = joinpath(randdir, "src", "extra.jl")
            write(extrajl, "println(\"extra\")")
            write(mainjl, """
                module $modname
                include("extra.jl")
                end
                """)
            sleep(mtimedelay)
            repo = LibGit2.GitRepo(randdir)
            LibGit2.add!(repo, joinpath("src", "extra.jl"))
            pkgdata = Revise.pkgdatas[id]
            logs, _ = Test.collect_test_logs() do
                Revise.track_subdir_from_git!(pkgdata, joinpath(randdir, "src"); commit="HEAD")
            end
            @yry()
            @test Revise.hasfile(pkgdata, mainjl)
            @test startswith(logs[end].message, "skipping src/extra.jl") || startswith(logs[end-1].message, "skipping src/extra.jl")
            rm_precompile("ModuleWithNewFile")
            pop!(LOAD_PATH)
        end
    end

    do_test("Recipes") && @testset "Recipes" begin
        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        # https://github.com/JunoLab/Juno.jl/issues/257#issuecomment-473856452
        meth = @which gcd(10, 20)
        signatures_at(Base.find_source_file(String(meth.file)), meth.line)  # this should track Base

        # Tracking Base
        # issue #250
        @test_throws ErrorException("use Revise.track(Base) or Revise.track(<stdlib module>)") Revise.track(joinpath(Revise.juliadir, "base", "intfuncs.jl"))

        id = Base.PkgId(Base)
        pkgdata = Revise.pkgdatas[id]
        @test any(k->endswith(k, "number.jl"), Revise.srcfiles(pkgdata))
        @test length(filter(k->endswith(k, "file.jl"), Revise.srcfiles(pkgdata))) == 1
        m = @which show([1,2,3])
        @test definition(m) isa Expr
        m = @which redirect_stdout()
        @test definition(m).head ∈ (:function, :(=))

        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        # Tracking stdlibs
        Revise.track(Unicode)
        id = Base.PkgId(Unicode)
        pkgdata = Revise.pkgdatas[id]
        @test any(k->endswith(k, "Unicode.jl"), Revise.srcfiles(pkgdata))
        m = first(methods(Unicode.isassigned))
        @test definition(m) isa Expr
        @test isfile(whereis(m)[1])

        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        # Submodule of Pkg (note that package is developed outside the
        # Julia repo, this tests new cases)
        id = Revise.get_tracked_id(Pkg.Types)
        pkgdata = Revise.pkgdatas[id]
        @test definition(first(methods(Pkg.API.add))) isa Expr

        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        # Test that we skip over files that don't end in ".jl"
        logs, _ = Test.collect_test_logs() do
            Revise.track(REPL)
        end
        @test isempty(logs)

        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        Revise.get_tracked_id(Core)   # just test that this doesn't error

        @test !isempty(methods(Core.Compiler.NativeInterpreter))

        if !haskey(ENV, "BUILDKITE") # disable on buildkite, see discussion in https://github.com/JuliaCI/julia-buildkite/pull/372#issuecomment-2262840304
            # Determine whether a git repo is available. Travis & Appveyor do not have this.
            repo, path = Revise.git_repo(Revise.juliadir)
            if repo != nothing && isfile(joinpath(path, "VERSION")) && isdir(joinpath(path, "base"))
                # Capture some Core.Compiler types/methods that have outer constructors
                # alongside inner constructors so we can verify that tracking does not
                # clobber them (https://github.com/JuliaLang/julia/issues/61308).
                @static if VERSION >= v"1.12.0-DEV"
                    pre_inf_methods = length(methods(Core.Compiler.InferenceResult))
                end
                # Tracking Core.Compiler
                Revise.track(Core.Compiler)
                id = Revise.pkgidid_for_mod(Core.Compiler)
                pkgdata = Revise.pkgdatas[id]
                @test any(k->endswith(k, "optimize.jl"), Revise.srcfiles(pkgdata))
                m = first(methods(Core.Compiler.typeinf_code))
                @test definition(m) isa Expr
                @static if VERSION >= v"1.12.0-DEV"
                    # tracking should not have redefined types and dropped methods
                    @test length(methods(Core.Compiler.InferenceResult)) == pre_inf_methods
                    @test hasmethod(Core.Compiler.InferenceResult,
                                    Tuple{Core.MethodInstance, Core.Compiler.AbstractLattice})
                    # An end-to-end check: InteractiveUtils.code_warntype invokes
                    # Core.Compiler.InferenceResult(::MethodInstance, ::AbstractLattice).
                    io = IOBuffer()
                    InteractiveUtils.code_warntype(IOContext(io, :color=>false),
                                                   sin, Tuple{Float64})
                    @test !isempty(String(take!(io)))
                end
            else
                @test_throws Revise.GitRepoException Revise.track(Core.Compiler)
                @warn "skipping Core.Compiler tests due to lack of git repo"
            end
        end

        @test !isempty(methods(Core.Compiler.NativeInterpreter))
    end

    do_test("CodeTracking #48") && @testset "CodeTracking #48" begin
        m = @which sum([1]; dims=1)
        file, line = whereis(m)
        @test endswith(file, "reducedim.jl") && line > 1
    end

    do_test("Prompt color #755") && @testset "Prompt color #755" begin
        # A revision error raised while the REPL is in a non-Julia mode (e.g.
        # shell mode) must recolor only the `julia>` prompt, and must restore it
        # to its own original color, rather than leaking the active mode's color
        # onto `julia>` (issue #755).
        green = Base.text_colors[:green]
        red = Base.text_colors[:red]
        julia_prompt = REPL.LineEdit.Prompt("julia> "; prompt_prefix=green)
        shell_prompt = REPL.LineEdit.Prompt("shell> "; prompt_prefix=red)
        term = REPL.Terminals.TTYTerminal("dumb", stdin, stdout, stderr)
        repl = REPL.LineEditREPL(term, true)
        repl.interface = REPL.LineEdit.ModalInterface(
            REPL.LineEdit.TextInterface[julia_prompt, shell_prompt])
        old_prefix = Revise.original_repl_prefix[]
        try
            Revise.original_repl_prefix[] = nothing
            Revise.set_prompt_color!(:warn, repl)
            @test julia_prompt.prompt_prefix == "\e[33m"   # yellow
            @test shell_prompt.prompt_prefix == red        # left untouched
            Revise.set_prompt_color!(:ok, repl)
            @test julia_prompt.prompt_prefix == green       # restored, not red
            @test shell_prompt.prompt_prefix == red
        finally
            Revise.original_repl_prefix[] = old_prefix
        end
    end

    do_test("Methods at REPL") && @testset "Methods at REPL" begin
        if isdefined(Base, :active_repl) && !isnothing(Base.active_repl)
            hp = Base.active_repl.interface.modes[1].hist
            # REPL history changed from `Vector{String}` to `HistoryFile` in Julia
            # 1.14. Work with its in-memory records so test entries are not persisted.
            history = if isdefined(REPL, :History) &&
                    isdefined(REPL.History, :HistoryFile) &&
                    hp.history isa REPL.History.HistoryFile
                hp.history.records
            else
                hp.history
            end
            push_hist! = if isdefined(REPL, :History) && isdefined(REPL.History, :HistEntry)
                (h, s) -> push!(h, REPL.History.HistEntry(
                    :julia, Dates.now(), s, UInt32(length(h) + 1)))
            else
                (h, s) -> push!(h, s)
            end
            fstr = "__fREPL__(x::Int16) = 0"
            histidx = length(history) + 1 - hp.start_idx
            ex = Base.parse_input_line(fstr; filename="REPL[$histidx]")
            f = Core.eval(Main, ex)
            if ex.head === :toplevel
                ex = ex.args[end]
            end
            push_hist!(history, fstr)
            m = first(methods(f))
            @test !isempty(signatures_at(String(m.file), m.line))
            @test isequal(Revise.RelocatableExpr(definition(m)), Revise.RelocatableExpr(ex))
            @test definition(String, m)[1] == fstr

            # Test that revisions work (https://github.com/timholy/CodeTracking.jl/issues/38)
            fstr = "__fREPL__(x::Int16) = 1"
            histidx = length(history) + 1 - hp.start_idx
            ex = Base.parse_input_line(fstr; filename="REPL[$histidx]")
            f = Core.eval(Main, ex)
            if ex.head === :toplevel
                ex = ex.args[end]
            end
            push_hist!(history, fstr)
            m = first(methods(f))
            @test isequal(Revise.RelocatableExpr(definition(m)), Revise.RelocatableExpr(ex))
            @test definition(String, m)[1] == fstr
            @test !isempty(signatures_at(String(m.file), m.line))

            pop!(history)
            pop!(history)
        else
            @warn "REPL tests skipped"
        end
    end

    do_test("baremodule") && @testset "baremodule" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "Baremodule", "src")
        mkpath(dn)
        write(joinpath(dn, "Baremodule.jl"), """
            baremodule Baremodule
            f() = 1
            end
            """)
        sleep(mtimedelay)
        @eval using Baremodule
        sleep(mtimedelay)
        @test Baremodule.f() == 1
        write(joinpath(dn, "Baremodule.jl"), """
            module Baremodule
            f() = 2
            end
            """)
        @yry()
        @test Baremodule.f() == 2
        rm_precompile("Baremodule")
        pop!(LOAD_PATH)
    end

    do_test("module style 2-argument includes (issue #670)") && @testset "module style 2-argument includes (issue #670)" begin
        testdir = newtestdir()
        dn = joinpath(testdir, "B670", "src")
        mkpath(dn)
        write(joinpath(dn, "A670.jl"), """
            x = 6
            y = 7
            """)
        sleep(mtimedelay)
        write(joinpath(dn, "B670.jl"), """
            module B670
                x = 5
            end
            """)
        sleep(mtimedelay)
        write(joinpath(dn, "C670.jl"), """
            using B670
            Base.include(B670, "A670.jl")
            """)
        sleep(mtimedelay)
        @eval using B670
        path = joinpath(dn, "C670.jl")
        @eval include($path)
        @test B670.x == 6
        @test B670.y == 7
        rm_precompile("B670")
    end

    do_test("External method tables") && @testset "External method tables" begin
        function retval(m)
            src = Base.uncompressed_ast(m)
            i = findfirst(!isnothing, src.code)
            node = src.code[i]::Core.ReturnNode
            node.val
        end

        testdir = newtestdir()

        unique_name(base) = Symbol(replace(lstrip(String(gensym(base)), '#'), '#' => '_'))
        first_revision(name) = """
            module $name

            Base.Experimental.@MethodTable(method_table)
            Base.Experimental.@MethodTable(method_table_2)
            get_method_table() = method_table

            macro override(ex) esc(:(Base.Experimental.@overlay \$method_table \$ex)) end
            macro override_2(ex) esc(:(Base.Experimental.@overlay $name.method_table \$ex)) end
            macro override_3(ex) esc(:(Base.Experimental.@overlay get_method_table() \$ex)) end

            foo() = 1

            @override print(x) = "print"
            @override show(x) = "show"
            @override cos(x) = "cos"
            @override_2 sin(x) = "sin"
            @override_3 sincos(x) = "sincos"
            Base.Experimental.@overlay method_table_2 foo() = 2

            bar() = foo()
            baz() = bar()

            end
            """
        second_revision(name) = """
            module $name

            Base.Experimental.@MethodTable(method_table)
            Base.Experimental.@MethodTable(method_table_2)
            get_method_table() = method_table

            macro override(ex) esc(:(Base.Experimental.@overlay \$method_table \$ex)) end
            macro override_2(ex) esc(:(Base.Experimental.@overlay $name.method_table \$ex)) end
            macro override_3(ex) esc(:(Base.Experimental.@overlay get_method_table() \$ex)) end

            foo() = 1

            @override print(x) = "print"
            # @override show(x) = "show"
            @override cos(x) = "sin"
            @override_2 sin(x) = "cos"
            @override_3 sincos(x) = "cossin"
            Base.Experimental.@overlay method_table_2 foo() = 3

            bar() = foo() + 1
            # baz() = bar()

            end
            """

        function test_first_revision(mod::Module)
            @test mod.foo() == 1
            @test mod.bar() == 1
            @test length(methods(mod.baz)) == 1
            (; ms) = Base.MethodList(mod.method_table)
            @test length(ms) == 5 # cos/sin/sincos/print/show
            (; ms) = Base.MethodList(mod.method_table_2)
            @test length(ms) == 1 # foo
            @test retval(first(ms)) == 2
        end

        function test_second_revision(mod::Module)
            current_world_age = isdefined(Base, :tls_world_age) ? Base.tls_world_age() : Base.get_world_counter()
            @test mod.foo() == 1
            @test mod.bar() == 2
            @test isempty(methods(mod.baz))
            (; ms) = Base.MethodList(mod.method_table)
            @test length(ms) == 8 # cos/sin/sincos x2 + print/show
            VERSION < v"1.12-" && @test count(x -> x.deleted_world < current_world_age, ms) == 4 # deleted cos/sin/sincos/show
            (; ms) = Base.MethodList(mod.method_table_2)
            @test length(ms) == 2 # foo x2
            VERSION < v"1.12-" && @test count(x -> x.deleted_world < current_world_age, ms) == 1 # deleted foo
            @test retval(first(ms)) == 3
        end

        name = unique_name(:ExternalMT)
        dn = joinpath(testdir, "$name", "src")
        mkpath(dn)
        write(joinpath(dn, "$name.jl"), first_revision(name))
        sleep(mtimedelay)
        @eval import $name
        sleep(mtimedelay)
        test_first_revision(@eval $name)
        write(joinpath(dn, "$name.jl"), second_revision(name))
        @yry()
        test_second_revision(@eval $name)

        file = tempname() * ".jl"
        name = unique_name(:ExternalMT_includet)
        write(file, first_revision(name))
        sleep(mtimedelay)
        Revise.track(@__MODULE__(), file; mode=:includet)
        sleep(mtimedelay)
        @eval import .$name
        sleep(mtimedelay)
        test_first_revision(@eval $name)
        write(file, second_revision(name))
        @yry()
        test_second_revision(@eval $name)
    end
end

do_test("Utilities") && @testset "Utilities" begin
    # Used by Rebugger but still lives here
    io = IOBuffer()
    Revise.println_maxsize(io, "a"^100; maxchars=50)
    str = String(take!(io))
    @test startswith(str, "a"^25)
    @test endswith(chomp(chomp(str)), "a"^24)
    @test occursin("…", str)
end

do_test("Switching free/dev") && @testset "Switching free/dev" begin
    function make_a2d(path, val, mode="r"; generate=true, uuid=nothing)
        # Create a new "read-only package" (which mimics how Pkg works when you `add` a package)
        # use generate=false and copy the Project.toml to use the same UUID
        cd(path) do
            pkgpath = normpath(joinpath(path, "A2D"))
            srcpath = joinpath(pkgpath, "src")
            if generate
                Pkg.generate("A2D")
                projpath = joinpath(pkgpath, "Project.toml")
                open(projpath, "a") do io
                    write(io, """

                    [weakdeps]
                    Dummy = "$uuid"

                    [extensions]
                    A2DDummyExt = "Dummy"
                    """)
                end
            else
                mkpath(srcpath)
            end
            filepath = joinpath(srcpath, "A2D.jl")
            write(filepath, """
                module A2D
                f() = $val
                function g end
                end
                """)
            chmod(filepath, mode=="r" ? 0o100444 : 0o100644)
            mkdir(joinpath(pkgpath, "ext"))
            extpath = joinpath(pkgpath, "ext", "A2DDummyExt.jl")
            write(extpath, """
                module A2DDummyExt
                using A2D
                using Dummy
                A2D.g() = $val
                end
                """)
            chmod(extpath, mode=="r" ? 0o100444 : 0o100644)
            return pkgpath
        end
    end
    # Create a new package depot
    depot = mktempdir()
    old_depots = copy(DEPOT_PATH)
    empty!(DEPOT_PATH)
    push!(DEPOT_PATH, depot)
    # Ensure we start fresh with no dependencies
    old_project = Base.ACTIVE_PROJECT[]
    Base.ACTIVE_PROJECT[] = joinpath(depot, "environments", "v$(VERSION.major).$(VERSION.minor)", "Project.toml")
    mkpath(dirname(Base.ACTIVE_PROJECT[]))
    write(Base.ACTIVE_PROJECT[], "[deps]")
    cd(depot) do
        Pkg.generate("Dummy")
    end
    dummypath = joinpath(depot, "Dummy")
    Pkg.develop(PackageSpec(path=dummypath))
    dummyuuid = match(r"uuid = \"([0-9a-f].*)\"", read(joinpath(dummypath, "Project.toml"), String)).captures[1]
    ropkgpath = make_a2d(depot, 1; uuid=dummyuuid)
    Pkg.develop(PackageSpec(path=ropkgpath))
    sleep(mtimedelay)
    @eval using A2D
    sleep(mtimedelay)
    @test Base.invokelatest(A2D.f) == 1
    @test_throws MethodError Base.invokelatest(A2D.g)
    @eval using Dummy
    @test Base.invokelatest(A2D.g) == 1
    for dir in keys(Revise.watched_files)
        @test !startswith(dir, ropkgpath)
    end
    devpath = joinpath(depot, "dev")
    mkpath(devpath)
    mfile = Revise.manifest_file()
    schedule(Task(Revise.TaskThunk(Revise.watch_manifest, (mfile,))))
    sleep(mtimedelay)
    pkgdevpath = make_a2d(devpath, 2, "w"; generate=false)
    cp(joinpath(ropkgpath, "Project.toml"), joinpath(devpath, "A2D/Project.toml"))
    Pkg.develop(PackageSpec(path=pkgdevpath))
    @yry()
    @test Base.invokelatest(A2D.f) == 2
    @test Base.invokelatest(A2D.g) == 2
    Pkg.develop(PackageSpec(path=ropkgpath))
    @yry()
    @test Base.invokelatest(A2D.f) == 1
    @test Base.invokelatest(A2D.g) == 1
    for dir in keys(Revise.watched_files)
        @test !startswith(dir, ropkgpath)
    end

    # Restore internal Pkg data
    empty!(DEPOT_PATH)
    append!(DEPOT_PATH, old_depots)
    Base.ACTIVE_PROJECT[] = old_project

    push!(to_remove, depot)
end

do_test("pkgversion update") && @testset "pkgversion update" begin
    # issue #684: Julia caches a package's version in `Base.pkgorigins` at load
    # time (and on 1.12+ `pkgversion` returns that cached value). When developing
    # a package and bumping its version, revising the source should refresh the
    # cached version so `pkgversion` doesn't go stale.
    testdir = newtestdir()
    pkgdir = joinpath(testdir, "VersionPkg")
    dn = joinpath(pkgdir, "src")
    mkpath(dn)
    projfile = joinpath(pkgdir, "Project.toml")
    write(projfile, """
        name = "VersionPkg"
        uuid = "00000000-1111-2222-3333-000000000684"
        version = "0.1.0"
        """)
    write(joinpath(dn, "VersionPkg.jl"), """
        module VersionPkg
        f() = 1
        end
        """)
    sleep(mtimedelay)
    @eval using VersionPkg
    @latestworld
    @test Base.invokelatest(VersionPkg.f) == 1
    @test pkgversion(VersionPkg) == v"0.1.0"

    sleep(mtimedelay)
    # A normal dev cycle: bump the version and change the code together.
    write(projfile, """
        name = "VersionPkg"
        uuid = "00000000-1111-2222-3333-000000000684"
        version = "0.2.0"
        """)
    write(joinpath(dn, "VersionPkg.jl"), """
        module VersionPkg
        f() = 2
        end
        """)
    @yry()
    @test Base.invokelatest(VersionPkg.f) == 2
    @test pkgversion(VersionPkg) == v"0.2.0"
    @test Base.pkgorigins[Base.PkgId(VersionPkg)].version == v"0.2.0"

    rm_precompile("VersionPkg")
    pop!(LOAD_PATH)
end

do_test("Manifest re-extraction errors") && @testset "Manifest re-extraction errors" begin
    # When a package's directory changes (e.g. a version bump), `switch_basepath`
    # eagerly re-extracts signatures. For some macro-generated top-level code this can
    # fail on re-lowering (issue #706, JLLWrappers' gensym'd closures). Such a failure
    # must be recorded rather than abort the manifest watcher.
    mod = Module(:Issue706)
    Core.eval(mod, :(using Base))
    # A method whose *signature* needs a throwing call, so extraction throws in `:sigs`.
    badex = :(foo(::Val{error("issue 706 sigs failure")}) = 1)
    mkfi() = begin
        mei = Revise.ModuleExprsInfos(mod)
        mei[mod][Revise.RelocatableExpr(badex)] = nothing
        Revise.FileInfo(mei)
    end
    id = Base.PkgId(Base.UUID("00000000-0000-0000-0000-000000000706"), "Issue706")
    pkgdata = Revise.PkgData(id, "/nonexistent/Issue706")
    file = "src/Issue706.jl"
    delete!(Revise.queue_errors, (pkgdata, file))
    # Unguarded extraction identifies partial-evaluation failure separately from
    # an error thrown by normally-evaluated package code.
    err = try
        Revise.maybe_extract_sigs!(mkfi())
        nothing
    catch err
        err
    end
    @test err isa Revise.SignatureExtractionError
    @test err.mod === mod
    @test err.exc isa Revise.ReviseEvalException
    errmsg = sprint(showerror, err)
    @test startswith(errmsg, "failed to extract method signatures")
    @test !occursin("Revise evaluation error", errmsg)

    # Debugging and non-`:sigs` modes preserve their original exception behavior.
    @test_throws ErrorException Revise.instantiate_sigs!(mkfi().mod_exs_infos; always_rethrow=true)
    @test_throws Revise.ReviseEvalException Revise.instantiate_sigs!(mkfi().mod_exs_infos; mode=:eval)

    # The resilient helper records the contextual error instead of throwing.
    @test (Revise.maybe_extract_sigs_or_queue_error!(pkgdata, file, mkfi()); true)
    @test haskey(Revise.queue_errors, (pkgdata, file))
    queued_err, _ = Revise.queue_errors[(pkgdata, file)]
    @test queued_err isa Revise.SignatureExtractionError

    # Self-rendering exceptions use the standard `(exception, backtrace)` log shape
    # without appending Revise's internal backtrace.
    logger = Revise.ReviseLogger()
    redirect_stderr(devnull) do
        Base.CoreLogging.with_logger(logger) do
            Revise.errors([(pkgdata, file)])
        end
    end
    logged_err, logged_bt = only(logger.logs).kwargs[:exception]
    @test logged_err === queued_err
    @test logged_bt === nothing
    @test occursin("failed to extract method signatures", sprint(show, only(logger.logs)))
    delete!(Revise.queue_errors, (pkgdata, file))   # leave global state clean
end

do_test("@require path switch") && @testset "@require path switch" begin
    # `@require` blocks are tracked under a synthetic "__@require__" filename that has
    # no corresponding file on disk. When a package's directory changes, `switch_basepath`
    # must not try to read or watch that fictitious path (issue #678).
    mod = Module(:Issue678)
    Core.eval(mod, :(using Base))
    id = Base.PkgId(Base.UUID("00000000-0000-0000-0000-000000000678"), "Issue678")
    olddir, newdir = mktempdir(), mktempdir()
    pkgdata = Revise.PkgData(id, olddir)
    # Reproduce the state `add_require` leaves behind for a deferred `@require` block:
    # empty `mod_exs_infos` plus an unprocessed `cacheexpr` and no cache file, which
    # drives `switch_basepath` into its read-from-disk fallback.
    reqfile = joinpath("src", "Issue678.jl") * Revise.requires_suffix
    fi = Revise.FileInfo(Revise.ModuleExprsInfos(), identity, "", "", nothing,
                         Tuple{Module,Expr}[(mod, :(g() = 42))], Ref(false), Ref(false))
    push!(pkgdata, reqfile=>fi)
    @test Revise.is_requires_file(reqfile)
    @test (Revise.switch_basepath(pkgdata, newdir); true)   # must not throw
    @test Revise.basedir(pkgdata) == newdir
end

do_test("Unchanged files across a path switch") && @testset "Unchanged files across a path switch" begin
    # A package directory can change while most of its files stay as they are: a
    # `Pkg.develop`ed copy of a release, or a new release that edits only part of its
    # source. `switch_basepath` revises the files that differ and leaves the rest alone,
    # because extracting signatures re-executes macro expansions that define types or
    # constants, which fails once those definitions exist.
    mod = Module(:PathSwitch)
    Core.eval(mod, :(using Base))
    id = Base.PkgId(Base.UUID("00000000-0000-0000-0000-000000000679"), "PathSwitch")
    olddir, newdir = mktempdir(), mktempdir()
    mkpath(joinpath(olddir, "src"))
    mkpath(joinpath(newdir, "src"))

    samefile, changedfile = joinpath("src", "same.jl"), joinpath("src", "changed.jl")
    write(joinpath(olddir, samefile), "f() = 1\n")
    write(joinpath(newdir, samefile), "f() = 1\n")
    write(joinpath(olddir, changedfile), "g() = 1\n")
    write(joinpath(newdir, changedfile), "g() = 2\n")

    pkgdata = Revise.PkgData(id, olddir)
    for (file, ex) in ((samefile, :(f() = 1)), (changedfile, :(g() = 1)))
        mei = Revise.ModuleExprsInfos(mod)
        mei[mod][Revise.RelocatableExpr(ex)] = nothing
        push!(pkgdata, file=>Revise.FileInfo(mei))
    end

    try
        Revise.switch_basepath(pkgdata, newdir)
        @test Revise.basedir(pkgdata) == newdir
        @test (pkgdata, changedfile) ∈ Revise.revision_queue
        @test (pkgdata, samefile) ∉ Revise.revision_queue
        # Signatures are extracted only for the file that is queued.
        @test Revise.fileinfo(pkgdata, changedfile).extracted[]
        @test !Revise.fileinfo(pkgdata, samefile).extracted[]
        # Both files are watched at the new location, queued or not.
        watchlist = Revise.watched_files[joinpath(newdir, "src")]
        @test haskey(watchlist.trackedfiles, "same.jl")
        @test haskey(watchlist.trackedfiles, "changed.jl")
    finally
        filter!(pr -> first(pr) !== pkgdata, Revise.revision_queue)
        delete!(Revise.watched_files, joinpath(olddir, "src"))
        delete!(Revise.watched_files, joinpath(newdir, "src"))
    end
end

do_test("Path switch without a baseline") && @testset "Path switch without a baseline" begin
    # A file whose cached source snapshot is gone has no baseline to compare the new
    # location against, and the old location's source on disk is not one either. The file
    # is queued so that `revise` reports the loss, rather than `switch_basepath` adopting
    # the new source as though the session already held it.
    mod = Module(:PathSwitchStale)
    Core.eval(mod, :(using Base))
    id = Base.PkgId(Base.UUID("00000000-0000-0000-0000-000000001115"), "PathSwitchStale")
    olddir, newdir = mktempdir(), mktempdir()
    mkpath(joinpath(olddir, "src"))
    mkpath(joinpath(newdir, "src"))
    file = joinpath("src", "stale.jl")
    write(joinpath(olddir, file), "h() = 1\n")
    write(joinpath(newdir, file), "h() = 2\n")

    pkgdata = Revise.PkgData(id, olddir)
    # An unparsed file whose only baseline was a cache that can no longer be read
    push!(pkgdata, file=>Revise.FileInfo(mod, joinpath(olddir, "nonexistent.ji"), joinpath(olddir, file)))
    pkgdata.cachebuildid = 0x0123    # nonzero: the session did load from a cache

    try
        @test !Revise.cached_source_is_current(pkgdata, Revise.fileinfo(pkgdata, file))
        @test_logs (:warn, r"source it held for one or more edited files is gone") match_mode=:any Revise.switch_basepath(pkgdata, newdir)
        @test Revise.basedir(pkgdata) == newdir
        @test (pkgdata, file) ∈ Revise.revision_queue
        # Nothing was adopted as a baseline
        @test !Revise.fileinfo(pkgdata, file).parsed[]
        @test !Revise.fileinfo(pkgdata, file).extracted[]
        # The file is still watched at the new location
        @test haskey(Revise.watched_files[joinpath(newdir, "src")].trackedfiles, "stale.jl")
    finally
        filter!(pr -> first(pr) !== pkgdata, Revise.revision_queue)
        delete!(Revise.rewritten_caches, id)
        delete!(Revise.watched_files, joinpath(olddir, "src"))
        delete!(Revise.watched_files, joinpath(newdir, "src"))
    end
end

do_test("Switching environments") && @testset "Switching environments" begin
    old_project = Base.active_project()

    function generate_package(path, val)
        cd(path) do
            pkgpath = normpath(joinpath(path, "TestPackage"))
            srcpath = joinpath(pkgpath, "src")
            if !isdir(srcpath)
                Pkg.generate("TestPackage")
            end
            filepath = joinpath(srcpath, "TestPackage.jl")
            write(filepath, """
                module TestPackage
                f() = $val
                end
                """)
            return pkgpath
        end
    end

    try
        Pkg.activate(; temp=true)

        # generate a package
        root = mktempdir()
        pkg = generate_package(root, 1)
        LibGit2.with(LibGit2.init(pkg)) do repo
            LibGit2.add!(repo, "Project.toml")
            LibGit2.add!(repo, "src/TestPackage.jl")
            test_sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(); digits=0), 0)
            LibGit2.commit(repo, "version 1"; author=test_sig, committer=test_sig)
        end

        # install the package
        Pkg.add(url=pkg)
        sleep(mtimedelay)

        @eval using TestPackage
        sleep(mtimedelay)
        @test Base.invokelatest(TestPackage.f) == 1

        # update the package
        generate_package(root, 2)
        LibGit2.with(LibGit2.GitRepo(pkg)) do repo
            LibGit2.add!(repo, "src/TestPackage.jl")
            test_sig = LibGit2.Signature("TEST", "TEST@TEST.COM", round(time(); digits=0), 0)
            LibGit2.commit(repo, "version 2"; author=test_sig, committer=test_sig)
        end

        # install the update
        Pkg.add(url=pkg)
        sleep(mtimedelay)

        revise()
        @test Base.invokelatest(TestPackage.f) == 2
    finally
        Pkg.activate(old_project)
    end
end

# in v1.8 and higher, a package can't be loaded at all when its precompilation failed
@static if Base.VERSION < v"1.8.0-DEV.1451"
do_test("Broken dependencies (issue #371)") && @testset "Broken dependencies (issue #371)" begin
    testdir = newtestdir()
    srcdir = joinpath(testdir, "DepPkg371", "src")
    filepath = joinpath(srcdir, "DepPkg371.jl")
    cd(testdir) do
        Pkg.generate("DepPkg371")
        write(filepath, """
            module DepPkg371
            using OrderedCollections   # undeclared dependency
            greet() = "Hello world!"
            end
            """)
    end
    sleep(mtimedelay)
    @info "A warning about not having OrderedCollection in dependencies is expected"
    @eval using DepPkg371
    @test DepPkg371.greet() == "Hello world!"
    sleep(mtimedelay)
    write(filepath, """
        module DepPkg371
        using OrderedCollections   # undeclared dependency
        greet() = "Hello again!"
        end
        """)
    @yry()
    @test DepPkg371.greet() == "Hello again!"

    rm_precompile("DepPkg371")
    pop!(LOAD_PATH)
end
end # @static if VERSION ≤ v"1.7"

do_test("Non-jl include_dependency (issue #388)") && @testset "Non-jl include_dependency (issue #388)" begin
    push!(LOAD_PATH, joinpath(@__DIR__, "pkgs"))
    @eval using ExcludeFile
    sleep(0.01)
    pkgdata = Revise.pkgdatas[Base.PkgId(UUID("b915cca1-7962-4ffb-a1c7-2bbdb2d9c14c"), "ExcludeFile")]
    files = Revise.srcfiles(pkgdata)
    @test length(files) == 2
    @test joinpath("src", "ExcludeFile.jl") ∈ files
    @test joinpath("src", "f.jl") ∈ files
    @test joinpath("deps", "dependency.txt") ∉ files
end

do_test("Event-named files bypass the ctime filter") && @testset "Event-named files bypass the ctime filter" begin
    # The kernel stamps inodes at timer-tick resolution (often ~10ms), so a
    # delete-and-recreate can leave the new file with a ctime identical to the
    # stored one; the event's filename plus a content-hash comparison identify
    # the change (issue #945).
    dir = randtmp()
    mkdir(dir)
    push!(to_remove, dir)
    file = "tracked.jl"
    fullpath = joinpath(dir, file)
    write(fullpath, "f() = 1")
    id = Base.PkgId("FakePkg")
    wf = Revise.WatchList()
    push!(wf, file=>id)
    tracked = collect(wf.trackedfiles)

    # Simulated ctime collision: stored ctime matches current and the content
    # was never hashed, but the event names the file
    wf.file_ctimes[file] = ctime(fullpath)
    @test Revise.scan_changed_files(dir, wf, tracked, Set([file])) == [file=>id]
    @test wf.file_hashes[file] == Revise.filehash(fullpath)   # hash recorded on queueing
    # A duplicate notification (same name, same ctime, same content) is absorbed
    wf.file_ctimes[file] = ctime(fullpath)
    @test isempty(Revise.scan_changed_files(dir, wf, tracked, Set([file])))
    # Same-tick rewrite: ctime and name as before, but the content differs
    write(fullpath, "f() = 2")
    wf.file_ctimes[file] = ctime(fullpath)
    @test Revise.scan_changed_files(dir, wf, tracked, Set([file])) == [file=>id]
    # An event naming only an untracked sibling leaves the unchanged file alone
    wf.file_ctimes[file] = ctime(fullpath)
    @test isempty(Revise.scan_changed_files(dir, wf, tracked, Set(["other.jl"])))
    # Unknown event names fall back to the timestamp sweep
    wf.file_ctimes[file] = ctime(fullpath)
    @test isempty(Revise.scan_changed_files(dir, wf, tracked, nothing))
    wf.file_ctimes[file] = ctime(fullpath) - 1
    @test Revise.scan_changed_files(dir, wf, tracked, nothing) == [file=>id]
end

## A missing tracked file is often transient: code generators delete a whole
## directory and rewrite it over several seconds (issue #945). Within
## `missing_file_grace`, a `revise()` must neither delete the file's methods nor
## drop it from tracking; past the grace its methods are deleted but the file
## stays registered so a later recreation is picked up. The orchestration below
## relies on directory-watch events, hence the `watching_files` gate.
do_test("Missing-file grace") && !Revise.watching_files[] && @testset "Missing-file grace" begin
    testdir = newtestdir()
    dn = joinpath(testdir, "MissingFileGrace", "src")
    mkpath(dn)
    write(joinpath(dn, "MissingFileGrace.jl"), """
        module MissingFileGrace
        include("gen.jl")
        end
        """)
    genfile = joinpath(dn, "gen.jl")
    write(genfile, "gen() = 1")
    sleep(mtimedelay)
    @eval using MissingFileGrace
    sleep(mtimedelay)
    @test MissingFileGrace.gen() == 1

    old_grace = Revise.missing_file_grace[]
    try
        # Within the grace period: methods survive the absence, and recreating
        # the file with new content is a normal revision
        Revise.missing_file_grace[] = 5.0
        rm(genfile)
        @yry()
        @test MissingFileGrace.gen() == 1
        @test !isempty(Revise.revision_queue)   # stays queued for revisiting
        @yry()
        @test MissingFileGrace.gen() == 1
        write(genfile, "gen() = 2")
        @yry()
        @test MissingFileGrace.gen() == 2
        @test isempty(Revise.revision_queue)

        # Past the grace period: methods are deleted (with a warning), but the
        # file remains registered, so recreating it still restores the methods.
        # The grace clock starts only when a `revise()` first observes the
        # absence, and that observation rides a filesystem event whose delivery
        # time we don't control. Rather than sleep a fixed interval and assume one
        # revise deletes, poll: keep revising until the deletion lands. This can't
        # flake on a late clock start; a genuine failure to ever delete still
        # surfaces as the `timedwait` timing out.
        Revise.missing_file_grace[] = 0.5
        rm(genfile)
        @yry()
        @test MissingFileGrace.gen() == 2          # survives while within grace
        logs = []
        deleted = timedwait(20.0; pollint=0.1) do
            logs, _ = Test.collect_test_logs() do
                revise()
            end
            isempty(methods(MissingFileGrace.gen))
        end
        @test deleted === :ok                      # :timed_out ⇒ a genuine "never deleted" bug
        @latestworld
        @test isempty(Revise.revision_queue)
        @test_throws MethodError MissingFileGrace.gen()
        # the deleting revise (the final poll iteration) is the one that warns
        @test any(r -> occursin("no longer exists, deleted all methods", r.message), logs)
        write(genfile, "gen() = 3")
        @yry()
        @test MissingFileGrace.gen() == 3
    finally
        Revise.missing_file_grace[] = old_grace
    end
    rm_precompile("MissingFileGrace")
    pop!(LOAD_PATH)
end

do_test("New files & Requires.jl") && @testset "New files & Requires.jl" begin
    # Issue #107
    testdir = newtestdir()
    dn = joinpath(testdir, "NewFile", "src")
    mkpath(dn)
    write(joinpath(dn, "NewFile.jl"), """
            module NewFile
            f() = 1
            module SubModule
            struct NewType end
            end
            end
            """)
    sleep(mtimedelay)
    @eval using NewFile
    @test NewFile.f() == 1
    @test_throws UndefVarError NewFile.g()
    sleep(mtimedelay)
    write(joinpath(dn, "g.jl"), "g() = 2")
    write(joinpath(dn, "NewFile.jl"), """
        module NewFile
        include("g.jl")
        f() = 1
        module SubModule
        struct NewType end
        end
        end
        """)
    @yry()
    @test NewFile.f() == 1
    @test NewFile.g() == 2
    sd = joinpath(dn, "subdir")
    mkpath(sd)
    write(joinpath(sd, "h.jl"), "h(::NewType) = 3")
    write(joinpath(dn, "NewFile.jl"), """
        module NewFile
        include("g.jl")
        f() = 1
        module SubModule
        struct NewType end
        include("subdir/h.jl")
        end
        end
        """)
    @yry()
    @test NewFile.f() == 1
    @test NewFile.g() == 2
    @test NewFile.SubModule.h(NewFile.SubModule.NewType()) == 3

    dn = joinpath(testdir, "DeletedFile", "src")
    mkpath(dn)
    write(joinpath(dn, "DeletedFile.jl"), """
        module DeletedFile
        include("g.jl")
        f() = 1
        end
        """)
    write(joinpath(dn, "g.jl"), "g() = 1")
    sleep(mtimedelay)
    @eval using DeletedFile
    @test DeletedFile.f() == DeletedFile.g() == 1
    sleep(mtimedelay)
    write(joinpath(dn, "DeletedFile.jl"), """
        module DeletedFile
        f() = 1
        end
        """)
    rm(joinpath(dn, "g.jl"))
    @yry()
    @test DeletedFile.f() == 1
    @test_throws MethodError DeletedFile.g()

    rm_precompile("NewFile")
    rm_precompile("DeletedFile")

    # https://discourse.julialang.org/t/revise-with-requires/19347
    dn = joinpath(testdir, "TrackRequires", "src")
    mkpath(dn)
    write(joinpath(dn, "TrackRequires.jl"), """
        module TrackRequires
        using Requires
        const called_onearg = Ref(false)
        onearg(x) = called_onearg[] = true
        module SubModule
        abstract type SuperType end
        end
        function __init__()
            @require EndpointRanges="340492b5-2a47-5f55-813d-aca7ddf97656" begin
                export testfunc
                include("testfile.jl")
            end
            @require CatIndices="aafaddc9-749c-510e-ac4f-586e18779b91" onearg(1)
            @require IndirectArrays="9b13fd28-a010-5f03-acff-a1bbcff69959" @eval SubModule include("st.jl")
            @require RoundingIntegers="d5f540fe-1c90-5db3-b776-2e2f362d9394" begin
                fn = joinpath(@__DIR__, "subdir", "anotherfile.jl")
                include(fn)
                @require Revise="295af30f-e4ad-537b-8983-00126c2a3abe" Revise.track(TrackRequires, fn)
            end
            @require UnsafeArrays="c4a57d5a-5b31-53a6-b365-19f8c011fbd6" begin
                fn = joinpath(@__DIR__, "subdir", "yetanotherfile.jl")
                include(fn)
            end
        end
        end # module
        """)
    write(joinpath(dn, "testfile.jl"), "testfunc() = 1")
    write(joinpath(dn, "st.jl"), """
        struct NewType <: SuperType end
        h(::NewType) = 3
        """)
    sd = mkpath(joinpath(dn, "subdir"))
    write(joinpath(sd, "anotherfile.jl"), "ftrack() = 1")
    write(joinpath(sd, "yetanotherfile.jl"), "fauto() = 1")
    sleep(mtimedelay)
    @eval using TrackRequires
    notified = isdefined(TrackRequires.Requires, :withnotifications)
    notified || @warn "Requires does not support notifications"
    @test_throws UndefVarError TrackRequires.testfunc()
    @test_throws UndefVarError TrackRequires.SubModule.h(TrackRequires.SubModule.NewType())
    @eval using EndpointRanges  # to trigger Requires
    sleep(mtimedelay)
    notified && @test TrackRequires.testfunc() == 1
    write(joinpath(dn, "testfile.jl"), "testfunc() = 2")
    @yry()
    notified && @test TrackRequires.testfunc() == 2
    @test_throws UndefVarError TrackRequires.SubModule.h(TrackRequires.SubModule.NewType())
    # Issue #477
    @eval using IndirectArrays
    sleep(mtimedelay)
    notified && @test TrackRequires.SubModule.h(TrackRequires.SubModule.NewType()) == 3
    # Check a non-block expression
    warnfile = randtmp()
    open(warnfile, "w") do io
        redirect_stderr(io) do
            @eval using CatIndices
            sleep(0.5)
        end
    end
    notified && @test TrackRequires.called_onearg[]
    @test isempty(read(warnfile, String))
    # Issue #431
    @test_throws UndefVarError TrackRequires.ftrack()
    if !(get(ENV, "CI", nothing) == "true" && Base.VERSION.major == 1 && Base.VERSION.minor == 8)   # circumvent CI hang
        @eval using RoundingIntegers
        sleep(2)  # allow time for the @async in all @require blocks to finish
        if notified
            @test TrackRequires.ftrack() == 1
            id = Base.PkgId(TrackRequires)
            pkgdata = Revise.pkgdatas[id]
            sf = Revise.srcfiles(pkgdata)
            @test count(name->occursin("@require", name), sf) == 1
            @test count(name->occursin("anotherfile", name), sf) == 1
            @test !any(isequal("."), sf)
            idx = findfirst(name->occursin("anotherfile", name), sf)
            @test !isabspath(sf[idx])
        end
    end
    @test_throws UndefVarError TrackRequires.fauto()
    @eval using UnsafeArrays
    sleep(2)  # allow time for the @async in all @require blocks to finish
    if notified
        @test TrackRequires.fauto() == 1
        id = Base.PkgId(TrackRequires)
        pkgdata = Revise.pkgdatas[id]
        sf = Revise.srcfiles(pkgdata)
        @test count(name->occursin("@require", name), sf) == 1
        @test count(name->occursin("yetanotherfile", name), sf) == 1
        @test !any(isequal("."), sf)
        idx = findfirst(name->occursin("yetanotherfile", name), sf)
        @test !isabspath(sf[idx])
    end

    # Ensure it also works if the Requires dependency is pre-loaded
    dn = joinpath(testdir, "TrackRequires2", "src")
    mkpath(dn)
    write(joinpath(dn, "TrackRequires2.jl"), """
        module TrackRequires2
        using Requires
        function __init__()
            @require EndpointRanges="340492b5-2a47-5f55-813d-aca7ddf97656" begin
                export testfunc
                include("testfile.jl")
            end
            @require MappedArrays="dbb5928d-eab1-5f90-85c2-b9b0edb7c900" begin
                export othertestfunc
                include("testfile2.jl")
            end
        end
        end # module
        """)
    write(joinpath(dn, "testfile.jl"), "testfunc() = 1")
    write(joinpath(dn, "testfile2.jl"), "othertestfunc() = -1")
    sleep(mtimedelay)
    @eval using TrackRequires2
    sleep(mtimedelay)
    notified && @test TrackRequires2.testfunc() == 1
    @test_throws UndefVarError TrackRequires2.othertestfunc()
    write(joinpath(dn, "testfile.jl"), "testfunc() = 2")
    @yry()
    notified && @test TrackRequires2.testfunc() == 2
    @test_throws UndefVarError TrackRequires2.othertestfunc()
    @eval using MappedArrays
    @test TrackRequires2.othertestfunc() == -1
    sleep(mtimedelay)
    write(joinpath(dn, "testfile2.jl"), "othertestfunc() = -2")
    @yry()
    notified && @test TrackRequires2.othertestfunc() == -2

    # Issue #442
    push!(LOAD_PATH, joinpath(@__DIR__, "pkgs"))
    @eval using Pkg442
    sleep(0.01)
    @test check442()
    @test Pkg442.check442A()
    @test Pkg442.check442B()
    @test Pkg442.Dep442B.has442A()
    pop!(LOAD_PATH)

    rm_precompile("TrackRequires")
    rm_precompile("TrackRequires2")
    pop!(LOAD_PATH)
end

do_test("revision_event autoreset") && @testset "revision_event autoreset (issue #837)" begin
    # `revision_event` must be an autoreset `Base.Event`, so that `wait`
    # clears the bit. Without autoreset, a `notify` fired between the time
    # `entr`'s loop returns from `wait` and the time it would call
    # `reset(revision_event)` is silently dropped, and the corresponding
    # file change is not serviced until another notify happens.
    ev = Revise.revision_event
    @test ev isa Base.Event
    # Drain any stale state left by earlier testsets.
    notify(ev); wait(ev)
    # After `wait`, the bit must be cleared: a second wait with no
    # intervening `notify` must block. Without autoreset it would return
    # immediately because the bit set by the previous `notify` would still
    # be latched.
    t = @async wait(ev)
    @test timedwait(() -> istaskdone(t), 0.2; pollint=0.02) === :timed_out
    notify(ev)
    wait(t)
end

do_test("entr") && @testset "entr" begin
    # `entr` debounces file changes: a change schedules the callback to run
    # `pause` seconds later, and any further change before then pushes that
    # deadline out, so a burst of changes less than `pause` apart triggers the
    # callback exactly once. Poll for the expected state with `timedwait`
    # rather than fixed `sleep`s, which flake on slow CI runners (#1039-#1042).
    waitfor(pred) = timedwait(pred, event_timeout; pollint=0.02)

    # A generous `pause` keeps a burst coalesced despite variable
    # filesystem-event detection latency; a tight value flaked on Windows (#1040).
    pause = 2.0

    srcfile1 = joinpath(tempdir(), randtmp()*".jl"); push!(to_remove, srcfile1)
    srcfile2 = joinpath(tempdir(), randtmp()*".jl"); push!(to_remove, srcfile2)
    revise(throw=true)   # force compilation
    write(srcfile1, "Core.eval(Main, :(__entr__ = 1))")
    touch(srcfile2)
    Core.eval(Main, :(__entr__ = 0))
    sleep(mtimedelay)
    try
        @sync begin
            @test Main.__entr__ == 0

            @async begin
                entr([srcfile1, srcfile2]; pause) do
                    include(srcfile1)
                end
            end
            # `postpone=false` runs the callback synchronously, before `entr`
            # arms its watch.
            waitfor(() -> Main.__entr__ >= 1)
            @test Main.__entr__ == 1  # callback should have been run (postpone=false)

            # Drive a single modification through the watch. The watch may not
            # be armed the instant `entr` starts (#1040, #1041), so retry the
            # write — but space the retries more than `pause` apart so each is
            # its own burst and the debounce is allowed to fire between them.
            probed = timedwait(event_timeout; pollint=pause+1) do
                Main.__entr__ >= 2 || (write(srcfile1, "Core.eval(Main, :(__entr__ = 2))"); false)
            end
            @test probed === :ok
            sleep(2*pause)            # let the debounce settle
            @test Main.__entr__ == 2  # callback should have been called

            # A burst of changes less than `pause` apart must coalesce into a
            # single callback. Switch `srcfile1` to an increment so a surplus
            # callback would be visible, hammer both files, then stop and let
            # the debounce fire.
            write(srcfile1, "Core.eval(Main, :(__entr__ += 1))")
            for _ in 1:8
                touch(srcfile2)
                sleep(pause/4)
            end
            waitfor(() -> Main.__entr__ >= 3)
            sleep(2*pause)            # any further callback would fire within ~pause
            # The burst must *coalesce*: 8 rapid changes collapse to far fewer
            # callbacks than changes. An exact count is too strict on Windows,
            # where the OS can spread event delivery across more than one
            # `pause` window and split the burst into a second callback (e.g.,
            # CI failure on #1066); bound it loosely so it still catches a
            # debounce that fails to coalesce at all.
            nburst = Main.__entr__ - 2
            @test 1 <= nburst <= 4

            write(srcfile1, "error(\"stop\")")
        end
        @test false
    catch err
        while err isa CompositeException
            err = err.exceptions[1]
            if err isa TaskFailedException
                err = err.task.exception
            end
            if err isa CapturedException
                err = err.ex
            end
        end
        @test isa(err, LoadError)
        @test err.error.msg == "stop"
    end

    # Callback should have been removed
    @test isempty(Revise.user_callbacks_by_file[srcfile1])


    # Watch directories (#470). Skipped under polling: poll mode watches a
    # directory through its own `stat`, which does not change when a file
    # inside it is modified in place, so the checks below cannot be detected.
    if !Revise.polling_files[]
        try
            @sync let
                srcdir = joinpath(tempdir(), randtmp())
                mkdir(srcdir)

                trigger = joinpath(srcdir, "trigger.txt")

                counter = Ref(0)
                stop = Ref(false)

                @async begin
                    entr([srcdir]; pause) do
                        counter[] += 1
                        stop[] && error("stop watching directory")
                    end
                end
                waitfor(() -> counter[] >= 1)
                @test counter[] == 1               # postpone=false
                @test length(readdir(srcdir)) == 0 # directory should still be empty

                # Drive a detected change to confirm the watch is live, retrying
                # more than `pause` apart so the debounce can fire between attempts.
                probed = timedwait(event_timeout; pollint=pause+1) do
                    counter[] >= 2 || (touch(trigger); false)
                end
                @test probed === :ok
                sleep(2*pause)                     # let the debounce settle
                counter[] = 0                      # watch is live now; count from a clean slate

                # File modification
                touch(trigger)
                waitfor(() -> counter[] >= 1)
                @test counter[] == 1

                # File deletion -> the directory should be empty again
                rm(trigger)
                waitfor(() -> counter[] >= 2)
                @test length(readdir(srcdir)) == 0
                @test counter[] == 2

                # A burst of changes less than `pause` apart must coalesce into a
                # single callback.
                for _ in 1:8
                    touch(trigger)
                    rm(trigger)
                    sleep(pause/4)
                end
                waitfor(() -> counter[] >= 3)
                sleep(2*pause)       # any further callback would fire within ~pause
                # As above: assert the burst coalesced to far fewer callbacks than
                # changes, but don't insist on just one.
                nburst = counter[] - 2
                @test 1 <= nburst <= 4

                # Stop
                stop[] = true
                touch(trigger)
            end

            # `entr` should have errored by now
            @test false
        catch err
            while err isa CompositeException
                err = err.exceptions[1]
                if err isa TaskFailedException
                    err = err.task.exception
                end
                if err isa CapturedException
                    err = err.ex
                end
            end
            @test isa(err, ErrorException)
            @test err.msg == "stop watching directory"
        end
    end
end

const A354_result = Ref(0)

# issue #354
do_test("entr with modules") && @testset "entr with modules" begin

    testdir = newtestdir()
    modname = "A354"
    srcfile = joinpath(testdir, modname * ".jl")

    setvalue(x) = write(srcfile, "module $modname test() = $x end")

    setvalue(1)

    # these sleeps may not be needed...
    sleep(mtimedelay)
    @eval using A354
    sleep(mtimedelay)

    A354_result[] = 0

    @async begin
        sleep(mtimedelay)
        setvalue(2)
        # belt and suspenders -- make sure we trigger entr:
        sleep(mtimedelay)
        touch(srcfile)
        sleep(mtimedelay)
    end

    try
        entr([], [A354], postpone=true) do
            A354_result[] = A354.test()
            error()
        end
    catch
    end

    @test A354_result[] == 2

    rm_precompile(modname)

end

# issue #469
do_test("entr with all files") && @testset "entr with all files" begin
    testdir = newtestdir()
    modname = "A469"
    srcfile = joinpath(testdir, modname * ".jl")
    write(srcfile, "module $modname test() = 469 end")

    sleep(mtimedelay)
    @eval using A469
    sleep(mtimedelay)
    result = Ref(0)

    try
        @sync begin
            @async begin
                # Watch all files known to Revise
                # (including `srcfile`)
                entr([]; all=true, postpone=true) do
                    result[] = 1
                    error("stop")
                end
            end
            sleep(mtimedelay)
            @test result[] == 0

            # Trigger the callback
            touch(srcfile)
        end
        @test false
    catch err
        while err isa CompositeException
            err = err.exceptions[1]
            if err isa TaskFailedException
                err = err.task.exception
            end
            if err isa CapturedException
                err = err.ex
            end
        end
        @test isa(err, ErrorException)
        @test err.msg == "stop"
    end

    # If we got to this point, the callback should have been triggered. But
    # let's check nonetheless
    @test result[] == 1

    rm_precompile(modname)
end

do_test("callbacks") && @testset "callbacks" begin

    append(path, x...) = open(path, append=true) do io
        write(io, x...)
    end

    mktemp() do path, _
        contents = Ref("")
        key = Revise.add_callback([path]) do
            contents[] = read(path, String)
        end

        # Wait for the watcher to enqueue the change before revising, instead of
        # hoping a fixed sleep outlasts event-detection latency (leading to
        # occasional CI failures, e.g., #1066). A callback-only file is not part
        # of a tracked package, so its change lands in `user_callbacks_queue`
        # rather than `revision_queue`.
        function revise_after_event()
            timedwait(() -> !isempty(Revise.user_callbacks_queue), event_timeout; pollint=0.02)
            sleep(0.02)
            revise()
            sleep(mtimedelay)   # ctime guard so the next write has a larger ctime
        end

        sleep(2*mtimedelay)

        append(path, "abc")
        revise_after_event()
        @test contents[] == "abc"

        append(path, "def")
        revise_after_event()
        @test contents[] == "abcdef"

        # The callback is removed, so nothing is enqueued and there is no event to
        # wait for; a fixed wait is correct here because we assert the *absence*
        # of an update.
        Revise.remove_callback(key)
        sleep(mtimedelay)
        append(path, "ghi")
        sleep(mtimedelay)
        revise()
        @test contents[] == "abcdef"
    end

    testdir = newtestdir()
    modname = "A355"
    srcfile = joinpath(testdir, modname * ".jl")

    setvalue(x) = write(srcfile, "module $modname test() = $x end")

    setvalue(1)

    sleep(mtimedelay)
    @eval using A355
    sleep(mtimedelay)

    A355_result = Ref(0)

    Revise.add_callback([], [A355]) do
        A355_result[] = A355.test()
    end

    sleep(mtimedelay)
    setvalue(2)
    # belt and suspenders -- make sure we trigger entr:
    sleep(mtimedelay)
    touch(srcfile)

    @yry()

    @test A355_result[] == 2

    rm_precompile(modname)

    # Issue 574 - ad-hoc revision of a file, combined with add_callback()
    A574_path = joinpath(testdir, "A574.jl")

    set_foo_A574(x) = write(A574_path, "foo_574() = $x")

    set_foo_A574(1)
    includet(@__MODULE__, A574_path)
    @test Base.invokelatest(foo_574) == 1

    foo_A574_result = Ref(0)
    key = Revise.add_callback([A574_path]) do
        foo_A574_result[] = foo_574()
    end

    sleep(mtimedelay)
    set_foo_A574(2)
    sleep(mtimedelay)
    revise()
    @test Base.invokelatest(foo_574) == 2
    @test foo_A574_result[] == 2

    Revise.remove_callback(key)

    sleep(mtimedelay)
    set_foo_A574(3)
    sleep(mtimedelay)
    revise()
    @test Base.invokelatest(foo_574) == 3
    @test foo_A574_result[] == 2 # <- callback removed - no longer updated
end

do_test("includet with mod arg (issue #689)") && @testset "includet with mod arg (issue #689)" begin
    testdir = newtestdir()

    common = joinpath(testdir, "common.jl")
    write(common, """
        module Common
            const foo = 2
        end
        """)

    routines = joinpath(testdir, "routines.jl")
    write(routines, """
        module Routines
            using Revise
            includet(@__MODULE__, raw"$common")
            using .Common
        end
        """)

    codes = joinpath(testdir, "codes.jl")
    write(codes, """
        module Codes
            using Revise
            includet(@__MODULE__, raw"$common")
            using .Common
        end
        """)

    driver = joinpath(testdir, "driver.jl")
    write(driver, """
        module Driver
            using Revise
            includet(@__MODULE__, raw"$routines")
            using .Routines
            includet(@__MODULE__, raw"$codes")
            using .Codes
        end
        """)

    includet(@__MODULE__, driver)
    @test parentmodule(Driver.Routines.Common) == Driver.Routines
    @test Base.moduleroot(Driver.Routines.Common) == Main

    @test parentmodule(Driver.Codes.Common) == Driver.Codes
    @test Base.moduleroot(Driver.Codes.Common) == Main

    @test Driver.Routines.Common.foo == 2
    @test Driver.Codes.Common.foo == 2
end

do_test("@includet uses caller's module (issue #682)") && @testset "@includet uses caller's module (issue #682)" begin
    testdir = newtestdir()

    srcfile = joinpath(testdir, "evalscope.jl")
    write(srcfile, "f_682() = 1")

    module682 = Module(:Module682)
    Core.eval(module682, :(using Revise))
    sleep(mtimedelay)
    Core.eval(module682, :(@includet $srcfile))

    # The macro evaluates into the caller's module, not `Main`.
    @test Base.invokelatest(module682.f_682) == 1
    @test !isdefined(Main, :f_682)

    # Tracking still works.
    sleep(mtimedelay)
    write(srcfile, "f_682() = 2")
    yry()
    @test Base.invokelatest(module682.f_682) == 2
end

do_test("misc - coverage") && !isinteractive() && @testset "misc - coverage" begin
    @test Revise.ReviseEvalException("undef", UndefVarError(:foo)).loc isa String
    @test !Revise.throwto_repl(UndefVarError(:foo))   # this causes an error in interactive

    @test endswith(Revise.fallback_juliadir(), "julia")

    # issue #717: the stale-cache hint must point at the active depot, not a
    # hardcoded ~/.julia
    major, minor = Base.VERSION.major, Base.VERSION.minor
    @test Revise.revise_cache_dir() == joinpath(first(DEPOT_PATH), "compiled", "v$major.$minor", "Revise")

    @test isnothing(Revise.revise(REPL.REPLBackend()))
end

do_test("watch reappearance") && @testset "watch reappearance" begin
    # A transiently-missing watched path must not be abandoned (#523): the watcher
    # waits up to `watch_reappear_grace[]` for it to return before giving up.
    dir = mktempdir()
    key = dir
    Revise.watched_files[key] = Revise.WatchList()
    old_grace = Revise.watch_reappear_grace[]
    try
        # Present: resume immediately.
        @test Revise.await_watched_path(isdir, dir, key) === :reappeared
        # Missing but reappears within the grace period: resume.
        rm(dir; recursive=true)
        recreate = @async (sleep(0.3); mkdir(dir))
        Revise.watch_reappear_grace[] = 5.0
        @test Revise.await_watched_path(isdir, dir, key) === :reappeared
        wait(recreate)
        # Missing past the grace period: give up (and the caller warns).
        rm(dir; recursive=true)
        Revise.watch_reappear_grace[] = 0.2
        @test Revise.await_watched_path(isdir, dir, key) === :gone
        # Missing and no longer registered (package moved/removed): give up quietly.
        Revise.watch_reappear_grace[] = 5.0
        delete!(Revise.watched_files, key)
        @test Revise.await_watched_path(isdir, dir, key) === :removed
    finally
        Revise.watch_reappear_grace[] = old_grace
        haskey(Revise.watched_files, key) && delete!(Revise.watched_files, key)
        isdir(dir) && rm(dir; recursive=true)
    end
end

# Restricted to directory-watching mode on Linux: this exercises a full
# delete/recreate/edit round-trip through the filesystem watcher, and only inotify
# delivers those events reliably enough to test. (FSEvents drops directory-removal
# events -- cf. the `Cleanup` test -- and on Windows a same-path recreate-then-edit
# races ReadDirectoryChangesW arming.) The fix itself is platform-general.
do_test("re-watch after reappearance") && !Revise.watching_files[] && Sys.islinux() &&
        @testset "re-watch after reappearance (#1057)" begin
    # A branch switch can remove a watched subdirectory and later restore it. When
    # it reappears, Revise must resume watching it so that subsequent edits are
    # still detected, rather than leaving a stale, watcher-less registration.
    testdir = newtestdir()
    dn = joinpath(testdir, "ReWatch", "src")
    mkpath(joinpath(dn, "sub"))
    withinclude = """
        module ReWatch
        include("sub/extra.jl")
        f() = 1
        end
        """
    noinclude = """
        module ReWatch
        f() = 1
        end
        """
    write(joinpath(dn, "ReWatch.jl"), withinclude)
    write(joinpath(dn, "sub", "extra.jl"), "g() = 2")
    sleep(mtimedelay)
    @eval using ReWatch
    sleep(mtimedelay)
    subdir = joinpath(dn, "sub")
    @test ReWatch.f() == 1
    @test ReWatch.g() == 2
    @test haskey(Revise.watched_files, subdir)

    old_grace = Revise.watch_reappear_grace[]
    Revise.watch_reappear_grace[] = 0.0   # give up promptly so the round-trip is quick
    try
        # "Switch away": drop the subdirectory and the `include`.
        rm(subdir; recursive=true)
        write(joinpath(dn, "ReWatch.jl"), noinclude)
        # The watcher must relinquish the directory: releasing its registration is
        # exactly what lets a fresh watcher start when the directory returns.
        @test timedwait(() -> !haskey(Revise.watched_files, subdir), event_timeout) === :ok
        @yry

        # "Switch back": restore the subdirectory and the `include`.
        mkdir(subdir)
        write(joinpath(subdir, "extra.jl"), "g() = 2")
        write(joinpath(dn, "ReWatch.jl"), withinclude)
        @yry
        @test haskey(Revise.watched_files, subdir)
        @test ReWatch.g() == 2

        # The crucial check: an edit to the reappeared file is detected.
        write(joinpath(subdir, "extra.jl"), "g() = 99")
        @yry
        @test ReWatch.g() == 99
    finally
        Revise.watch_reappear_grace[] = old_grace
    end
end

do_test("Frozen world") && @testset "Frozen world" begin
    # Revise pins its own method dispatch to the world it froze at `__init__` (issue #552),
    # so revising a method Revise itself uses cannot invalidate Revise mid-operation. The rest
    # of this suite is itself the functional test: every revision here runs while Revise is
    # frozen. Here we check the freeze/advance semantics directly.
    @test Revise.worldage[] isa UInt
    saved = Revise.worldage[]
    try
        @eval frozenworld_probe() = 1
        Revise.advance_world!()                      # freeze with the `== 1` method visible
        wfrozen = Revise.worldage[]
        @test Revise.frozen(frozenworld_probe) == 1
        @eval frozenworld_probe() = 2                # redefine; advances the global world
        @test frozenworld_probe() == 2               # latest dispatch sees the new method
        @test Revise.frozen(frozenworld_probe) == 1  # frozen dispatch still sees the old one
        @test Revise.worldage[] == wfrozen           # advancing is manual-only
        Revise.advance_world!()
        @test Revise.frozen(frozenworld_probe) == 2  # now the frozen world includes it
    finally
        Revise.worldage[] = saved
    end
end

do_test("Frozen world user-code frame") && @testset "Frozen world user-code frame" begin
    # Counterpart to "Frozen world": while Revise's own dispatch is frozen, the code it
    # interprets on the user's behalf must run at the *latest* world, or a definition made
    # earlier in the same revision is "too new" when later code dispatches to it (issues
    # #552, #607). Revise secures this by building the interpreter `Frame` with
    # `world=get_world_counter()` (see `methods_by_execution!`). This checks the underlying
    # capability directly: the same call interpreted in a stale world throws "method too
    # new", while at the latest world it succeeds.
    @eval function frozenframe_target end           # commit the binding at its own world
    wstale = Base.get_world_counter()               # after the binding, before any method
    @eval frozenframe_target(x::Int) = 42           # only applicable method, defined after wstale
    lwr = Meta.lower(@__MODULE__, :(frozenframe_target(1)))
    mkframe(w) = Frame(@__MODULE__, lwr.args[1]::Core.CodeInfo; world=w)
    # At the latest world the freshly-defined method is visible and the call succeeds:
    @test Revise.JuliaInterpreter.finish_and_return!(mkframe(Base.get_world_counter())) == 42
    # Pinned to the pre-definition world it is not: the call throws (a "method too new"
    # MethodError, or its binding-partition analog). The two frames differ only in `world`,
    # so the world is the sole cause — exactly what threading it into the frame fixes.
    # `NullLogger` silences any stale-world access warning a given Julia version may emit.
    @test_throws Exception Base.CoreLogging.with_logger(Base.CoreLogging.NullLogger()) do
        Revise.JuliaInterpreter.finish_and_return!(mkframe(wstale))
    end
end

do_test("Frozen world include path") && @testset "Frozen world include path" begin
    # An `include` filename in interpreted user code may be a custom AbstractString
    # (e.g. RelocatableFolders.Path in Plots' @require blocks) whose methods are newer
    # than Revise's frozen world. Recording it in `exinfo.includes` converts it to
    # `String`; that conversion must happen at the latest world or it throws a
    # "method too new" MethodError (e.g. for `iterate`).
    saved = Revise.worldage[]
    try
        Revise.advance_world!()      # freeze before the fake path type's methods exist
        @eval module FrozenIncludePath
            struct FakePath <: AbstractString
                path::String
            end
            Base.ncodeunits(p::FakePath) = ncodeunits(p.path)
            Base.codeunit(p::FakePath) = codeunit(p.path)
            Base.codeunit(p::FakePath, i::Integer) = codeunit(p.path, i)
            Base.isvalid(p::FakePath, i::Integer) = isvalid(p.path, i)
            Base.iterate(p::FakePath) = iterate(p.path)
            Base.iterate(p::FakePath, i::Integer) = iterate(p.path, i)
            const fake = FakePath("frozen_include_target.jl")
            module Target end
        end
        # invokelatest: these bindings are newer than this thunk's world
        FIP = Base.invokelatest(getglobal, @__MODULE__, :FrozenIncludePath)
        fake = Base.invokelatest(getglobal, FIP, :fake)
        target = Base.invokelatest(getglobal, FIP, :Target)
        ex = quote
            frozen_include_dummy() = 1   # force the interpreter path
            include($fake)
        end
        exinfo = Revise.ExInfo(ex)
        Revise.frozen(Revise.methods_by_execution!, exinfo, target, ex;
                      mode=:eval, skip_include=true, always_rethrow=true)
        @test exinfo.includes == [(target, identity, "frozen_include_target.jl")]
    finally
        Revise.worldage[] = saved
    end
end

do_test("Revise issue #607") && @testset "Revise issue #607" begin
    # A top-level loop that defines methods (so Revise must interpret it during a revision)
    # and, while building each method's arguments, applies a freshly-created closure (the
    # comprehension). Under Revise's frozen world this closure-application must still see the
    # latest world; otherwise it throws "method too new" and the revision silently fails
    # (issue #607, originally hit via Plots' `utils_eval.jl`).
    testdir = newtestdir()
    dn = joinpath(testdir, "WA607", "src"); mkpath(dn)
    fn = joinpath(dn, "WA607.jl")
    write(fn, """
        module WA607
        for fname in (:wa_a, :wa_b)
            argtypes = (Int, Float64)
            args = Any[:(\$(Symbol(:x, i))::\$T) for (i, T) in enumerate(argtypes)]
            @eval \$fname(\$(args...)) = 1
        end
        end""")
    sleep(mtimedelay)
    @eval using WA607
    sleep(mtimedelay)
    @test WA607.wa_a(1, 2.0) == 1
    write(fn, """
        module WA607
        for fname in (:wa_a, :wa_b)
            argtypes = (Int, Float64)
            args = Any[:(\$(Symbol(:x, i))::\$T) for (i, T) in enumerate(argtypes)]
            @eval \$fname(\$(args...)) = 2
        end
        end""")
    @yry()
    @test WA607.wa_a(1, 2.0) == 2
    @test WA607.wa_b(3, 4.0) == 2
    rm_precompile("WA607")
end

do_test("deprecated") && @testset "deprecated" begin
    @test_logs (:warn, r"`steal_repl_backend` has been removed.*") Revise.async_steal_repl_backend()
    @test_logs (:warn, r"`steal_repl_backend` has been removed.*") Revise.wait_steal_repl_backend()
end

println("beginning cleanup")
GC.gc(); GC.gc()

@testset "Cleanup" begin
    # The watchers defer their "is not watching" warning by `watch_reappear_grace`
    # to ride out transient disappearances (#523). These deletions are permanent,
    # so drop the grace to make the warning fire promptly within the window below.
    old_grace = Revise.watch_reappear_grace[]
    Revise.watch_reappear_grace[] = 0.0
    try
        _logs, _ = Test.collect_test_logs() do
            warnfile = randtmp()
            open(warnfile, "w") do io
                redirect_stderr(io) do
                    for name in to_remove
                        try
                            rm(name; force=true, recursive=true)
                            deleteat!(LOAD_PATH, findall(LOAD_PATH .== name))
                        catch
                        end
                    end
                    for _ = 1:3
                        yry()
                        GC.gc()
                    end
                end
            end
            msg = Revise.watching_files[] ? "is not an existing file" : "is not an existing directory"
            isempty(ARGS) && !Sys.isapple() && @test occursin(msg, read(warnfile, String))
            rm(warnfile)
        end
    finally
        Revise.watch_reappear_grace[] = old_grace
    end
end

GC.gc(); GC.gc(); GC.gc()   # work-around for https://github.com/JuliaLang/julia/issues/28306

# see #532 Fix InitError opening none existent Project.toml
function load_in_empty_project_test()
    # This will try to load Revise in a julia seccion
    # with an empty environment (missing Project.toml)

    julia = Base.julia_cmd()
    revise_proj = escape_string(Base.active_project())
    @assert isfile(revise_proj)

    src = """
    import Pkg
    Pkg.activate("fake_env")
    @assert !isfile(Base.active_project())

    # force to load the package env Revise version
    empty!(LOAD_PATH)
    push!(LOAD_PATH, "$revise_proj")

    @info "A warning about no Manifest.toml file found is expected"
    try; using Revise
        catch err
            # just fail for this error (see #532)
            err isa InitError && rethrow(err)
    end
    """
    cmd = `$julia --project=@. -E $src`

    @test begin
        wait(run(cmd))
        true
    end
end

do_test("Import in empty environment (issue #532)") && @testset "Import in empty environment (issue #532)" begin
    load_in_empty_project_test();
end

do_test("startup precompilation") && @testset "startup precompilation" begin
    # issue #900: loading Revise and exiting must not force runtime compilation
    # of any Revise-owned method; the whole load path should be covered by
    # `src/precompile.jl`. `-i` without a tty exercises the REPL-backend-wait
    # branch of `__init__`. Use a bare executable with pkgimages enabled (not
    # `Base.julia_cmd()`): under coverage the parent runs `--pkgimages=no`,
    # which bypasses the precompile image and makes everything compile at load.
    julia = Base.julia_cmd().exec[1]
    proj = dirname(Base.active_project())
    loadfile = tempname()
    write(loadfile, "using Revise\n")
    tracefile = tempname()
    code = "exit()"
    try
        run(pipeline(`$julia --startup-file=no --project=$proj --pkgimages=yes -i -L $loadfile --trace-compile=$tracefile -e $code`;
                        stdin=devnull, stdout=devnull, stderr=devnull))
        # `--trace-compile` only creates the file once something compiles; an
        # absent file therefore means nothing compiled at all, which is the
        # best possible outcome here.
        trace = isfile(tracefile) ? read(tracefile, String) : ""
        @test !occursin("Revise.", trace)
    finally
        rm(loadfile; force=true)
        rm(tracefile; force=true)
    end
end

# Issue #961: when an indirect dep has no precompile cache and Julia is started
# with `--compiled-modules=existing`, the dep is loaded via
# `include(Base.__toplevel__, path)`. Revise's `queue_includes!` must not rewrite
# `__toplevel__` to `Main`, or `ExprSplitter` synthesizes an empty `Main.<PkgName>`
# stub instead of resolving to the real loaded module.
function empty_main_stub_test_961()
    foo_uuid = "11111111-2222-3333-4444-555555555555"
    bar_uuid = "66666666-7777-8888-9999-aaaaaaaaaaaa"
    sandbox = mktempdir()
    push!(to_remove, sandbox)
    foo = joinpath(sandbox, "Foo961")
    bar = joinpath(sandbox, "Bar961")
    proj = joinpath(sandbox, "active"); mkpath(proj)
    mkpath(joinpath(foo, "src")); mkpath(joinpath(bar, "src"))
    write(joinpath(foo, "Project.toml"),
        "name = \"Foo961\"\nuuid = \"$foo_uuid\"\nversion = \"0.1.0\"\n")
    write(joinpath(foo, "src", "Foo961.jl"),
        "module Foo961\ngreet() = \"hi\"\nend\n")
    write(joinpath(bar, "Project.toml"),
        "name = \"Bar961\"\nuuid = \"$bar_uuid\"\nversion = \"0.1.0\"\n[deps]\nFoo961 = \"$foo_uuid\"\n")
    write(joinpath(bar, "src", "Bar961.jl"),
        "module Bar961\nusing Foo961\nend\n")

    julia = Base.julia_cmd()
    # The Revise being tested must be the one at this checkout, not whatever
    # version `@v#.#` happens to have installed.
    revise_dir = normpath(joinpath(dirname(pathof(Revise)), ".."))

    # Set up `proj` with Bar961 as a *direct* dep and Foo961 only as a
    # transitive dep (so Main's environment cannot see Foo961 by name).
    # Loading Bar961 here precompiles both packages.
    setup = """
        import Pkg
        Pkg.activate($(repr(proj)))
        Pkg.develop([Pkg.PackageSpec(path=$(repr(bar))),
                     Pkg.PackageSpec(path=$(repr(foo))),
                     Pkg.PackageSpec(path=$(repr(revise_dir)))])
        Pkg.rm("Foo961")
        using Bar961
    """
    @test success(pipeline(`$julia --project=$proj -e $setup`, stderr=stderr))

    # Force Foo961 to load from source on the next launch.
    vdir = "v" * string(VERSION.major) * "." * string(VERSION.minor)
    for depot in DEPOT_PATH
        d = joinpath(depot, "compiled", vdir, "Foo961")
        isdir(d) && rm(d; recursive=true, force=true)
    end

    # With the bug, `Main.Foo961` is created as an empty stub during Revise's
    # parsing pass over the non-precompiled Foo961 source.
    check = """
        import Revise
        import Bar961
        print(isdefined(Main, :Foo961))
    """
    out = read(`$julia --project=$proj --compiled-modules=existing -e $check`, String)
    @test out == "false"
end

do_test("Empty Main.<Pkg> stub (issue #961)") && if VERSION >= v"1.11"
    # `--compiled-modules=existing` was added in Julia 1.11.
    @testset "Empty Main.<Pkg> stub (issue #961)" begin
        empty_main_stub_test_961()
    end
end

include("backedges.jl")

# include("non_jl_test.jl")

do_test("Base signatures") && @testset "Base signatures" begin
    println("beginning signatures tests")
    # Using the extensive repository of code in Base as a testbed
    sigfile = normpath(@__DIR__, "sigtest.jl")
    @test success(pipeline(`$(Base.julia_cmd()) $sigfile`, stderr=stderr))
end

# Run this test in a separate julia process, since it messes with projects, and we don't want to have to
# worry about making sure it resets cleanly.
do_test("Switch Versions") && let
    switch_version = normpath(@__DIR__, "switch_version.jl")
    @test success(pipeline(`$(Base.julia_cmd()) $switch_version`, stderr=stderr))
end
