Skip to content

Commit

Permalink
Test stale dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
tkf committed Aug 4, 2020
1 parent 366a1ff commit e8d6be5
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 12 deletions.
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -13,6 +13,9 @@ Aqua.jl provides functions to run a few automatable checks for Julia packages:
* There are no method ambiguities.
* There are no undefined `export`s.
* There are no unbound type parameters.
* There are no stale dependencies listed in `Project.toml` (optional).
* Check that test target of the root project `Project.toml` and test project
(`test/Project.toml`) are consistent (optional).

## Quick usage

Expand Down
3 changes: 2 additions & 1 deletion src/Aqua.jl
Expand Up @@ -5,7 +5,7 @@ module Aqua
replace(read(path, String), "```julia" => "```jldoctest")
end Aqua

using Base: PkgId
using Base: PkgId, UUID
using Pkg: TOML
using Test

Expand All @@ -14,6 +14,7 @@ include("ambiguities.jl")
include("unbound_args.jl")
include("exports.jl")
include("project_extras.jl")
include("stale_deps.jl")

"""
test_all(testtarget::Module)
Expand Down
16 changes: 5 additions & 11 deletions src/project_extras.jl
Expand Up @@ -25,18 +25,12 @@ analyze_project_extras(packages) = map(_analyze_project_extras, aspkgids(package

function _analyze_project_extras(pkg::PkgId)
label = string(pkg)

result = root_project_or_failed_lazytest(pkg)
result isa LazyTestResult && return result
root_project_path = result

pkgpath = dirname(dirname(Base.locate_package(pkg)))
root_project_path, found = project_toml_path(pkgpath)
if !found
return LazyTestResult(
label,
"""
Project.toml file at project directory does not exist:
$root_project_path
""",
false,
)
end
test_project_path, found = project_toml_path(joinpath(pkgpath, "test"))
if !found
return LazyTestResult(label, "test/Project.toml file does not exist.", true)
Expand Down
97 changes: 97 additions & 0 deletions src/stale_deps.jl
@@ -0,0 +1,97 @@
"""
Aqua.test_stale_deps(package; [ignore])
Test that `package` loads all dependencies listed in `Project.toml`.
!!! note "Known bug"
Currently, `Aqua.test_stale_deps` does not detect stale
dependencies when they are stdlib. This is considered a bug and
may be fixed in the future. Such a release is considered
non-breaking.
# Arguments
- `packages`: a top-level `Module`, a `Base.PkgId`, or a collection of
them.
# Keyword Arguments
- `ignore::Vector{Symbol}`: names of dependent packages to be ignored.
"""
test_stale_deps
function test_stale_deps(packages; kwargs...)
@testset "$(result.label)" for result in analyze_stale_deps(packages, kwargs)
@debug result.label result
@test result true
end
end

analyze_stale_deps(packages; kwargs...) = analyze_stale_deps(packages, kwargs)
analyze_stale_deps(packages, kwargs) =
[_analyze_stale_deps_1(pkg; kwargs...) for pkg in aspkgids(packages)]

function _analyze_stale_deps_1(pkg::PkgId; ignore::AbstractArray{Symbol} = Symbol[])
label = "$pkg"

result = root_project_or_failed_lazytest(pkg)
result isa LazyTestResult && return result
root_project_path = result

@debug "Parsing `$root_project_path`"
deps = [PkgId(UUID(v), k) for (k, v) in TOML.parsefile(root_project_path)["deps"]]

code = """
$(Base.load_path_setup_code())
Base.require($(reprpkgid(pkg)))
for pkg in keys(Base.loaded_modules)
pkg.uuid === nothing || println(pkg.uuid)
end
"""
cmd = Base.julia_cmd()
output = read(`$cmd --startup-file=no --color=no -e $code`, String)
@debug("Checked modules loaded in a separate process.", cmd, Text(code), Text(output))
loaded_uuids = map(UUID, eachline(IOBuffer(output)))

return _analyze_stale_deps_2(;
pkg = pkg,
deps = deps,
loaded_uuids = loaded_uuids,
ignore = ignore,
)
end

# Side-effect -free part of stale dependency analysis.
function _analyze_stale_deps_2(;
pkg::PkgId,
deps::AbstractArray{PkgId},
loaded_uuids::AbstractArray{UUID},
ignore::AbstractArray{Symbol},
)
label = "$pkg"
deps_uuids = [p.uuid for p in deps]
pkgid_from_uuid = Dict(p.uuid => p for p in deps)

stale_uuids = setdiff(deps_uuids, loaded_uuids)
stale_pkgs = [pkgid_from_uuid[uuid] for uuid in stale_uuids]
stale_pkgs = [p for p in stale_pkgs if !(Symbol(p.name) in ignore)]

if isempty(stale_pkgs)
return LazyTestResult(
label,
"""
All packages in `deps` are loaded via `using $(pkg.name)`.
""",
true,
)
end

stale_msg = join(("* $p" for p in stale_pkgs), "\n")
msglines = [
"Some package(s) in `deps` of $pkg are not loaded during via" *
" `using $(pkg.name)`.",
stale_msg,
"",
"To ignore from stale dependency detection, pass the package name to" *
" `ignore` keyword argument of `Aqua.test_stale_deps`",
]
return LazyTestResult(label, join(msglines, "\n"), false)
end
29 changes: 29 additions & 0 deletions src/utils.jl
Expand Up @@ -27,3 +27,32 @@ function Base.show(io::IO, ::MIME"text/plain", result::LazyTestResult)
println(io, " "^4, line)
end
end

function root_project_or_failed_lazytest(pkg::PkgId)
label = "$pkg"

srcpath = Base.locate_package(pkg)
if srcpath === nothing
return LazyTestResult(
label,
"""
Package $pkg does not have a corresponding source file.
""",
false,
)
end

pkgpath = dirname(dirname(srcpath))
root_project_path, found = project_toml_path(pkgpath)
if !found
return LazyTestResult(
label,
"""
Project.toml file at project directory does not exist:
$root_project_path
""",
false,
)
end
return root_project_path
end
2 changes: 2 additions & 0 deletions test/pkgs/AquaTesting.jl
Expand Up @@ -31,6 +31,8 @@ const SAMPLE_PKGIDS = [
PkgId(nothing, "PkgWithoutProject"),
]

const SAMPLE_PKG_BY_NAME = Dict(pkg.name => pkg for pkg in SAMPLE_PKGIDS)

function with_sample_pkgs(f)
sampledir = joinpath(@__DIR__, "sample")

Expand Down
5 changes: 5 additions & 0 deletions test/test_smoke.jl
Expand Up @@ -3,4 +3,9 @@ module TestSmoke
using Aqua
Aqua.test_all(Aqua)

using Test
@testset "test_stale_deps" begin
Aqua.test_stale_deps(Aqua)
end

end # module
68 changes: 68 additions & 0 deletions test/test_stale_deps.jl
@@ -0,0 +1,68 @@
module TestStaleDeps

include("preamble.jl")
using Aqua: PkgId, UUID, _analyze_stale_deps_2, ispass,

@testset "_analyze_stale_deps_2" begin
pkg = PkgId(UUID(42), "TargetPkg")

dep1 = PkgId(UUID(1), "Dep1")
dep2 = PkgId(UUID(2), "Dep2")
dep3 = PkgId(UUID(3), "Dep3")

@testset "pass" begin
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[],
loaded_uuids = UUID[],
ignore = Symbol[],
) true
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[dep1],
loaded_uuids = UUID[dep1.uuid, dep2.uuid, dep3.uuid],
ignore = Symbol[],
) true
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[dep1],
loaded_uuids = UUID[dep2.uuid, dep3.uuid],
ignore = Symbol[:Dep1],
) true
end
@testset "failure" begin
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[dep1],
loaded_uuids = UUID[],
ignore = Symbol[],
) false
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[dep1],
loaded_uuids = UUID[dep2.uuid, dep3.uuid],
ignore = Symbol[],
) false
@test _analyze_stale_deps_2(;
pkg = pkg,
deps = PkgId[dep1, dep2],
loaded_uuids = UUID[dep3.uuid],
ignore = Symbol[:Dep1],
) false
end
end

with_sample_pkgs() do
@testset "PkgWithoutProject" begin
pkg = AquaTesting.SAMPLE_PKG_BY_NAME["PkgWithoutProject"]
results = Aqua.analyze_stale_deps(pkg)
@test length(results) == 1
r, = results
@test !ispass(r)
@test r false
msg = sprint(show, "text/plain", r)
@test occursin("Project.toml file at project directory does not exist", msg)
end
end

end # module

0 comments on commit e8d6be5

Please sign in to comment.