From e8d6be5a6dbce054ee8085df705770e0f4191d07 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 4 Aug 2020 08:31:55 -0700 Subject: [PATCH] Test stale dependencies --- README.md | 3 ++ src/Aqua.jl | 3 +- src/project_extras.jl | 16 +++---- src/stale_deps.jl | 97 ++++++++++++++++++++++++++++++++++++++++ src/utils.jl | 29 ++++++++++++ test/pkgs/AquaTesting.jl | 2 + test/test_smoke.jl | 5 +++ test/test_stale_deps.jl | 68 ++++++++++++++++++++++++++++ 8 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 src/stale_deps.jl create mode 100644 test/test_stale_deps.jl diff --git a/README.md b/README.md index 5e87629b..e67296a9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Aqua.jl b/src/Aqua.jl index f842c82b..7c67415a 100644 --- a/src/Aqua.jl +++ b/src/Aqua.jl @@ -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 @@ -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) diff --git a/src/project_extras.jl b/src/project_extras.jl index 4a5e6179..8c8f91ed 100644 --- a/src/project_extras.jl +++ b/src/project_extras.jl @@ -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) diff --git a/src/stale_deps.jl b/src/stale_deps.jl new file mode 100644 index 00000000..11b7fb79 --- /dev/null +++ b/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 diff --git a/src/utils.jl b/src/utils.jl index 81d8995a..643ec719 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -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 diff --git a/test/pkgs/AquaTesting.jl b/test/pkgs/AquaTesting.jl index 23ef0923..4a93b41a 100644 --- a/test/pkgs/AquaTesting.jl +++ b/test/pkgs/AquaTesting.jl @@ -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") diff --git a/test/test_smoke.jl b/test/test_smoke.jl index 04d739ed..2768383b 100644 --- a/test/test_smoke.jl +++ b/test/test_smoke.jl @@ -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 diff --git a/test/test_stale_deps.jl b/test/test_stale_deps.jl new file mode 100644 index 00000000..70ffb63e --- /dev/null +++ b/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