Skip to content

Commit 28d29dc

Browse files
lgoettgenshyrodium
andauthored
Overhaul documentation (#250)
* Small docs adjustments * Split docs into one file per test * Enable Documenter checks * Add more docs for `ambiguities` * Move parts of docstring of `test_persistent_tasks` to docs * Add docs for `deps_compat` * Move parts of docstring of `test_unbound_args` to docs * Add docs for `project_extras` * Add docs for `piracies` * Add more docs * Add changelgog * Apply suggestions from code review Co-authored-by: Yuto Horikawa <[email protected]> * Apply suggestions from code review * Update docs/src/unbound_args.md * Update docs/src/unbound_args.md --------- Co-authored-by: Yuto Horikawa <[email protected]>
1 parent ce2a7d6 commit 28d29dc

18 files changed

+266
-102
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
1011
## [0.8.3] - 2023-11-29
1112

12-
## Changed
13+
### Changed
1314

1415
- `test_persistent_tasks` is now less noisy. ([#256](https://github.com/JuliaTesting/Aqua.jl/pull/256))
16+
- Completely overhauled the documentation. Every test now has its dedicated page. ([#250](https://github.com/JuliaTesting/Aqua.jl/pull/250))
1517

1618

1719
## [0.8.2] - 2023-11-16

docs/make.jl

+14-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,26 @@ using Documenter, Aqua
22

33
makedocs(;
44
modules = [Aqua],
5-
pages = ["Home" => "index.md"],
5+
pages = [
6+
"Home" => "index.md",
7+
"Tests" => [
8+
"test_all.md",
9+
"ambiguities.md",
10+
"unbound_args.md",
11+
"exports.md",
12+
"project_extras.md",
13+
"stale_deps.md",
14+
"deps_compat.md",
15+
"piracies.md",
16+
"persistent_tasks.md",
17+
],
18+
],
619
sitename = "Aqua.jl",
720
format = Documenter.HTML(;
821
repolink = "https://github.com/JuliaTesting/Aqua.jl",
922
assets = ["assets/favicon.ico"],
1023
),
1124
authors = "Takafumi Arakaki",
12-
warnonly = true,
1325
)
1426

1527
deploydocs(; repo = "github.com/JuliaTesting/Aqua.jl", push_preview = true)

docs/src/ambiguities.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Ambiguities
2+
3+
Method ambiguities are cases where multiple methods are applicable to a given set of arguments, without having a most specific method.
4+
5+
## Examples
6+
One easy example is the following:
7+
```@repl
8+
f(x::Int, y::Integer) = 1
9+
f(x::Integer, y::Int) = 2
10+
11+
println(f(1, 2))
12+
```
13+
This will throw an `MethodError` because both methods are equally specific. The solution is to add a third method:
14+
```julia
15+
f(x::Int, y::Int) = ? # `?` is dependent on the use case, most times it will be `1` or `2`
16+
```
17+
18+
## [Test function](@id test_ambiguities)
19+
20+
```@docs
21+
Aqua.test_ambiguities
22+
```

docs/src/deps_compat.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compat entries
2+
3+
In your `Project.toml` you can (and should) use compat entries to specify
4+
with which versions of Julia and your dependencies your package is compatible with.
5+
This is important to ease the installation and upgrade of your package for users,
6+
and to keep everything working in the case of breaking changes in Julia or your dependencies.
7+
8+
For more details, see the [Pkg docs](https://julialang.github.io/Pkg.jl/v1/compatibility/).
9+
10+
## [Test function](@id test_deps_compat)
11+
12+
```@docs
13+
Aqua.test_deps_compat
14+
```

docs/src/exports.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Undefined exports
2+
3+
## [Test function](@id test_undefined_exports)
4+
5+
```@docs
6+
Aqua.test_undefined_exports
7+
```

docs/src/index.md

+3-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# Aqua.jl:
2-
## *A*uto *QU*ality *A*ssurance for Julia packages
1+
# Aqua.jl: *A*uto *QU*ality *A*ssurance for Julia packages
32

43
Aqua.jl provides functions to run a few automatable checks for Julia packages:
54

@@ -84,20 +83,10 @@ end
8483
```
8584
Note, that for all tests with no explicit options provided, the default options are used.
8685

87-
For more details on the options, see the respective functions [below](@ref test_functions).
86+
For more details on the options, see the respective functions [here](@ref test_all).
8887

89-
### Example uses
88+
## Examples
9089
The following is a small selection of packages that use Aqua.jl:
9190
- [GAP.jl](https://github.com/oscar-system/GAP.jl)
9291
- [Hecke.jl](https://github.com/thofma/Hecke.jl)
9392
- [Oscar.jl](https://github.com/oscar-system/Oscar.jl)
94-
95-
## [Test functions](@id test_functions)
96-
```@docs
97-
Aqua.test_all
98-
```
99-
100-
```@autodocs
101-
Modules = [Aqua]
102-
Filter = t -> (startswith(String(nameof(t)), "test_") && t != Aqua.test_all) || t == Aqua.find_persistent_tasks_deps
103-
```

docs/src/persistent_tasks.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Persistent Tasks
2+
3+
## Motivation
4+
5+
Julia 1.10 and higher wait for all running `Task`s to finish
6+
before writing out the precompiled (cached) version of the package.
7+
One consequence is that a package that launches
8+
`Task`s in its `__init__` function may precompile successfully,
9+
but block precompilation of any packages that depend on it.
10+
11+
## Example
12+
13+
Let's create a dummy package, `PkgA`, that launches a persistent `Task`:
14+
15+
```julia
16+
module PkgA
17+
const t = Ref{Any}() # to prevent the Timer from being garbage-collected
18+
__init__() = t[] = Timer(0.1; interval=1) # create a persistent `Timer` `Task`
19+
end
20+
```
21+
22+
`PkgA` will precompile successfully, because `PkgA.__init__()` does not
23+
run when `PkgA` is precompiled. However,
24+
25+
```julia
26+
module PkgB
27+
using PkgA
28+
end
29+
```
30+
31+
fails to precompile: `using PkgA` runs `PkgA.__init__()`, which
32+
leaves the `Timer` `Task` running, and that causes precompilation
33+
of `PkgB` to hang.
34+
35+
## How the test works
36+
37+
This test works by launching a Julia process that tries to precompile a
38+
dummy package similar to `PkgB` above, modified to signal back to Aqua when
39+
`PkgA` has finished loading. The test fails if the gap between loading `PkgA`
40+
and finishing precompilation exceeds time `tmax`.
41+
42+
## How to fix failing packages
43+
44+
Often, the easiest fix is to modify the `__init__` function to check whether the
45+
Julia process is precompiling some other package; if so, don't launch the
46+
persistent `Task`s.
47+
48+
```julia
49+
function __init__()
50+
# Other setup code here
51+
if ccall(:jl_generating_output, Cint, ()) == 0 # if we're not precompiling...
52+
# launch persistent tasks here
53+
end
54+
end
55+
```
56+
57+
In more complex cases, you may need to set up independently-callable functions
58+
to launch the tasks and set conditions that allow them to cleanly exit.
59+
60+
## [Test functions](@id test_persistent_tasks)
61+
62+
```@docs
63+
Aqua.test_persistent_tasks
64+
Aqua.find_persistent_tasks_deps
65+
```

docs/src/piracies.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Type piracy
2+
3+
Type piracy is a term used to describe adding methods to a foreign function
4+
with only foreign arguments.
5+
This is considered bad practice because it can cause unexpected behavior
6+
when the function is called, in particular, it can change the behavior of
7+
one of your dependencies depending on if your package is loaded or not.
8+
This makes it hard to reason about the behavior of your code, and may
9+
introduce bugs that are hard to track down.
10+
11+
See [Julia documentation](https://docs.julialang.org/en/v1/manual/style-guide/#Avoid-type-piracy) for more information about type piracy.
12+
13+
## Examples
14+
15+
Say that `PkgA` is foreign, and let's look at the different ways that `PkgB` extends its function `bar`.
16+
17+
```julia
18+
module PkgA
19+
struct C end
20+
bar(x::C) = 42
21+
bar(x::Vector) = 43
22+
end
23+
24+
module PkgB
25+
import PkgA: bar, C
26+
struct D end
27+
bar(x::C) = 1
28+
bar(xs::D...) = 2
29+
bar(x::Vector{<:D}) = 3
30+
bar(x::Vector{D}) = 4 # slightly bad (may cause invalidations)
31+
bar(x::Union{C,D}) = 5 # slightly bad (a change in PkgA may turn it into piracy)
32+
# (for example changing bar(x::C) = 1 to bar(x::Union{C,Int}) = 1)
33+
end
34+
```
35+
36+
The following cases are enumerated by the return values in the example above:
37+
1. This is the worst case of type piracy. The value of `bar(C())` can be
38+
either `1` or `42` and will depend on whether `PkgB` is loaded or not.
39+
2. This is also a bad case of type piracy. `bar()` throws a `MethodError` with
40+
only `PkgA` available, and returns `2` with `PkgB` loaded. `PkgA` may add
41+
a method for `bar()` that takes no arguments in the future, and then this
42+
is equivalent to case 1.
43+
3. This is a moderately bad case of type piracy. `bar(Union{}[])` returns `3`
44+
when `PkgB` is loaded, and `43` when `PkgB` is not loaded, although neither
45+
of the occurring types are defined in `PkgB`. This case is not as bad as
46+
cases 1 and 2, because it is only about behavior around `Union{}`, which has
47+
no instances.
48+
4. Depending on ones understanding of type piracy, this could be considered piracy
49+
as well. In particular, this may cause invalidations.
50+
5. This is a slightly bad case of type piracy. In the current form, `bar(C())`
51+
returns `42` as the dispatch on `Union{C,D}` is less specific. However, a
52+
future change in `PkgA` may change this behavior, e.g. by changing `bar(x::C)`
53+
to `bar(x::Union{C,Int})` the call `bar(C())` would become ambiguous.
54+
55+
!!! note
56+
The test function below currently only checks for cases 1 and 2.
57+
58+
## [Test function](@id test_piracies)
59+
60+
```@docs
61+
Aqua.test_piracies
62+
```

docs/src/project_extras.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Project.toml extras
2+
3+
There are two different ways to specify test-only dependencies (see [the Pkg docs](https://julialang.github.io/Pkg.jl/v1/creating-packages/#Test-specific-dependencies)):
4+
1. Add the test-only dependencies to the `[extras]` section of your `Project.toml` file
5+
and use a test target.
6+
2. Add the test-only dependencies to the `[deps]` section of your `test/Project.toml` file.
7+
This is only available in Julia 1.2 and later.
8+
9+
This test checks checks that, in case you use both methods, the set of test-only dependencies
10+
is the same in both ways.
11+
12+
## [Test function](@id test_project_extras)
13+
14+
```@docs
15+
Aqua.test_project_extras
16+
```

docs/src/stale_deps.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Stale dependencies
2+
3+
## [Test function](@id test_stale_deps)
4+
5+
```@docs
6+
Aqua.test_stale_deps
7+
```

docs/src/test_all.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# [Test everything](@id test_all)
2+
3+
This test runs most of the other tests in this module.
4+
The defaults should be fine for most packages.
5+
If you have a package that needs to customize the test, you can do so by providing appropriate keyword arguments to `Aqua.test_all()` (see below)
6+
7+
```@docs
8+
Aqua.test_all
9+
```

docs/src/unbound_args.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Unbound Type Parameters
2+
3+
An unbound type parameter is a type parameter with a `where`,
4+
that does not occur in the signature of some dispatch of the method.
5+
6+
## Examples
7+
8+
The following methods each have `T` as an unbound type parameter:
9+
10+
```@repl unbound
11+
f(x::Int) where {T} = do_something(x)
12+
g(x::T...) where {T} = println(T)
13+
```
14+
15+
In the cases of `f` above, the unbound type parameter `T` is neither
16+
present in the signature of the methods nor as a bound of another type parameter.
17+
Here, the type parameter `T` can be removed without changing any semantics.
18+
19+
For signatures with `Vararg` (cf. `g` above), the type parameter is unbound for the
20+
zero-argument case (e.g. `g()`).
21+
22+
```@repl unbound
23+
g(1.0, 2.0)
24+
g(1)
25+
g()
26+
```
27+
28+
A possible fix would be to replace `g` by two methods.
29+
30+
```@repl unbound2
31+
g() = println(Int) # Defaults to `Int`
32+
g(x1::T, x2::T...) where {T} = println(T)
33+
g(1.0, 2.0)
34+
g(1)
35+
g()
36+
```
37+
38+
## [Test function](@id test_unbound_args)
39+
40+
```@docs
41+
Aqua.test_unbound_args
42+
```

src/Aqua.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ include("persistent_tasks.jl")
2727
"""
2828
test_all(testtarget::Module)
2929
30-
Run following tests in isolated testset:
30+
Run the following tests:
3131
3232
* [`test_ambiguities([testtarget, Base, Core])`](@ref test_ambiguities)
3333
* [`test_unbound_args(testtarget)`](@ref test_unbound_args)

src/ambiguities.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
Test that there is no method ambiguities in given package(s). It
66
calls `Test.detect_ambiguities` in a separated clean process to avoid
7-
false-positive.
7+
false-positives.
88
99
# Keyword Arguments
1010
- `broken::Bool = false`: If true, it uses `@test_broken` instead of

src/exports.jl

-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ function walkmodules(f, x::Module)
1212
end
1313
end
1414

15-
"""
16-
undefined_exports(m::Module) :: Vector{Symbol}
17-
"""
1815
function undefined_exports(m::Module)
1916
undefined = Symbol[]
2017
walkmodules(m) do x

0 commit comments

Comments
 (0)