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 # moduleNow, in a Julia session, we load MyPackage and ReTest (needed to actually run the tests):
julia> using MyPackage, ReTest
julia> MyPackage.runtests()
Pass
greet | 1Suppose 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 # moduleWe 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 | 1Here 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
verboseoption, 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 @macDeclare @mac as a macro which must be expanded statically by retest so that contained @testsets 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)
endThis 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)
endThen, running retest("iseven", verbose=2) gives:
Pass
test 2 | 1
iseven 2 | 1
test 4 | 1
iseven 4 | 1
Main | 2Running 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 patterns can be specified to run only a subset of the tests.
Keywords
- If
dryistrue, don't actually run the tests, just print the descriptions of the testsets which would (presumably) run. - If
statsistrue, print some time/memory statistics for each testset. - If specified,
verbosemust be an integer orInfindicating the nesting level of testsets whose results must be printed (this is equivalent to adding theverbose=trueannotation to corresponding testsets); the default behavior (trueor1) corresponds to printing the result of top-level testsets. - If
idistrue, a unique (per module) integer ID is printed next to each testset, which can be used for filtering. The default value ofiddepends on other options. - If
shuffleistrue, shuffle the order in which top-level testsets within a given module are run, as well as the list of passed modules. - If
recursiveistrue, the tests for all the recursive submodules of the passed modulesmodare also run. - The
statickeyword controls testsets filtering: iftrue, only testsets which are known to match "statically" the passed patterns, i.e. at filtering time, are run. See docstring ofinterpolatedfor more details. - If
dupistrue, multiple toplevel testsets can have the same description. Iffalse, only the last testset of a "duplicate group" is kept. The default isfalsein 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
loadistrue, for each package moduleModwhich is selected,retestattempts to also select a correspondingMain.ModTestsmodule with the same pattern specification, unless such module is already explicitly passed as an argument. If this test module doesn't already exist,retestattempts first to include intoMainthe 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
seedis 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
marksanddryaretrue, "check marks" are printed next to testsets which passed or failed in previous runs, as well as labels. - The
tagkeyword allows to tag a testset with labels, encoded as symbols. Whentagis a list of symbols, tag all matching testsets with these. Whentagis 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:symlabel from matching testsets. Currently,taghas an effect only ifdryistrue. - When
spinistrue, 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 than1forspinto 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 patterns. 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
endWhen 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 @testsets 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 — ConstantpassFiltering pattern which matches any testset which already ran successfully. The pattern [pass, fail] matches any testset which already ran.
ReTest.fail — ConstantfailFiltering 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| cNote 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| aHere, 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| aThis function requires at least Julia 1.3.
ReTest.interpolated — ConstantinterpolatedSingleton 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
endWe 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| otherWithout 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 | 1static 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 | 1One 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
endOne 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| 4ReTest.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
evaled 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, implyevaling 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.
Testdocumentation).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
Reviseis loaded; the reason is the current implemented heuristic to allowRevisedo 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@testand@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 Testbyusing ReTestin 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 included 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
retestfor 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 usingReTestin parallel mode would generally compensate for this); ReTestis 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 Testfrom "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
endrunning 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
MyPackageTestsas a module, i.e. viausing MyPackageTestsinstead ofinclude("test/MyPackageTests.jl")(this might involve updating yourLOAD_PATHto include "test/" and making sure the required packages are found) - load
MyPackageTestsviaReTest.load(MyPackage, revise=true), but this works only in "simple enough" situations - use the following
recursive_includetfunction 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
endWorking 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 aPackageTestsmodule when successful, on which you can callretest. To haveRevisetrack 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). Baseand standard library modules can also be passed toReTest.hijack(corresponding tests are loaded via the lower levelReTest.hijack_basefunction).
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 Testoccurrences byusing ReTest(usingcan have multiple arguments); - apply recursively these two rules:
- for all
included files, provided theincludestatement 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 includes), i.e. ReTest.hijack_base is used underneath.
lazy keyword
The lazy keyword specifies whether some toplevel expressions should be skipped:
falsemeans nothing is skipped;truemeans toplevel@test*macros are removed, as well as those defined within these toplevel (but possible nested) blocks:begin,let,for,while,if,try;:brutalmeans toplevel@test*macros are removed, as well as toplevelbegin,let,fororifblocks.
include keyword
The include keyword can help to handle the case where @testsets contain include expressions, like in the following example:
@testset "parent" begin
@test true
include("file_with_other_testsets.jl")
endThis 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.