ReTest.jl
ReTest
is a testing framework for Julia allowing:
Defining tests in source files, whose execution is deferred and triggered on demand.
This is useful when one likes to have definitions of methods and corresponding tests close to each other. This is also useful for code which is not (yet) organized as a package, and where one doesn't want to maintain a separate set of files for tests.
Filtering run testsets with a
Regex
, which is matched against the descriptions of testsets.This is useful for running only part of the test suite of a package. For example, if you made a change related to addition, and included "addition" in the description of the corresponding testsets, you can easily run only these tests.
Note that a pull request exists in the Julia repository to implement regex-filtering for
Test.@testset
.
A couple more features are also enabled, like shuffling the order in which the testsets are run, or running testsets in parallel (via Distributed
).
ReTest
is mostly backward-compatible with Test
, i.e. minimal change to test files is necessary in order to switch to ReTest
; it's often even possible to use ReTest
features without changing a line, e.g. on Julia's Base
/stdlib tests), cf. Working with test files which use Test
.
ReTest
is still at an early stage of development. If you like to use it for your packages, it's recommended to keep your tests compatible with Test
, so that they can be run through both frameworks (e.g. using ReTest
interactively and Test
in C.I.).
Usage
The exported ReTest.@testset
macro can be used as a direct replacement for Test.@testset
(with limitations, see below), and retest()
has to be called for the tests to be executed. See retest
's docstrings for more details. Moreover, ReTest
re-exports (almost) all exported symbols from Test
, so there should not be any need to import Test
together with ReTest
.
When using @testset
"inline", i.e. within the source-code of a package, one can use the InlineTest
package instead of ReTest
, which only defines the strict minimum and also exports @testset
, and therefore loads faster (even if ReTest
itself loads fast, it can be desirable to have an even lighter dependency). But ReTest
still has to be loaded (as a "test" dependency) in order to call retest
.
Finally, for convenience, ReTest.@testset
also implicitly defines a runtests
function within the enclosing module (and within all recursive parent modules), say M
, such that M.runtests(...)
is equivalent to calling retest(M, ...)
.
Quick start
Both ReTest
and InlineTest
are registered packages and can be installed the usual way. Let's create a package MyPackage
, which is initially a single file located in a directory known to LOAD_PATH
. We want to test its greet
function, by writing tests directly in the same file; this is a use-case for InlineTest
, which loads faster than ReTest
:
# MyPackage.jl file
module MyPackage
using InlineTest
greet() = "Hello World!"
@testset "greet" begin
@test greet() == "Hello World!"
end
end # module
Now, in a Julia session, we load MyPackage
and ReTest
(needed to actually run the tests):
julia> using MyPackage, ReTest
julia> MyPackage.runtests()
Pass
greet | 1
Suppose now that we organize MyPackage
as a standard package, with a proper "runtests.jl" file. We can still keep testsets within "MyPackage.jl", while adding more thorough tests in the "test" folder, which can contain two files, "runtests.jl" and "MyPackageTests.jl":
# MyPackage/test/runtests.jl file
using ReTest, MyPackage
include("MyPackageTests.jl")
# when including this file (e.g. with `Pkg.test`), all the tests
# in both modules will be run:
retest(MyPackage, MyPackageTests)
# MyPackage/test/MyPackageTests.jl file
module MyPackageTests
using MyPackage, ReTest
@testset "more greet" begin
@testset "concatenation" begin
@test MyPackage.greet()^2 == "Hello World!Hello World!"
end
end
@testset "stuff" begin
@test true
end
end # module
We can now load tests either via using MyPackageTests
, if LOAD_PATH
is configured appropriately, or via include
, and run whichever tests we want:
julia> include("test/MyPackageTests.jl");
julia> using ReTest # to use the `retest` function
julia> retest(dry=true, verbose=2) # just list tests, showing nested ones
MyPackage
1| greet
Main.MyPackageTests
1| more greet
2| concatenation
3| stuff
julia> retest("greet", verbose=2) # run only tests related to `greet()`
Pass
MyPackage:
greet | 1
Main.MyPackageTests:
more greet | 1
concatenation | 1
Overall | 2
julia> MyPackageTests.runtests(3) # run only testset with ID 3 in MyPackageTests
Pass
3| stuff | 1
Here it is for basic usage!
API
Defining tests
InlineTest.@testset
— Macro@testset args...
Similar to Test.@testset args...
, but the contained tests are not run immediately, and are instead stored for later execution, triggered by retest()
or runtests()
.
Besides the @testset
body (last argument) and a description string, arguments of @testset
can be:
- the
verbose
option, with a literal boolean value (e.g.verbose=true
) - a literal symbol, serving as a label which can be used for testset filtering (see
retest
's docstring for details). All nested testsets inherit such labels.
Invocations of @testset
can be nested, but qualified invocations of ReTest.@testset
can't.
A @testset
can contain a nested Test.@testset
, or call a function which defines a Test.@testset
: in this case, the Test.@testset
will be run whenever the parent testset is run, but retest
won't know about it: it won't be taken into account during the filtering phase, and won't be printed in dry mode.
Internally, @testset
expressions are converted to an equivalent of Test.@testset
at execution time.
InlineTest.@testset_macro
— Macro@testset_macro @mac
Declare @mac
as a macro which must be expanded statically by retest
so that contained @testset
s can be discovered.
Consider this pattern with Test
which factors out testsets in a function:
using Test
function test_iseven(x)
@testset "iseven $x" begin
@test iseven(x)
end
end
@testset "test $x" for x=2:2:4
test_iseven(x)
end
This doesn't translate directly with ReTest
, as the call to test_iseven
will be performed at run-time, and will end up declaring a new @testset "iseven $x"
at toplevel (this is a problem similar to having include
inside testsets). So on the first run of retest()
, no @test
is run, and on the second one, it fails because x
is not defined at global scope.
The alternative is to turn test_iseven
into a macro and declare it with @testset_macro
:
using ReTest
macro test_iseven(x)
quote
@testset "iseven $($x)" begin
@test iseven($x)
end
end
end
@testset_macro @test_iseven
@testset "test $x" for x=2:2:4
@test_iseven(x)
end
Then, running retest("iseven", verbose=2)
gives:
Pass
test 2 | 1
iseven 2 | 1
test 4 | 1
iseven 4 | 1
Main | 2
Running tests
InlineTest.retest
— Functionretest(mod..., pattern...;
dry::Bool=false, stats::Bool=false, verbose::Real=true,
[id::Bool], shuffle::Bool=false, recursive::Bool=true,
static::Union{Bool,Nothing}=nothing, dup::Bool=false,
load::Bool=false, seed::Union{Integer,Bool}=false,
marks::Bool=true, tag=[], spin::Bool=true)
Run tests declared with @testset
blocks, within modules mod
if specified, or within all currently loaded modules otherwise. Filtering pattern
s can be specified to run only a subset of the tests.
Keywords
- If
dry
istrue
, don't actually run the tests, just print the descriptions of the testsets which would (presumably) run. - If
stats
istrue
, print some time/memory statistics for each testset. - If specified,
verbose
must be an integer orInf
indicating the nesting level of testsets whose results must be printed (this is equivalent to adding theverbose=true
annotation to corresponding testsets); the default behavior (true
or1
) corresponds to printing the result of top-level testsets. - If
id
istrue
, a unique (per module) integer ID is printed next to each testset, which can be used for filtering. The default value ofid
depends on other options. - If
shuffle
istrue
, shuffle the order in which top-level testsets within a given module are run, as well as the list of passed modules. - If
recursive
istrue
, the tests for all the recursive submodules of the passed modulesmod
are also run. - The
static
keyword controls testsets filtering: iftrue
, only testsets which are known to match "statically" the passed patterns, i.e. at filtering time, are run. See docstring ofinterpolated
for more details. - If
dup
istrue
, multiple toplevel testsets can have the same description. Iffalse
, only the last testset of a "duplicate group" is kept. The default isfalse
in order to encourage having unique descriptions (useful for filtering) but also and mostly to play well withRevise
. This keyword applies only to newly added testsets since the last run. - When
load
istrue
, for each package moduleMod
which is selected,retest
attempts to also select a correspondingMain.ModTests
module with the same pattern specification, unless such module is already explicitly passed as an argument. If this test module doesn't already exist,retest
attempts first to include intoMain
the corresponding test file "test/ModTests.jl" which is assumed, if it exists, to define one or more test modules (typicallyModTests
); these new test modules are associated toMod
(they inherit its pattern specification as above), and are cached and used again on subsequent invocations. - If
seed
is provided, it is used to seed the global RNG before running the tests. As a special case, ifseed === false
(the default), no seeding is performed, and ifseed === true
, a seed is chosen randomly. - When
marks
anddry
aretrue
, "check marks" are printed next to testsets which passed or failed in previous runs, as well as labels. - The
tag
keyword allows to tag a testset with labels, encoded as symbols. Whentag
is a list of symbols, tag all matching testsets with these. Whentag
is a symbol, tag all matching testsets with it. Instead of a symbol:sym
, it's possible to instead passnot(:sym)
in order to remove the:sym
label from matching testsets. Currently,tag
has an effect only ifdry
istrue
. - When
spin
istrue
, the description of the testset being currently executed is shown (if there is only one), as well as a "spinner". This is disabled when all the available threads/workers are used to run tests (i.e. typicallyThreads.nthreads()
should be greater than1
forspin
to take effect). Note also that this feature slows down a bit the execution of tests.
The default values of these keywords can be overriden by defining a dictionary or named tuple within Main
called __retest_defaults__
, whose keys are symbols. E.g. __retest_defaults__ = (verbose=Inf, spin=false)
.
Filtering
It's possible to filter run testsets by specifying one or multiple pattern
s. A testset is guaranteed to run only if it "matches" all passed patterns (conjunction). Even if a testset is run, its nested testsets might not run if they don't match the patterns. Moreover if a testset is run, its enclosing testset, if any, also has to run (although not necessarily exhaustively, i.e. other nested testsets might be filtered out).
A pattern
can be a string, a Regex
, an integer, a symbol, an array or a tuple. For a testset to "match" an array, it must match at least one of its elements (disjunction). To match a tuple, it must match all of its elements (conjunction). To match an integer, its ID must be equal to this integer (cf. the id
keyword). To match a symbol, it must be tagged with that symbol (label).
A pattern can also be the "negation" of a pattern, via the not
function, which allows to exclude testsets from being run. As a special case, the negation of an integer can be expressed as its arithmetic negation, e.g. not(3)
is equivalent to -3
.
Patterns can also be created via reachable
, interpolated
and depth
.
Regex
filtering
The "subject" of a testset is the concatenation of the subject of its parent @testset
, if any, with "/$description"
where description
is the testset's description. For example:
@testset "a" begin # subject is "/a"
@testset "b" begin # subject is "/a/b"
end
@testset "c$i" for i=1:2 # subjects are "/a/c1" and "/a/c2"
end
end
When pattern
is a Regex
, a testset is guaranteed to run only when its subject matches pattern
. Moreover, even if a testset matches (e.g. "/a"
above with pattern == r"a$"
), its nested testsets might be filtered out if they don't also match (e.g. "a/b"
doesn't match pattern
).
If a passed pattern
is a string, then it is wrapped in a Regex
with the "case-insensitive" flag, and must match literally the subjects. This means for example that "a|b"
will match a subject like "a|b"
or "A|B"
, but not like "a"
(only in Julia versions >= 1.3; in older versions, the regex is simply created as Regex(pattern, "i")
).
As a special case, if a string pattern starts with the '-'
character, it's interpreted as the negation of the pattern corresponding to the string with '-'
chopped off, e.g. "-abc"
is equivalent to not("abc")
. Unless the string starts with two '-'
characters, in which case the first '-'
is chopped off, e.g. "--abc"
will match subjects such as "123-abc"
. To negate such a pattern, just use not
, e.g. not("--abc")
.
Per-module patterns
In addition to modules or patterns, positional arguments of retest
can also be a pair of the form mod => pattern
: then pattern
is used to filter only testsets from mod
; if other "standalone" patterns (not attached to a module) are specified, they also conjunctively apply to mod
. For example, a call like retest(mod1 => 1:3, mod2, "x")
is equivalent to retest(mod1 => (1:3, "x"), mod2 => "x")
. If recursive
is true
, pattern
is also applied to all recursive submodules sub
of mod
; if sub
is also specified as sub => subpat
, the patterns are merged, i.e. this is equivalent to specifying sub => (pattern, subpat)
.
This function executes each (top-level) @testset
block using eval
within the module in which it was written (e.g. mod
, when specified).
ReTest.watch
— FunctionReTest.watch(args...; kwargs...)
Run retest(args...; kwargs...)
repeatedly each time Revise
detects file updates. Revise
must be loaded beforehand in your Julia session.
This experimental function is not tested and is currently very basic.
Loading tests
Cf. Working with test files which use Test for hijack
and hijack_base
.
ReTest.load
— FunctionReTest.load(testpath::AbstractString;
parentmodule::Module=Main, [revise::Bool])
Include file testpath
into parentmodule
. If revise
is true
, Revise
, which must be loaded beforehand in your Julia session, is used to track all recursively included files (in particular testsets). The revise
keyword defaults to true
when Revise
is loaded and VERSION >= v"1.5"
, and to false
otherwise.
The point of using this function is when revise
is true
and in particular when files are included recursively. If revise
is false, this is equivalent to parentmodule.include(testpath)
, and if there are no recursively included files, this should be equivalent to Revise.includet(testpath)
, provided parentmodule == Main
and all @testset
s defined in testpath
are in a module defining __revise_mode__ = :eval
.
This function requires at least Julia 1.5 when revise
is true
.
ReTest.load(Mod::Module, testfile::AbstractString="ModTests.jl";
parentmodule::Module=Main, [revise::Bool])
Given a package Mod
, include into parentmodule
the corresponding tests from file testfile
, which is assumed to be located in the "test" directory of the package. It is expected that a unique new test module is created within parentmodule
after the inclusion, which is then returned. Otherwise, the list of all newly created test modules is returned, triggering a warning if it's empty.
If revise
is true
, Revise
, which must be loaded beforehand in your Julia session, is used to track the test files (in particular testsets). Note that this might be brittle, and it's recommended instead to load your test module via using ModTests
. The revise
keyword defaults to true
when Revise
is loaded and VERSION >= v"1.5"
, and to false
otherwise.
This function requires at least Julia 1.5 when revise
is true
.
Filtering tests
ReTest.not
— Functionnot(pattern)
Create an object suitable for filtering testsets (in the retest
function), which "negates" the meaning of pattern
: a testset matches not(pattern)
if and only if it doesn't match pattern
.
For example not("a")
matches any testset whose subject doesn't contain "a"
, and not(1:3)
matches all the testsets but the first three of a module.
If pattern
is an integer or a ReTest
object (i.e. not a AbstractString
, Regex
, Tuple
or AbstractArray
), not(pattern)
can be expressed as -pattern
.
String
patterns can also be negated by prepending '-'
, see retest
for details.
ReTest.pass
— Constantpass
Filtering pattern which matches any testset which already ran successfully. The pattern [pass, fail]
matches any testset which already ran.
ReTest.fail
— Constantfail
Filtering pattern which matches any testset which already ran with at least one error. The pattern [pass, fail]
matches any testset which already ran.
ReTest.reachable
— Functionreachable(pattern)
Create a filtering pattern which matches any testset matching pattern
or whose parent testset, if any, matches reachable(pattern)
. In other words, if a testset matches pattern
, all its recursive nested testsets will also match.
When pattern::String
, reachable(pattern)
has the same effect as pattern
, because the subject of a testset is contained in the subjects of all its nested testsets. So reachable
is typically useful when pattern
is an integer.
Examples
julia> module T
using ReTest
@testset "a" verbose=true begin
@test true
@testset "b" begin
@test true
end
end
@testset "c" begin
@test true
end
end;
julia> retest(T, reachable(1), dry=true)
1| a
2| b
julia> retest(T, not(reachable(1)), dry=true)
3| c
Note that the algorithm for reachable
is currently not optimized, i.e. it will match pattern
against all parents of a testset until success, even when this match was already performed earlier (i.e. the result of matching against pattern
is not cached).
Also, in the current implementation, the subject of a parent testset is inferred from the subject of a testset, by chopping off the last component, determined by the last occurrence of '/'
. This has two consequences. It will produce incorrect results if the description of a testset contains '/'
, and also, with interpolated
when the subject is "unknown" due to un-interpolated descriptions. Consider the following example:
julia> module Fail
using ReTest
@testset "a" begin
x = 1
@testset "b$x" begin
@testset "c" begin end
end
end
end;
julia> retest(Fail, reachable(1), verbose=9, dry=true)
1| a
2| "b$(x)"
3| c
julia> retest(Fail, reachable(interpolated), verbose=9, dry=true)
1| a
Here, both testsets with id 2
and 3
have an unknown subject (at filtering time), which prevents the algorithm to detect that one of their parents (testset 1
) actually has an "interpolated" description.
On the other hand, even with these unknown subjects, something like reachable("a")
would work as expected:
julia> retest(Fail, reachable("a"), verbose=9, dry=true)
1| a
2| "b$(x)"
3| c
julia> retest(Fail, reachable("a"), verbose=9, dry=true, static=true)
1| a
This function requires at least Julia 1.3.
ReTest.interpolated
— Constantinterpolated
Singleton pattern which matches any testset whose subject can be interpolated "statically", i.e. at filtering time before testset are actually run. Non-inferrable subjects include those constructed from descriptions containing interpolated values which can't be known until run time. This pattern has an effect closely related to that of the static
keyword of retest
, discussed below, which is probably more generally useful.
Examples
Given these testset:
@testset "outer" verbose=true begin
@test true
inner = "inner"
@testset "$inner" begin
@test true
end
end
@testset "other" begin
@test true
end
We get:
julia> retest("other", dry=true)
Main
1| outer
2| "$(inner)"
3| other
julia> retest("other", dry=false)
Pass
outer | 1
other | 1
Main | 2
julia> retest("other", dry=true, interpolated)
Main
3| other
Without interpolated
, retest
can't decide at filtering time whether the "inner" testset will run, so must mark the "outer" testset as having to run. At run time, "inner" is not run because it doesn't match the pattern, but "outer" still had to run to determine this. With the interpolated
pattern, "inner" is filtered out and retest
selects only testsets which are statically known to have to run.
So again, interpolated
doesn't have the same effect at filtering time (like when dry=true
) and at run time. For example, one can see the list of non-interpolated subjects as follows with dry=true
, but not run them (because everything is interpolated at run time):
julia> retest(not(interpolated), dry=true)
1| outer
2| "$(inner)"
julia> retest(not(interpolated), dry=false)
Pass
Main:
outer | 1
static
keyword
Unlike interpolated
, the static
keyword of retest
, when true
, filters out only testsets which can't be proven to have to run at filtering time, let's call them "undecidable". It can have sometimes the same effect as when using interpolated
, e.g. retest("other", dry=true, static=true)
and retest("other", dry=true, interpolated)
give the same result.
But in some cases we might want to filter out noisy testsets whose subjects can't be interpolated, but still include those which are relevant. For example, assume we want to run testsets 1
and 2
, while excluding other testsets with uninterpolated subjects:
julia> retest(1:2, dry=true, interpolated)
Main
1| outer
julia> retest(1:2, dry=true, static=true)
Main
1| outer
2| "$(inner)"
The solution with interpolated
is not what we want, as we specifically want testset 2
to run. Given the filtering specifications (1:2
here), the filtering algorithm can determine that 2
should run even though its subject is unknown at this point.
Given a filtering specification, there are three kind of testsets:
- "undecidable" (see above)
- "match": they are known statically to have to run
- "nomatch": they are known statically to not have to run
The default value of the static
keyword is nothing
, which means to run testsets which are not known with certainty to not match, i.e. "match" and "undecidable" testsets. As seen above, when static == true
, only "match" testsets are run. When static == false
, the behavior is the opposite: only "undecidable" testsets are run. Of course, other combinations involving "nomatch" testsets can be had by reversing the filtering pattern via not
.
For example, to get the equivalent to the not(interpolated)
example above, but with an effect which persists at run time (dry = false
), you can use static = false
together with the match-all regex pattern r".*"
, which will mark the "inner"
testset as "undecidable" (the algorithm inspects slightly patterns just to recognize the simple match-all patterns ""
and r""
, but won't detect that r".*"
would match "$(inner)"
):
julia> retest(r".*", static=false, dry=true)
Main
1| outer
2| "$(inner)"
julia> retest(r".*", static=false, dry=false)
Pass
Main:
outer | 2
inner | 1
One example of a rare case where a given testset is not in a single of the above three categories is as follows:
@testset "a" begin
x = 2
@testset "b$(i==1 ? 1 : x)" for i=1:2
@testset "c" begin
# subject is "match" at first iteration and
# "undecidable" at second iteration
@test true
end
end
end
One thing to understand is that the "identity" of a testset is determined by a given occurrence of the @testset
macro. In the example above, for either the patterns "b" or "c", the two inner testsets are both "match" and "undecidable". In this case, the filtering algorithm selects a testset to run if at least one iteration would lead to this decision. Here, if static=true
the first iteration would run, and if static=false
the second iteration would run. This results in the same selection whatever the value of static
is.
ReTest.depth
— Functiondepth(d::Integer)
Create a pattern which matches testsets at "depth" d
. Toplevel testsets have depth 1
, their direct children (nested testsets) depth 2
, and so on.
Examples
julia> module Depth
using ReTest
@testset "1" begin
@testset "2" begin
@testset "3" begin end
end
@testset "4" begin end
end
end;
julia> Depth.runtests(dry=true, verbose=3, depth(2))
1| 1
2| 2
4| 4
julia> Depth.runtests(dry=true, verbose=3, depth(3))
1| 1
2| 2
3| 3
julia> Depth.runtests(dry=true, verbose=3, reachable(depth(2)))
1| 1
2| 2
3| 3
4| 4
julia> Depth.runtests(dry=true, verbose=3, depth.(2:3))
1| 1
2| 2
3| 3
4| 4
ReTest.iter
— Functioniter(i::Integer)
Filtering pattern which matches only the i
-th iteration of a testset-for. A non-for testset is considered to have a unique iteration.
This is very experimental, not tested, and likely to be removed in a future version.
Caveats
ReTest.@testset
comes with a couple of caveats/limitations, some of which should be fixable:
Toplevel testsets (which are not nested within other testsets), when run, are
eval
ed at the toplevel of their parent module, which means that they can't depend on local variables for example."testsets-for" (
@testset "description" for ...
), when run, implyeval
ing their loop variables at the toplevel of their parent module; this implies that iteration expressions shouldn't depend on local variables (otherwise, the testset subject usually can't be known statically and the testset can't be filtered out with aRegex
).Testsets can not be "custom testsets" (cf.
Test
documentation).Nested testsets can't be "qualified" (i.e. written as
ReTest.@testset
).Regex filtering logic might improve in future versions, which means that with the same regex, less tests might be run (or more!). See
retest
's docstring to know which testsets are guaranteed to run.Descriptions of testsets must be unique within a module, otherwise they are overwritten and a warning is issued, unless
Revise
is loaded; the reason is the current implemented heuristic to allowRevise
do its magic.There is not yet a good solution to factor out testsets into functions called within other testsets; a work-around is to use
@testset_macro
, or to use only the@test
and@test_*
macros within these functions.Interrupting running testsets with Control-C sometimes doesn't work well, because of the use of multiple tasks in
retest
. There can also be usability annoyances when some tests are failing. This will hopefully be fixed soon.
Including files from within testsets
TLDR: don't use include(file)
within testsets when file
defines other testsets.
There is limited support for include(path)
expressions within testsets: all what ReTest
does is to adjust the path
according to the location of the containing file parentfile
. This is necessary, because include
is not run immediately when that file is evaluated; when the given testset is triggered (via a retest
call), include
doesn't have the same "context" as parentfile
, which would lead to path
being interpreted as non-existing (unless path
is an absolute path). So when parsing testsets, ReTest
prepends the directory name of parentfile
to path
.
The important point is that include
is executed at retest
-time; if the included file defines other @testset
expressions, this will define new testsets in the enclosing module, but these won't be run immediately; upon a new retest()
invocation, these new testsets will be run, but the old one too (the one containing include
), which will redefine included testsets. This is brittle, and it's recommended to not include, within testsets, files defining other testsets.
Switching from Test
to ReTest
When used in a package MyPackage
, the recommended way to organize test code is as follows:
- replace
using Test
byusing ReTest
in the "runtests.jl" file (and in all other test files havingusing Test
) - wrap the whole content of "runtests.jl" within a module named
MyPackageTests
- rename "runtests.jl" to "MyPackageTests.jl"
- create a "runtests.jl" file with the following content:
include("MyPackageTests.jl"); MyPackageTests.runtests()
This means that running "runtests.jl" will have the same net effect as before. The "MyPackageTests.jl" file can now be include
d in your REPL session (include("MyPackageTests.jl")
), and you can run all or some of its tests (e.g. MyPackageTests.runtests("addition")
). This test file can also be included via the ReTest.load
function or via the load
keyword of retest
.
Wrapping the tests in MyPackageTests
allows to not pollute Main
and keeps the tests of different packages separated. Also, you can modify "MyPackageTests.jl" and re-include it to have the corresponding tests updated (the MyPackageTests
module is replaced in Main
); otherwise, without a MyPackageTests
module, including the file a second time currently triggers a warning for each overwritten toplevel testset.
Keeping the ability to use Test
One might want to have the possibility to use either Test
or ReTest
depending on the context. Reasons to still use Test
include:
- when running
retest
for the first time in a Julia session, more code has to be compiled than when running tests withTest
, so in the case of running the whole test suite, few seconds can be spared (although usingReTest
in parallel mode would generally compensate for this); ReTest
is not yet a fully mature and battle tested package, so you might want to not rely exclusively on it, e.g. for C.I.
An alternate way to organize the test files is as follows, assuming using Test
is only present in "runtests.jl":
- remove
using Test
from "runtests.jl" - rename "runtests.jl" to "tests.jl"
- create a "MyPackageTests.jl" file with the following content:
module MyPackageTests using ReTest include("tests.jl") end
- create a "runtests.jl" file with the following content:
using Test include("tests.jl")
That way, include("test/runtests.jl")
or Pkg.test()
will run tests using Test
, while include("test/MyPackageTests.jl"); MyPackageTests.runtests()
will use ReTest
.
Filtering
Most of the time, filtering with a simple string is likely to be enough. For example, in
@testset "a" begin
@test true
@testset "b" begin
end
@testset "c" begin
end
end
running retest(M, "a")
will run everything, retest(M, "b")
will run @test true
and @testset "b"
but not @testset "c"
. Note that if you want to run @testset "b"
, there is no way to not run @test true
in @testset "a"
; so if it was an expensive test to run, instead of @test true
, it could be useful to wrap it in its own testset, so that it can be filtered out.
Running tests in parallel with Distributed
Currently, the tests are automatically run in parallel whenever there are multiple workers, which have to be set manually. Running the tests looks like:
using Distributed
addprocs(2)
@everywhere include("test/MyPackageTests.jl")
MyPackageTests.runtests()
If the test code doesn't use ReTest
(cf. Working with test files which use Test
), this can be done as follows:
using Distributed
addprocs(2)
using ReTest
@everywhere begin
using ReTest, MyPackage
ReTest.hijack(MyPackage)
end
MyPackageTests.runtests()
As was already mentioned, testset-for iterators are evaluated at load time in the enclosing module, but this currently happens only in the main process. This can lead to unexpected errors when the package was written without a Distributed
use-case in mind.
For example, say the package defines a constant singleton object X
which is normally equal to itself (because X === X
). But if X
is assigned to a testset-for loop variable x
, it will be the one from the main process, so within the testset-for, a test like x == X
might fail because X
refers to the singleton object defined in another process; a solution in this case could be to define explicitly ==
for objects of the type of X
.
It should be relatively easy to support threaded execution of testsets (it was actually implemented at one point). But it often happens that compiling package code and testset code (which currently is not threaded) takes quite more time than actually running the code, in which case using Distributed
has more tangible benefits.
Working with Revise
When Revise
is loaded and a testset is updated, ReTest
will observe that a new testset is added with the same description as a previously existing one, which is then overwritten. This works only if the description is not modified, otherwise both the old and new versions of the testset will co-exist.
For testsets in a "script" loaded with includet
, e.g. those in a "test/MyPackageTests.jl" file, you can request Revise
to "load" the updated testsets by putting __revise_mode__ = :eval
in the enclosing module.
When files are included recursively, plain includet
won't work (it is currently documented to be "deliberately non-recursive"). There are three work-arounds, of which the first is recommended:
- load
MyPackageTests
as a module, i.e. viausing MyPackageTests
instead ofinclude("test/MyPackageTests.jl")
(this might involve updating yourLOAD_PATH
to include "test/" and making sure the required packages are found) - load
MyPackageTests
viaReTest.load(MyPackage, revise=true)
, but this works only in "simple enough" situations - use the following
recursive_includet
function instead ofincludet
:
function recursive_includet(filename)
already_included = copy(Revise.included_files)
includet(filename)
newly_included = setdiff(Revise.included_files, already_included)
for (mod, file) in newly_included
Revise.track(mod, file)
end
end
Working with test files which use Test
It's sometimes possible to use ReTest
features on a test code base which uses Test
, without modifications:
- if you have a package
Package
, you can tryReTest.hijack(Package)
, which will define aPackageTests
module when successful, on which you can callretest
. To haveRevise
track changes to test files, useReTest.hijack(Package, revise=true)
. - if you have a test file
"testfile.jl"
, tryReTest.hijack("testfile.jl")
(this will define a fresh module like above). Base
and standard library modules can also be passed toReTest.hijack
(corresponding tests are loaded via the lower levelReTest.hijack_base
function).
ReTest.hijack
— FunctionReTest.hijack(source, [modname];
parentmodule::Module=Main, lazy=false, [revise::Bool],
[include::Symbol], include_functions=[:include])
Given test files defined in source
using the Test
package, try to load them by replacing Test
with ReTest
, wrapping them in a module modname
defined within parentmodule
. If successful, the newly created module modname
is returned and modname.runtests()
should be callable.
If source::AbstractString
, then it's interpreted as the top level test file (possibly including other files), and modname
defaults to a name based on basename(source)
.
If source::Module
, then it's interpreted as the name of a package, and the "test/runtests.jl" file from this package is loaded. In this case, modname
defaults to Symbol(source, :Tests)
.
The current procedure is as follows:
- replace toplevel
using Test
occurrences byusing ReTest
(using
can have multiple arguments); - apply recursively these two rules:
- for all
include
d files, provided theinclude
statement is at the toplevel, or nested within these toplevel constructs:begin
,let
,for
,while
,if
,try
; - on the content of all included modules.
- for all
When source
is Base
or a standard library module, a slightly different mechanism is used to find test files (which can contain e.g. non-toplevel include
s), i.e. ReTest.hijack_base
is used underneath.
lazy
keyword
The lazy
keyword specifies whether some toplevel expressions should be skipped:
false
means nothing is skipped;true
means toplevel@test*
macros are removed, as well as those defined within these toplevel (but possible nested) blocks:begin
,let
,for
,while
,if
,try
;:brutal
means toplevel@test*
macros are removed, as well as toplevelbegin
,let
,for
orif
blocks.
include
keyword
The include
keyword can help to handle the case where @testset
s contain include
expressions, like in the following example:
@testset "parent" begin
@test true
include("file_with_other_testsets.jl")
end
This works well with Test
because testsets are run immediately, as well as testsets contained in the included files, which are also recognized as children of the testset which include them. With ReTest
, the include
expressions would be evaluated only when the parent testsets are run, so that included testsets are not run themselves, but only "declared".
If the include
keyword is set to :static
, include(...)
expressions are evaluated when @testset
expressions containing them are parsed, before filtering and before testsets are run. Testsets which are declared (within the same module) as a side effect of include(...)
are then inserted in place of the call to include(...)
.
If the include
keyword is set to :outline
, hijack
inspects topelevel @testset
expressions and puts toplevel include(...)
expressions outside of the containing testset, and should therefore be evaluated immediately. This is not ideal, but at least allows ReTest
to know about all the testsets right after the call to hijack
, and to not declare new testsets when parent testsets are run.
The :outline
option might be deprecated in the future, and include=:static
should generally be preferred. One case where :outline
might work better is when the included file defines a submodule: ReTest
doesn't have the concept of a nested testset belonging to a different module than the parent testset, so the best that can be done here is to "outline" such nested testsets; with include=:outline
, hijack
will "process" the content of such submodules (replace using Test
by using ReTest
, etc.), whereas with include=:static
, the subdmodules will get defined after hijack
has returned (on the first call to retest
thereafter), so won't be "processed".
include_functions
keyword
When the include=:static
keyword argument is passed, it's possible to tell hijack
to apply the same treatment to other functions than include
, by passing a list a symbols to include_functions
. For example, if you defined a custom function custom_include(x)
which itself calls out to include
, you can pass include_functions=[:custom_include]
to hijack
.
revise
keyword
The revise
keyword specifies whether Revise
should be used to track the test files (in particular the testsets). If true
, Revise
must be loaded beforehand in your Julia session. Note that this might be brittle and not work in all cases. revise
defaults to true
when Revise
is loaded, and to false
otherwise.
This function requires at least Julia 1.5.
ReTest.hijack_base
— Functionhijack_base(tests, [modname];
parentmodule::Module=Main, lazy=false, [revise::Bool])
Similar to ReTest.hijack
, but specifically for Base
and stdlib tests. tests
speficies which test files should be loaded, in the exact same format as Base.runtests
(i.e. it uses the same choosetests
function to select the tests).
Tests corresponding to a "test/[somedir/]sometest.jl" file are loaded in the BaseTests.[somedir.]sometest
module (if sometest
is defined in Base
, then sometest_
is used instead).
Tests corresponding to a standard library Lib
are loaded in the StdLibTests.Lib_
module. When there are "testgroups", submodules are created accordingly.
If modname
is specified (experimental), this will be the name of the module in which testsets are defined; passing modname
is allowed only when all the loaded tests would otherwise be defined in the same second top-most module, the one under BaseTests
or StdLibTests
(e.g. somedir
if any, or sometest
otherwise, or Lib_
). This unique module is then named modname
, and not enclosed within BaseTests
or StdLibTests
.
The lazy
and revise
keywords have the same meaning as in ReTest.hijack
. Depending on the value of lazy
, some test files are skipped when they are known to fail.
This function requires at least Julia 1.5.