Skip to content

Commit

Permalink
Add the ability to use function calls in @testset directly. (JuliaL…
Browse files Browse the repository at this point in the history
…ang#42518)

* Add the ability to use function calls in @testset

This allows for easier factoring of tests into functions, making it possible to `include` a `test/runtests.jl` file without running tests directly and enabling running of specific testsets explicitly.

* Fix doctest of @testset on functions

* Add note about naming of testset for called functions

* Tweak documentation, expand on intended use case

* Add NEWS.md entry

* Fix explicit description behavior, add documentation to docstring

Co-authored-by: Sukera <[email protected]>
  • Loading branch information
2 people authored and LilithHafner committed Feb 22, 2022
1 parent 883ffed commit 70a1ebf
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 6 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ New library features
* `@test_throws "some message" triggers_error()` can now be used to check whether the displayed error text
contains "some message" regardless of the specific exception type.
Regular expressions, lists of strings, and matching functions are also supported. ([#41888])
* `@testset foo()` can now be used to create a test set from a given function. The name of the test set
is the name of the called function. The called function can contain `@test` and other `@testset`
definitions, including to other function calls, while recording all intermediate test results. ([#42518])

Standard library changes
------------------------
Expand Down
15 changes: 15 additions & 0 deletions stdlib/Test/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@ Test Summary: | Pass Total
Foo Tests | 8 8
```

As well as call functions:

```jldoctest testfoo
julia> f(x) = @test isone(x)
f (generic function with 1 method)
julia> @testset f(1)
Test Summary: | Pass Total
f | 1 1
Test.DefaultTestSet("f", Any[], 1, false, false)
```

This can be used to allow for factorization of test sets, making it easier to run individual
test sets by running the associated functions instead.
Note that in the case of functions, the test set will be given the name of the called function.
In the event that a nested test set has no failures, as happened here, it will be hidden in the
summary, unless the `verbose=true` option is passed:

Expand Down
19 changes: 13 additions & 6 deletions stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,7 @@ end
@testset [CustomTestSet] [option=val ...] ["description"] begin ... end
@testset [CustomTestSet] [option=val ...] ["description \$v"] for v in (...) ... end
@testset [CustomTestSet] [option=val ...] ["description \$v, \$w"] for v in (...), w in (...) ... end
@testset [CustomTestSet] [option=val ...] ["description \$v, \$w"] foo()
Starts a new test set, or multiple test sets if a `for` loop is provided.
Expand All @@ -1241,6 +1242,7 @@ nested testsets is shown even when they all pass (the default is `false`).
The description string accepts interpolation from the loop indices.
If no description is provided, one is constructed based on the variables.
If a function call is provided, its name will be used. Explicit description strings override this behavior.
By default the `@testset` macro will return the testset object itself, though
this behavior can be customized in other testset types. If a `for` loop is used
Expand Down Expand Up @@ -1275,24 +1277,29 @@ macro testset(args...)
tests = args[end]

# Determine if a single block or for-loop style
if !isa(tests,Expr) || (tests.head !== :for && tests.head !== :block)
error("Expected begin/end block or for loop as argument to @testset")
if !isa(tests,Expr) || (tests.head !== :for && tests.head !== :block && tests.head != :call)

error("Expected function call, begin/end block or for loop as argument to @testset")
end

if tests.head === :for
return testset_forloop(args, tests, __source__)
else
return testset_beginend(args, tests, __source__)
return testset_beginend_call(args, tests, __source__)
end
end

"""
Generate the code for a `@testset` with a `begin`/`end` argument
Generate the code for a `@testset` with a function call or `begin`/`end` argument
"""
function testset_beginend(args, tests, source)
function testset_beginend_call(args, tests, source)
desc, testsettype, options = parse_testset_args(args[1:end-1])
if desc === nothing
desc = "test set"
if tests.head === :call
desc = string(tests.args[1]) # use the function name as test name
else
desc = "test set"
end
end
# If we're at the top level we'll default to DefaultTestSet. Otherwise
# default to the type of the parent testset
Expand Down
24 changes: 24 additions & 0 deletions stdlib/Test/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1239,3 +1239,27 @@ Test.finish(ts::PassInformationTestSet) = ts
@test ts.results[2].value == ErrorException("Msg")
@test ts.results[2].source == LineNumberNode(test_throws_line_number, @__FILE__)
end

let
f(x) = @test isone(x)
function h(x)
@testset f(x)
@testset "success" begin @test true end
@testset for i in 1:3
@test !iszero(i)
end
end
tret = @testset h(1)
tdesc = @testset "description" h(1)
@testset "Function calls" begin
@test tret.description == "h"
@test tdesc.description == "description"
@test length(tret.results) == 5
@test tret.results[1].description == "f"
@test tret.results[2].description == "success"
for i in 1:3
@test tret.results[2+i].description == "i = $i"
end
end
end

0 comments on commit 70a1ebf

Please sign in to comment.