Writing a TestBlockInterface
By default TestPicker only recognizes standard @testset blocks (via StdTestset). If your test files use another macro to define individual tests — for example @testitem — you can teach TestPicker to recognize and run it by implementing your own TestBlockInterface.
The interface
A TestBlockInterface is an abstract type you subclass with a (usually singleton) struct. Two methods are required, and two more are optional.
Required methods
istestblock(interface, node::SyntaxNode)::Bool— given a top-level syntax node from a parsed test file, decide whether it represents one of your test blocks.blocklabel(interface, node::SyntaxNode)::String— produce a human-readable, preferably unique label for the block, used for display and fuzzy matching infzf.
Both are called on every top-level (and nested) node found in a test file, so they should be cheap and side-effect free.
Optional methods
preamble(interface)— anExpr(ornothing) that should be evaluated once before any block of this type is run, e.g. tousinga required package. Defaults tonothing.expr_transform(interface, ex::Expr, info::TestBlockInfo, root::AbstractString)::Expr— rewrite the block's expression before it gets evaluated.infocarries the block's label, file name and line range;rootis the package's test directory. Defaults to returningexunchanged.
Registering an interface
Once implemented, tell TestPicker about it with add_interface! (adds to the existing interfaces) or replace_interface! (replaces all of them):
add_interface!(MyInterface())Example: recognizing @testitem blocks
TestPicker ships with TestItemInterface as a built-in example — enable it with add_testitem_interface!(). Its implementation is a good template to follow:
struct TestItemInterface <: TestBlockInterface end
function istestblock(::TestItemInterface, node::SyntaxNode)
kind(node) == K"macrocall" || return false
nodes = JuliaSyntax.children(node)
isnothing(nodes) && return false
length(nodes) > 1 || return false
kind(first(nodes)) == K"MacroName" || return false
sourcetext(first(nodes)) == "testitem" || return false
# The second node needs to be a descriptive `String`.
return kind(nodes[2]) == K"string"
end
function blocklabel(::TestItemInterface, node::SyntaxNode)
return sourcetext(only(JuliaSyntax.children(JuliaSyntax.children(node)[2])))
end
function preamble(::TestItemInterface)
return :(using TestItemRunner)
end
function expr_transform(
::TestItemInterface, ::Expr, (; label, file_name)::TestBlockInfo, root::AbstractString
)
return :(esc(
TestItemRunner.run_tests(
$(dirname(root));
filter=ti ->
(ti.name == $(label) && ti.filename == $(joinpath(root, file_name))),
),
))
endA few things worth noting:
istestblockwalks theSyntaxNodechildren to check it is a@testitem "..." begin ... endmacrocall, mirroring howStdTestsetchecks for@testset.blocklabelstrips the surrounding quotes from the descriptive string so that it can be compared directly againstTestItemRunner'sti.name.- Rather than running the parsed block's
Exprdirectly,expr_transformdiscards it and instead builds a call toTestItemRunner.run_tests, filtering on the item's name and originating file. This is a good pattern whenever the underlying macro needs its own runner instead of being evaluated as plain Julia code. preambleensuresTestItemRunneris loaded before that generated call runs.
With add_testitem_interface!() registered, searching a test block query will surface both @testset and @testitem blocks side by side.