diff --git a/src/audit.jl b/src/audit.jl index f0d1727f6..6a6f01282 100644 --- a/src/audit.jl +++ b/src/audit.jl @@ -1,171 +1,329 @@ +# ============================================================================ +# PETSc object leak audit +# ============================================================================ +# +# Matches object creations against destroy calls by walking the parsed syntax tree. +# Matching on source text instead would miss multi-statement lines and +# would fire inside comments and string literals. + +# Names that create an object the caller owns +# ---------------------------------------------------------------------------- + +const AUDIT_TYPE_CREATORS = + Dict(:KSP => "KSP", :SNES => "SNES", :DMDA => "DM", :DMStag => "DM", :DMPlex => "DM") + +const AUDIT_NAMED_CREATORS = Dict( + :DMGlobalVec => "Vec", + :DMLocalVec => "Vec", + :DMGetCoordinateDM => "DM", + :DMStagCreateCompatibleDMStag => "DM", + :DMCreateMatrix => "Mat", + :MatCreateVecs => "Vec", + # high-level factories, whose names carry no Create/Duplicate marker + :VecSeq => "Vec", + :MatAIJ => "Mat", + :MatShell => "Mat", + :MatSeqAIJ => "Mat", + :MatSeqDense => "Mat", +) + """ - audit_petsc_file(path::AbstractString) - -Scan a PETSc.jl source file and report PETSc object creations and destroys. - -This utility reads `path`, looks for common PETSc object creation patterns -(`KSP(...)`, `SNES(...)`, `DMDA(...)`, `DMStag(...)`, as well as -`VecCreate...`, `VecDuplicate...`, `Vec...WithArray(...)` and corresponding -`Mat...` creators via either `PETSc.` or `LibPETSc.`), plus common DM/Vec/Mat -allocators like `DMGlobalVec`, `DMLocalVec`, `DMGetCoordinateDM`, -`DMStagCreateCompatibleDMStag`, and `DMCreateMatrix`; and for explicit -`destroy(x)` calls (optionally prefixed with `PETSc.`). - -It prints three sections: - - CREATION statements: line numbers and variables assigned to created objects - - DESTROY calls: line numbers and variables destroyed - - POSSIBLY UNDESTROYED objects: variables that appear in creations but have no - matching destroy call - - FINALIZE calls: presence of `PETSc.finalize(petsclib)`; suggests adding it - if missing - -Notes: - - Heuristic only: matches common constructors and `LibPETSc` creation routines; - it does not follow control flow or scopes. - - Creations without assignment cannot be cross-checked against destroys. - - Objects produced via helpers (e.g., `similar(...)`) are not detected unless - they use known creation patterns. - -Returns `nothing`. -""" -function audit_petsc_file(path::AbstractString) - # Read full content and strip block comments (#= ... =#) before line parsing - content = read(path, String) - content = replace(content, r"(?s)#=.*?=#" => "") - lines = split(content, '\n') - - # Patterns for creation calls - type_creators = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?(KSP|SNES|DMDA|DMStag)\s*\(" - # Match common PETSc Vec/Mat creators, including suffixed forms like - # VecCreateSeq, VecCreateMPI, VecDuplicateVecs, MatCreateSeqAIJ, etc. - vec_create = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?Vec(?:Create\w*|Duplicate\w*|Load\w*|\w*WithArray)\s*\(" - mat_create = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?Mat(?:Create\w*|Duplicate\w*|Load\w*|\w*With\w*Arrays)\s*\(" - # Additional allocators that should be destroyed - dm_vec_alloc = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?(DMGlobalVec|DMLocalVec)\s*\(" - dm_alloc_dm = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?(DMGetCoordinateDM|DMStagCreateCompatibleDMStag)\s*\(" - dm_alloc_mat = r"^(?:\s*)(?:([A-Za-z_]\w*)\s*=\s*)?(?:(?:PETSc|LibPETSc)\.)?(DMCreateMatrix)\s*\(" - - # Some PETSc calls create multiple objects via tuple assignment - # e.g. `x,b = LibPETSc.MatCreateVecs(petsclib, A)` - mat_create_vecs = r"^(?:\s*)(.+?)\s*=\s*(?:(?:PETSc|LibPETSc)\.)?MatCreateVecs\s*\(" - - # Pattern for destroy calls - destroy_pat = r"^(?:\s*)(?:(?:PETSc)\.)?destroy\s*\(\s*([A-Za-z_]\w*)" - # Pattern for finalize calls - finalize_pat = r"^(?:\s*)(?:(?:PETSc)\.)?finalize\s*\(\s*([A-Za-z_]\w*)" - - created = Vector{Tuple{Int,Union{Nothing,String},String}}() - destroyed = Vector{Tuple{Int,String}}() - finalized = Vector{Tuple{Int,String}}() - - for (i, ln_raw) in enumerate(lines) - # Skip empty and full-line comments - s = strip(ln_raw) - if isempty(s) || startswith(s, "#") - continue - end - ln_proc = ln_raw - - if occursin(type_creators, ln_proc) - m = match(type_creators, ln_proc) - var = m.captures[1] - func = m.captures[2] - push!(created, (i, var, func)) - continue - end - if occursin(vec_create, ln_proc) - m = match(vec_create, ln_proc) - var = m.captures[1] - push!(created, (i, var, "Vec")) - continue - end - if occursin(mat_create, ln_proc) - m = match(mat_create, ln_proc) - var = m.captures[1] - push!(created, (i, var, "Mat")) - continue + audit_creator(name::Symbol) -> Union{Nothing, String} + +The kind of object `name` creates, or `nothing` if it creates none. Covers the +`Vec`/`Mat` families (`VecCreateSeq`, `MatDuplicate`, `MatSeqAIJWithArrays`, …) +by shape rather than by listing every member. +""" +function audit_creator(name::Symbol) + haskey(AUDIT_TYPE_CREATORS, name) && return AUDIT_TYPE_CREATORS[name] + haskey(AUDIT_NAMED_CREATORS, name) && return AUDIT_NAMED_CREATORS[name] + s = String(name) + for (prefix, kind) in (("Vec", "Vec"), ("Mat", "Mat")) + startswith(s, prefix) || continue + rest = s[(length(prefix) + 1):end] + if occursin("Create", rest) || + occursin("Duplicate", rest) || + occursin("Load", rest) || + occursin("WithArray", rest) + return kind end - if occursin(dm_vec_alloc, ln_proc) - m = match(dm_vec_alloc, ln_proc) - var = m.captures[1] - push!(created, (i, var, "Vec")) - continue + end + return nothing +end + +""" + audit_destroyer(name::Symbol) -> Bool + +Whether `name` releases a PETSc object. Covers the high-level `destroy`/`destroy!` +and the low-level `VecDestroy`, `MatDestroy`, `DMDestroy` and friends, which the +previous text-matching version treated as leaks. + +`finalizer` counts too. `finalizer(destroy, v)` hands the release to the garbage +collector rather than performing it, but the object is accounted for and must not +read as a leak. The package uses that idiom for sequential objects. +""" +function audit_destroyer(name::Symbol) + name in (:destroy, :destroy!, :finalizer) && return true + s = String(name) + return endswith(s, "Destroy") && length(s) > length("Destroy") +end + +# Syntax tree helpers +# ———————————————————————————————————————————————————————————————————————————— + +""" + audit_isbroadcast(ex) -> Bool + +Whether `ex` is a dot-call such as `destroy!.(vs)`. Those parse as `Expr(:.)` +with a tuple of arguments, not as `Expr(:call)`, so they need recognising +separately. Plain field access `a.b` carries a `QuoteNode` instead and is not a +call. +""" +audit_isbroadcast(ex) = + ex isa Expr && + ex.head === :. && + length(ex.args) == 2 && + ex.args[2] isa Expr && + ex.args[2].head === :tuple + +""" + audit_callee(ex) -> Union{Nothing, Symbol} + +The bare name a call expression invokes, discarding any module qualification, so +`f(x)`, `PETSc.f(x)`, `PETSc.LibPETSc.f(x)` and `PETSc.f.(xs)` all yield `:f`. +""" +function audit_callee(ex) + ex isa Expr || return nothing + if ex.head === :call + f = ex.args[1] + elseif audit_isbroadcast(ex) + f = ex.args[1] + else + return nothing + end + f isa Symbol && return f + while f isa Expr && f.head === :. + f = f.args[2] + end + f isa QuoteNode && (f = f.value) + return f isa Symbol ? f : nothing +end + +""" + audit_argnames(ex) -> Vector{Symbol} + +The plain names a call passes, for either call form, looking through splats and +one level of array or tuple literal. `destroy!(v)`, `destroy!(v...)` and +`destroy!.([v, w])` all name the objects they release. +""" +function audit_argnames(ex) + args = if ex.head === :call + ex.args[2:end] + elseif audit_isbroadcast(ex) + ex.args[2].args + else + return Symbol[] + end + + names = Symbol[] + for a in args + if a isa Expr && a.head === :... + a = a.args[1] end - if occursin(dm_alloc_dm, ln_proc) - m = match(dm_alloc_dm, ln_proc) - var = m.captures[1] - push!(created, (i, var, "DM")) - continue + if a isa Symbol + push!(names, a) + elseif a isa Expr && a.head in (:vect, :tuple) + append!(names, (x for x in a.args if x isa Symbol)) end - if occursin(dm_alloc_mat, ln_proc) - m = match(dm_alloc_mat, ln_proc) - var = m.captures[1] - push!(created, (i, var, "Mat")) - continue + end + return names +end + +""" + audit_targets(lhs) -> Vector{Symbol} + +The variables an assignment binds. Handles `x = …`, `x, y = …` and `(x, y) = …`, +skipping anything that is not a plain name. +""" +function audit_targets(lhs) + lhs isa Symbol && return [lhs] + if lhs isa Expr && lhs.head in (:tuple, :block) + return Symbol[a for a in lhs.args if a isa Symbol] + end + return Symbol[] +end + +""" + audit_walk(f, ex, line = 0) -> Int + +Apply `f(expr, line)` to every `Expr` in `ex`, tracking the source line from the +`LineNumberNode`s the parser emits. Returns the last line seen. +""" +function audit_walk(f, ex, line::Int = 0) + if ex isa LineNumberNode + return ex.line + elseif ex isa Expr + # Quoted code is data, not execution. A `destroy!(v)` in a macro body + # releases nothing where it is written, and counting it would report a + # genuinely leaking file as clean. + ex.head === :quote && return line + f(ex, line) + for a in ex.args + line = audit_walk(f, a, line) end + end + return line +end + +""" + audit_hasparseerror(ex) -> Bool + +Whether the parsed tree contains an error or incomplete node, which happens when +the file has a syntax error. `Meta.parseall` reports those in the tree rather +than throwing. +""" +function audit_hasparseerror(ex) + ex isa Expr || return false + ex.head in (:error, :incomplete) && return true + return any(audit_hasparseerror, ex.args) +end + +# ———————————————————————————————————————————————————————————————————————————— + +""" + audit_petsc_file(path::AbstractString; verbose::Bool = true) + +Scan a Julia source file for PETSc objects that are created but never destroyed. + +Creations are calls to a type constructor (`KSP`, `SNES`, `DMDA`, `DMStag`, +`DMPlex`), to a `Vec`/`Mat` creation routine (`VecCreateSeq`, `MatDuplicate`, +`MatSeqAIJWithArrays`, …), or to one of the DM allocators (`DMGlobalVec`, +`DMLocalVec`, `DMCreateMatrix`, …), through either `PETSc` or `LibPETSc`. +Releases are `destroy`/`destroy!` and the low-level `VecDestroy`-style routines. + +Returns a `NamedTuple`: + +- `created`: `(line, var, kind)` for each creation, `var === nothing` when the + result is not assigned +- `destroyed`: `(line, var)` for each release +- `finalized`: `(line, var)` for each `finalize` call +- `leaked`: variables that are created and never released + +Pass `verbose = false` to suppress the printed report. + +# Notes + +Heuristic: it does not follow control flow, scopes, or aliasing, and creations +whose result is not assigned cannot be matched against a release. + +# Examples + +```julia +julia> report = audit_petsc_file("examples/ex1.jl"); - if occursin(mat_create_vecs, ln_proc) - m = match(mat_create_vecs, ln_proc) - lhs = m.captures[1] - # Strip parentheses and split on commas: "(x, b)" or "x,b" etc. - lhs = replace(lhs, "(" => "", ")" => "") - vars = [strip(v) for v in split(lhs, ',') if !isempty(strip(v))] - if isempty(vars) - push!(created, (i, nothing, "Vec")) - else - for v in vars - if occursin(r"^[A-Za-z_]\w*$", v) - push!(created, (i, v, "Vec")) +julia> isempty(report.leaked) +true +``` +""" +function audit_petsc_file(path::AbstractString; verbose::Bool = true) + ast = Meta.parseall(read(path, String); filename = path) + + # A file that does not parse yields no creations, which would otherwise be + # reported as "no leaks" and read as a clean bill of health. + if audit_hasparseerror(ast) + @warn "$(path) does not parse; the audit below covers only what could be read" + end + + created = Tuple{Int, Union{Nothing, Symbol}, String}[] + destroyed = Tuple{Int, Symbol}[] + finalized = Tuple{Int, Symbol}[] + assigned = Base.IdSet{Any}() + + audit_walk(ast) do ex, line + if ex.head === :(=) && ex.args[2] isa Expr + rhs = ex.args[2] + callee = audit_callee(rhs) + if callee !== nothing + kind = audit_creator(callee) + if kind !== nothing + push!(assigned, rhs) + targets = audit_targets(ex.args[1]) + if isempty(targets) + push!(created, (line, nothing, kind)) + else + for t in targets + push!(created, (line, t, kind)) + end end end end - continue + return end - if occursin(destroy_pat, ln_proc) - m = match(destroy_pat, ln_proc) - var = m.captures[1] - push!(destroyed, (i, var)) - continue + callee = audit_callee(ex) + callee === nothing && return + + if audit_creator(callee) !== nothing && !(ex in assigned) + push!(created, (line, nothing, audit_creator(callee))) end - if occursin(finalize_pat, ln_proc) - m = match(finalize_pat, ln_proc) - var = m.captures[1] - push!(finalized, (i, var)) - continue + + # The object is the first argument high-level (`destroy!(v)`) but the + # second low-level (`VecDestroy(petsclib, v)`), so record every plain + # name the call mentions. `petsclib` is never a tracked object, so the + # extra entries cannot mask a leak. + if audit_destroyer(callee) || callee === :finalize + sink = callee === :finalize ? finalized : destroyed + for a in audit_argnames(ex) + push!(sink, (line, a)) + end end end + created_vars = Set{Symbol}(v for (_, v, _) in created if v !== nothing) + released = Set{Symbol}(v for (_, v) in destroyed) + leaked = sort!(collect(setdiff(created_vars, released))) + + # Only report releases of objects this file created; the rest are arguments + # that happened to sit in a destroy call, such as `petsclib`. + filter!(((_, v),) -> v in created_vars, destroyed) + + verbose && audit_report(created, destroyed, finalized, leaked) + + return ( + created = created, + destroyed = destroyed, + finalized = finalized, + leaked = leaked, + ) +end + +""" + audit_report(created, destroyed, finalized, leaked) + +Print the human-readable form of an [`audit_petsc_file`](@ref) result. +""" +function audit_report(created, destroyed, finalized, leaked) println("CREATION statements:") - for (lnum, var, func) in created - if isnothing(var) - println(" line $(lnum): $(func)(...) (no assignment)") + for (line, var, kind) in created + if var === nothing + println(" line $(line): $(kind)(...) (no assignment)") else - println(" line $(lnum): $(var) = $(func)(...)") + println(" line $(line): $(var) = $(kind)(...)") end end println("DESTROY calls:") - for (lnum, var) in destroyed - println(" line $(lnum): destroy($(var))") + for (line, var) in destroyed + println(" line $(line): destroy($(var))") end - created_vars = Set{String}() - for (_, var, _) in created - if !isnothing(var) - push!(created_vars, var::String) - end - end - destroyed_vars = Set{String}(map(x -> x[2], destroyed)) - - missing = setdiff(created_vars, destroyed_vars) - if !isempty(missing) + if isempty(leaked) + println("No obvious leaks: all assigned creations have a destroy call.") + else println("POSSIBLY UNDESTROYED objects:") - for v in sort(collect(missing)) + for v in leaked println(" $(v)") end - else - println("No obvious leaks: all assigned creations have a destroy call.") end if isempty(finalized) @@ -173,8 +331,8 @@ function audit_petsc_file(path::AbstractString) println("Suggestion: add PETSc.finalize(petsclib) at the end of the routine.") else println("FINALIZE calls:") - for (lnum, var) in finalized - println(" line $(lnum): finalize($(var))") + for (line, var) in finalized + println(" line $(line): finalize($(var))") end end diff --git a/test/fixtures/constructs.jl b/test/fixtures/constructs.jl new file mode 100644 index 000000000..f999a5668 --- /dev/null +++ b/test/fixtures/constructs.jl @@ -0,0 +1,81 @@ +# Fixture for test_audit.jl. Never executed, only parsed. +# +# Every construct here has a known answer. Only `never_freed` and `quoted_v` +# leak; everything else is created and released, or is not a tracked creation. + +using PETSc + +# released inside a loop +for i in 1:3 + loop_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + LibPETSc.VecDestroy(petsclib, loop_v) +end + +# released in a finally block +function in_try() + try_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + try + use(try_v) + finally + PETSc.destroy!(try_v) + end +end + +# created on both branches, released once +if cond + branch_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +else + branch_v = LibPETSc.VecCreateSeq(petsclib, comm, 20) +end +PETSc.destroy!(branch_v) + +# creation wrapped in a macro +@time macro_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!(macro_v) + +# released by broadcasting over a literal +bcast_a = LibPETSc.VecCreateSeq(petsclib, comm, 10) +bcast_b = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!.([bcast_a, bcast_b]) + +# released through a splat +splat_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!(splat_v...) + +# release call carrying a keyword argument +kwarg_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!(kwarg_v; force = true) + +# handed to the garbage collector, in both finalizer spellings +fin_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +finalizer(destroy, fin_v) +fin_m = PETSc.MatSeqAIJ(petsclib, 10, 10, 3) +finalizer(m -> (destroy(m); data), fin_m) + +# borrowed pointer: the caller does not own it +borrowed = PETSc.VecPtr(petsclib, some_ptr, false) + +# let block, global binding, and a non-ASCII name +let + let_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + PETSc.destroy!(let_v) +end +global global_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!(global_v) +Δv = LibPETSc.VecCreateSeq(petsclib, comm, 10) +PETSc.destroy!(Δv) + +#= +block_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) +=# + +# the release below is quoted, so it releases nothing here +macro cleanup() + return quote + PETSc.destroy!(quoted_v) + end +end +quoted_v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + +# the control: nothing releases this +never_freed = LibPETSc.VecCreateSeq(petsclib, comm, 10) diff --git a/test/fixtures/leaky.jl b/test/fixtures/leaky.jl new file mode 100644 index 000000000..4dc7ea7fa --- /dev/null +++ b/test/fixtures/leaky.jl @@ -0,0 +1,33 @@ +# Fixture for test_audit.jl. Never executed, only parsed. +# +# Exercises what the auditor has to get right: qualified and unqualified calls, +# tuple assignment, several statements on one line, low-level destroys, and +# creations that appear only inside comments or string literals. + +using PETSc + +function leaky(petsclib, comm) + # created and destroyed through the high-level name + ksp = PETSc.KSP(petsclib, comm) + PETSc.destroy!(ksp) + + # created and destroyed through the low-level name + v1 = LibPETSc.VecCreateSeq(petsclib, comm, 10) + LibPETSc.VecDestroy(petsclib, v1) + + # two statements on one line: both must register + v2 = LibPETSc.VecCreateSeq(petsclib, comm, 10); LibPETSc.VecDestroy(petsclib, v2) + + # tuple assignment creates two objects, only one is released + x, b = LibPETSc.MatCreateVecs(petsclib, A) + LibPETSc.VecDestroy(petsclib, x) + + # never destroyed + dm = PETSc.DMStag(petsclib, comm, (PETSc.DM_BOUNDARY_NONE,), (10,), 1, 1) + mat = PETSc.MatSeqAIJ(petsclib, 10, 10, 3) + + # a comment mentioning PETSc.destroy!(dm) must not count as a release + note = "call LibPETSc.MatDestroy(petsclib, mat) when finished" + + return note +end diff --git a/test/runtests.jl b/test/runtests.jl index b3c6a6f93..7e812d551 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -43,6 +43,7 @@ include("dmproduct.jl") # test for DMProduct example include("matshell.jl") # autowrapped! include("test_dmstag.jl") include("test_snes.jl") +include("test_audit.jl") # leak auditor include("test_errors.jl") # argument validation include("old_test.jl") include("low_level_viewer.jl") # Low-level viewer convenience functions diff --git a/test/test_audit.jl b/test/test_audit.jl new file mode 100644 index 000000000..2d615b6c9 --- /dev/null +++ b/test/test_audit.jl @@ -0,0 +1,136 @@ +using Test +using PETSc + +const LEAKY = joinpath(@__DIR__, "fixtures", "leaky.jl") +const CONSTRUCTS = joinpath(@__DIR__, "fixtures", "constructs.jl") + +@testset "audit_petsc_file" begin + report = PETSc.audit_petsc_file(LEAKY; verbose = false) + + created = Set(v for (_, v, _) in report.created if v !== nothing) + destroyed = Set(v for (_, v) in report.destroyed) + + @testset "creations" begin + # high-level constructor, low-level creator, and tuple assignment + for v in (:ksp, :v1, :v2, :x, :b, :dm, :mat) + @test v in created + end + end + + @testset "releases" begin + # both PETSc.destroy! and LibPETSc.VecDestroy count + for v in (:ksp, :v1, :v2, :x) + @test v in destroyed + end + end + + @testset "leaks" begin + @test report.leaked == [:b, :dm, :mat] + end + + @testset "two statements on one line" begin + # v2 is created and destroyed on the same source line; the previous + # text-matching implementation stopped after the first match + line = only(l for (l, v, _) in report.created if v === :v2) + @test any(report.destroyed) do (l, v) + v === :v2 && l == line + end + end + + @testset "comments and strings are not code" begin + # the fixture mentions destroy!(dm) in a comment and MatDestroy(mat) + # inside a string; neither may register as a release + @test !(:dm in destroyed) + @test !(:mat in destroyed) + end + + @testset "finalizer counts as a release" begin + mktemp() do path, io + write( + io, + """ + v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + finalizer(destroy, v) + + mat = PETSc.MatSeqAIJ(petsclib, 10, 10, 3) + finalizer(m -> (destroy(m); data), mat) + + dm = PETSc.DMStag(petsclib, comm, bt, sz, 1, 1) + """, + ) + close(io) + # v and mat are handed to the garbage collector; only dm leaks + @test PETSc.audit_petsc_file(path; verbose = false).leaked == [:dm] + end + end + + @testset "borrowed pointers are not creations" begin + mktemp() do path, io + # VecPtr/MatPtr wrap a handle the caller may not own, so they must + # not be reported as objects needing a destroy + write(io, "f = PETSc.VecPtr(petsclib, f_ptr, false)\n") + close(io) + @test isempty(PETSc.audit_petsc_file(path; verbose = false).leaked) + end + end + + @testset "clean file has no leaks" begin + mktemp() do path, io + write( + io, + """ + v = LibPETSc.VecCreateSeq(petsclib, comm, 10) + LibPETSc.VecDestroy(petsclib, v) + """, + ) + close(io) + @test isempty(PETSc.audit_petsc_file(path; verbose = false).leaked) + end + end + + @testset "language constructs" begin + # Every construct in the fixture is released except the two named here. + # A failure names the construct that broke. + report = PETSc.audit_petsc_file(CONSTRUCTS; verbose = false) + @test report.leaked == [:never_freed, :quoted_v] + end + + @testset "does not parse" begin + mktemp() do path, io + write(io, "v = LibPETSc.VecCreateSeq(petsclib, comm, 10\nfunction broken(\n") + close(io) + # a syntax error must not silently read as a clean bill of health + @test_logs (:warn, r"does not parse") PETSc.audit_petsc_file( + path; + verbose = false, + ) + end + end + + @testset "helpers" begin + @test PETSc.audit_creator(:VecCreateSeq) == "Vec" + @test PETSc.audit_creator(:MatSeqAIJWithArrays) == "Mat" + @test PETSc.audit_creator(:DMStag) == "DM" + @test PETSc.audit_creator(:solve!) === nothing + + @test PETSc.audit_destroyer(:destroy) + @test PETSc.audit_destroyer(:destroy!) + @test PETSc.audit_destroyer(:VecDestroy) + @test PETSc.audit_destroyer(:finalizer) + @test PETSc.audit_creator(:VecPtr) === nothing + @test PETSc.audit_creator(:MatPtr) === nothing + @test !PETSc.audit_destroyer(:Destroy) + @test !PETSc.audit_destroyer(:assemble!) + + @test PETSc.audit_callee(:(PETSc.LibPETSc.f(x))) === :f + @test PETSc.audit_callee(:(PETSc.f.(xs))) === :f + @test !PETSc.audit_isbroadcast(:(a.b)) + @test PETSc.audit_isbroadcast(:(f.(x))) + @test PETSc.audit_argnames(:(destroy!(v))) == [:v] + @test PETSc.audit_argnames(:(destroy!(v...))) == [:v] + @test PETSc.audit_argnames(:(destroy!.([v, w]))) == [:v, :w] + @test PETSc.audit_argnames(:(VecDestroy(lib, v))) == [:lib, :v] + @test PETSc.audit_callee(:(f(x))) === :f + @test PETSc.audit_callee(:(x + 1)) === :+ + end +end