Skip to content

Commit 2051184

Browse files
Test: Add @include_files
1 parent 7dfa7d1 commit 2051184

File tree

9 files changed

+206
-1
lines changed

9 files changed

+206
-1
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Standard library changes
108108
* Transparent test sets (`@testset let`) now show context when tests error ([#58727]).
109109
* `@test_throws` now supports a three-argument form `@test_throws ExceptionType pattern expr` to test both exception type and message pattern in one call ([#59117]).
110110
* The testset stack was changed to use `ScopedValue` rather than task local storage ([#53462]).
111+
* New `@include_files` macro for organizing test files with optional filtering based on command-line arguments. Supports `Pkg.test(test_args=["--files=pattern"])` for substring matching and `Pkg.test(test_args=["--files-regex=pattern"])` for regex matching to run only matching test files. ([#59951])
111112

112113
#### InteractiveUtils
113114

stdlib/Test/docs/src/index.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,51 @@ in the test set reporting. The test will not run but gives a `Broken` `Result`.
296296
Test.@test_skip
297297
```
298298

299+
## Organizing Test Files
300+
301+
When working with larger test suites, it's common to split tests across multiple files. The
302+
`@include_files` macro provides a convenient way to include test files with optional filtering
303+
based on command-line arguments.
304+
305+
```@docs
306+
Test.@include_files
307+
```
308+
309+
For example, a package's `test/runtests.jl` might look like:
310+
311+
```julia
312+
using Test
313+
314+
# Include utility functions
315+
include("test_utils.jl")
316+
317+
# Include test files, optionally filtered by command-line args
318+
@include_files [
319+
"basics.jl",
320+
"advanced.jl",
321+
"performance.jl",
322+
]
323+
```
324+
325+
This allows running specific test files:
326+
327+
```julia
328+
# Run all tests
329+
Pkg.test("MyPackage")
330+
331+
# Run only files containing "basics"
332+
Pkg.test("MyPackage", test_args=["--files=basics"])
333+
334+
# Run files containing "basics" or "advanced"
335+
Pkg.test("MyPackage", test_args=["--files=basics,advanced"])
336+
337+
# Use regex patterns for more complex filtering
338+
Pkg.test("MyPackage", test_args=["--files-regex=^test_.*_integration\\.jl\$"])
339+
340+
# Match files with "basic" or "advanced" using regex
341+
Pkg.test("MyPackage", test_args=["--files-regex=basic|advanced"])
342+
```
343+
299344
## Test result types
300345

301346
```@docs

stdlib/Test/src/Test.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export @test, @test_throws, @test_broken, @test_skip,
2626

2727
export @testset
2828
export @inferred
29+
export @include_files
2930
export detect_ambiguities, detect_unbound_args
3031
export GenericString, GenericSet, GenericDict, GenericArray, GenericOrder
3132
export TestSetException
@@ -2535,6 +2536,90 @@ function _check_bitarray_consistency(B::BitArray{N}) where N
25352536
return true
25362537
end
25372538

2539+
function filter_test_files(files::Vector, args::Vector=ARGS)
2540+
# Parse args to extract file filters
2541+
filter_args = String[]
2542+
use_regex = false
2543+
2544+
# Check for --files-regex= argument (takes precedence)
2545+
regex_idx = findfirst(arg -> startswith(arg, "--files-regex="), args)
2546+
if regex_idx !== nothing
2547+
# Extract comma-separated list from --files-regex=
2548+
pattern_str = args[regex_idx][15:end] # Skip "--files-regex="
2549+
append!(filter_args, split(pattern_str, ','))
2550+
use_regex = true
2551+
else
2552+
# Check for --files= argument
2553+
files_idx = findfirst(arg -> startswith(arg, "--files="), args)
2554+
if files_idx !== nothing
2555+
# Extract comma-separated list from --files=
2556+
files_str = args[files_idx][9:end] # Skip "--files="
2557+
append!(filter_args, split(files_str, ','))
2558+
end
2559+
end
2560+
2561+
# Return all files if no filters
2562+
isempty(filter_args) && return files
2563+
2564+
# Filter files based on basename matching
2565+
return filter(files) do file
2566+
name = basename(file)
2567+
if use_regex
2568+
any(arg -> occursin(Regex(arg), name), filter_args)
2569+
else
2570+
any(arg -> occursin(arg, name), filter_args)
2571+
end
2572+
end
2573+
end
2574+
2575+
"""
2576+
@include_files(files)
2577+
2578+
Include test files from a list, optionally filtered by command-line test arguments.
2579+
2580+
When running tests via `Pkg.test()`, files can be filtered by passing `test_args`
2581+
with the `--files=` or `--files-regex=` flag. If no test args are provided, all files are included.
2582+
2583+
!!! compat "Julia 1.13"
2584+
`@include_files` was added in Julia 1.13, but is available and exported in Compat.jl
2585+
in earlier Julia versions.
2586+
2587+
# Example
2588+
```julia
2589+
using Test
2590+
2591+
# include common utilities
2592+
include("utils.jl")
2593+
2594+
@include_files [
2595+
"foo.jl",
2596+
"bar.jl",
2597+
"baz.jl",
2598+
]
2599+
```
2600+
2601+
## Usage patterns:
2602+
2603+
- `Pkg.test()` → includes all files
2604+
- `Pkg.test(test_args=["--files=foo"])` → includes only "foo.jl"
2605+
- `Pkg.test(test_args=["--files=foo,bar"])` → includes "foo.jl" and "bar.jl"
2606+
- `Pkg.test(test_args=["--files-regex=^test_.*\\.jl\$"])` → uses regex pattern matching
2607+
- `Pkg.test(test_args=["--files-regex=foo|bar"])` → includes files matching "foo" or "bar"
2608+
"""
2609+
macro include_files(files)
2610+
quote
2611+
let test_files = $(esc(files))
2612+
# Filter files based on ARGS
2613+
filtered_files = filter_test_files(test_files, ARGS)
2614+
2615+
# Include filtered files
2616+
for file in filtered_files
2617+
include(file)
2618+
end
2619+
end
2620+
end
2621+
end
2622+
25382623
include("logging.jl")
25392624
include("precompile.jl")
25402625

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using Test
2+
3+
@include_files ["test_foo.jl", "test_bar.jl", "test_baz.jl"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
println("BAR_RAN")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
println("BAZ_RAN")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
println("FOO_RAN")

stdlib/Test/test/runtests.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,3 +2096,71 @@ end
20962096
@test !occursin("Finished testset:", output)
20972097
end
20982098
end
2099+
2100+
@testset "filter_test_files" begin
2101+
files = ["test_foo.jl", "test_bar.jl", "test_baz.jl"]
2102+
2103+
@test Test.filter_test_files(files, String[]) == files
2104+
@test Test.filter_test_files(files, ["--files=foo"]) == ["test_foo.jl"]
2105+
@test Test.filter_test_files(files, ["--files=foo,bar"]) == ["test_foo.jl", "test_bar.jl"]
2106+
@test Test.filter_test_files(files, ["--files=ba"]) == ["test_bar.jl", "test_baz.jl"]
2107+
@test Test.filter_test_files(files, ["--files=nomatch"]) == String[]
2108+
@test Test.filter_test_files(files, ["--files=foo", "bar"]) == ["test_foo.jl"] # Other args ignored
2109+
@test Test.filter_test_files(files, ["foo"]) == files # No --files= means no filtering
2110+
@test Test.filter_test_files(files, ["bar", "baz"]) == files # No --files= means no filtering
2111+
2112+
# Test regex patterns
2113+
@test Test.filter_test_files(files, ["--files-regex=^test_foo"]) == ["test_foo.jl"]
2114+
@test Test.filter_test_files(files, ["--files-regex=foo|bar"]) == ["test_foo.jl", "test_bar.jl"]
2115+
@test Test.filter_test_files(files, ["--files-regex=ba[rz]"]) == ["test_bar.jl", "test_baz.jl"]
2116+
@test Test.filter_test_files(files, ["--files-regex=^test_.*\\.jl\$"]) == files
2117+
@test Test.filter_test_files(files, ["--files-regex=nomatch"]) == String[]
2118+
@test Test.filter_test_files(files, ["--files-regex=foo", "--files=bar"]) == ["test_foo.jl"] # --files-regex takes precedence
2119+
end
2120+
2121+
@testset "@include_files" begin
2122+
# Test in subprocess to avoid polluting ARGS and Main namespace
2123+
test_file = joinpath(@__DIR__, "include_test", "runtests.jl")
2124+
2125+
# Test 1: No args - all files included
2126+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file`, String)
2127+
@test occursin("FOO_RAN", output)
2128+
@test occursin("BAR_RAN", output)
2129+
@test occursin("BAZ_RAN", output)
2130+
2131+
# Test 2: --files= with single filter
2132+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file --files=foo`, String)
2133+
@test occursin("FOO_RAN", output)
2134+
@test !occursin("BAR_RAN", output)
2135+
@test !occursin("BAZ_RAN", output)
2136+
2137+
# Test 3: --files= with multiple filters
2138+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file --files=bar,baz`, String)
2139+
@test !occursin("FOO_RAN", output)
2140+
@test occursin("BAR_RAN", output)
2141+
@test occursin("BAZ_RAN", output)
2142+
2143+
# Test 4: --files= with comma-separated list
2144+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file --files=foo,bar`, String)
2145+
@test occursin("FOO_RAN", output)
2146+
@test occursin("BAR_RAN", output)
2147+
@test !occursin("BAZ_RAN", output)
2148+
2149+
# Test 5: Bare args without --files= should include all files
2150+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file foo bar`, String)
2151+
@test occursin("FOO_RAN", output)
2152+
@test occursin("BAR_RAN", output)
2153+
@test occursin("BAZ_RAN", output)
2154+
2155+
# Test 6: --files-regex= with pattern
2156+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file --files-regex=foo\|bar`, String)
2157+
@test occursin("FOO_RAN", output)
2158+
@test occursin("BAR_RAN", output)
2159+
@test !occursin("BAZ_RAN", output)
2160+
2161+
# Test 7: --files-regex= with anchored pattern
2162+
output = read(`$(Base.julia_cmd()) --startup-file=no $test_file --files-regex=^test_foo`, String)
2163+
@test occursin("FOO_RAN", output)
2164+
@test !occursin("BAR_RAN", output)
2165+
@test !occursin("BAZ_RAN", output)
2166+
end

test/reflection.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ let
193193
:TestModSub9475, :d7648, :eval, :f7648, :include])
194194
imported = Set(Symbol[:convert, :curmod_name, :curmod])
195195
usings_from_Test = Set(Symbol[
196-
Symbol("@inferred"), Symbol("@test"), Symbol("@test_broken"), Symbol("@test_deprecated"),
196+
Symbol("@inferred"), Symbol("@include_files"), Symbol("@test"), Symbol("@test_broken"), Symbol("@test_deprecated"),
197197
Symbol("@test_logs"), Symbol("@test_nowarn"), Symbol("@test_skip"), Symbol("@test_throws"),
198198
Symbol("@test_warn"), Symbol("@testset"), :GenericArray, :GenericDict, :GenericOrder,
199199
:GenericSet, :GenericString, :LogRecord, :Test, :TestLogger, :TestSetException,

0 commit comments

Comments
 (0)