diff --git a/.github/workflows/DocPreviewsCleanup.yml b/.github/workflows/DocPreviewsCleanup.yml new file mode 100644 index 0000000..7037f1c --- /dev/null +++ b/.github/workflows/DocPreviewsCleanup.yml @@ -0,0 +1,32 @@ +name: Doc Previews Cleanup + +on: + schedule: + - cron: "25 22 * * 2" + workflow_dispatch: + +concurrency: + # Same group concurrency as the `Documentation.yml` workflow, because they both + # git-push to the same branch, so we want to avoid clashes. + group: docs-pushing + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v6 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [[ -d previews ]]; then + git config user.name "${{github.actor}}" + git config user.email "${{github.actor_id}}+${{github.actor}}@users.noreply.github.com" + git rm -rf previews/ + git commit -m 'Delete previews directory' + git branch gh-pages-new $(echo "Delete history" | git commit-tree HEAD^{tree}) + git push --force-with-lease origin gh-pages-new:gh-pages + fi diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml new file mode 100644 index 0000000..a12d6cf --- /dev/null +++ b/.github/workflows/Documentation.yml @@ -0,0 +1,108 @@ +name: Documentation + +on: + push: + branches: + - main + tags: "*" + paths: + - ".github/workflows/Documentation.yml" + - "docs/**" + - "src/**" + - "Project.toml" + pull_request: + paths: + - ".github/workflows/Documentation.yml" + - "docs/**" + - "src/**" + - "Project.toml" + release: + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: always. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-docs: + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + id: julia-cache + with: + cache-name: Documentation + - name: Instantiate docs environment + shell: julia --color=yes --project=docs {0} + run: | + using Pkg + Pkg.instantiate() + - name: Build documentation + run: + julia --color=yes --project=docs docs/make.jl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Save Julia depot cache on cancel or failure + id: julia-cache-save + if: cancelled() || failure() + uses: actions/cache/save@v4 + with: + path: | + ${{ steps.julia-cache.outputs.cache-paths }} + key: ${{ steps.julia-cache.outputs.cache-key }} + - uses: actions/upload-artifact@v5 + with: + name: documentation-build + path: docs/build + retention-days: 10 + + deploy-docs: + # Deploy docs only if triggers is not a PR, or a PR not from a fork. + if: ${{ (github.event_name != 'pull_request') || (! github.event.pull_request.head.repo.fork) }} + needs: build-docs + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + # Have only one job pushing the docs at a time. + group: docs-pushing + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + with: + # Steal cache from the build job + cache-name: Documentation + - uses: actions/download-artifact@v6 + with: + name: documentation-build + path: docs/build + - name: Instantiate docs environment + shell: julia --color=yes {0} + run: | + # We only need `Documenter` for publishing the docs, let's not + # reinstall the world all over again. + using Pkg + Pkg.add(; name="Documenter", version="1") + - name: Deploy documentation + run: + julia --color=yes docs/deploy.jl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore index 2251642..6e24bde 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -Manifest.toml \ No newline at end of file +Manifest.toml +/docs/build/ diff --git a/README.md b/README.md index 0e923a8..dccb64f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # ParallelTestRunner.jl +[![Stable Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliatesting.github.io/ParallelTestRunner.jl/) +[![Latest Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://juliatesting.github.io/ParallelTestRunner.jl/dev) + Simple parallel test runner for Julia tests with autodiscovery. ## Usage @@ -40,102 +43,9 @@ using ParallelTestRunner runtests(MyModule, ARGS) ``` -### Customizing the test suite - -By default, `runtests` automatically discovers all `.jl` files in your `test/` directory (excluding `runtests.jl` itself) using the `find_tests` function. You can customize which tests to run by providing a custom `testsuite` dictionary: - -```julia -# Manually define your test suite -testsuite = Dict( - "basic" => quote - include("basic.jl") - end, - "advanced" => quote - include("advanced.jl") - end -) - -runtests(MyModule, ARGS; testsuite) -``` - -You can also use `find_tests` to automatically discover tests and then filter or modify them. This requires manually parsing arguments so that filtering is only applied when the user did not request specific tests to run: - -```julia -# Start with autodiscovered tests -testsuite = find_tests(pwd()) - -# Parse arguments -args = parse_args(ARGS) - -if filter_tests!(testsuite, args) - # Remove tests that shouldn't run on Windows - if Sys.iswindows() - delete!(testsuite, "ext/specialfunctions") - end -end - -runtests(MyModule, args; testsuite) -``` - -### Provide defaults - -`runtests` takes a keyword argument that one can use to provide default definitions to be loaded before each testfile. -As an example one could always load `Test` and the package under test. - -```julia -const init_code = quote - using Test - using MyPackage -end - -runtests(MyModule, ARGS; init_code) -``` - -### Interactive use - -Arguments can also be passed via the standard `Pkg.test` interface for interactive control. For example, here is how we could run the subset of tests that start with the testset name "MyTestsetA" in i) verbose mode, and ii) with default threading enabled: - -```julia-repl -# In an environment where `MyPackage.jl` is available -julia --proj - -julia> using Pkg - -# No need to start a fresh session to change threading -julia> Pkg.test("MyModule"; test_args=`--verbose MyTestsetA`, julia_args=`--threads=auto`); -``` -Alternatively, arguments can be passed directly from the command line with a shell alias like the one below: - -```julia-repl -jltest --threads=auto -- --verbose MyTestsetA -``` - -
Shell alias - -```shell -function jltest { - julia=(julia) - - # certain arguments (like those beginnning with a +) need to come first - if [[ $# -gt 0 && "$1" = +* ]]; then - julia+=("$1") - shift - fi - - "${julia[@]}" --startup-file=no --project -e "using Pkg; Pkg.API.test(; test_args=ARGS)" "$@" -} -``` - -
- -## Packages using ParallelTestRunner.jl - -There are a few packages already using `ParallelTestRunner.jl` to parallelize their tests, you can look at their setups if you need inspiration to move your packages as well: +## Documentation -* [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl/blob/main/test/runtests.jl) -* [`GPUArrays.jl`](https://github.com/JuliaGPU/GPUArrays.jl/blob/master/test/runtests.jl) -* [`GPUCompiler.jl`](https://github.com/JuliaGPU/GPUCompiler.jl/blob/master/test/runtests.jl) -* [`Metal.jl`](https://github.com/JuliaGPU/Metal.jl/blob/main/test/runtests.jl) +For more details about the use of this package, read the [documentation](https://juliatesting.github.io/ParallelTestRunner.jl/). ## Inspiration Based on [@maleadt](https://github.com/maleadt) test infrastructure for [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl). diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..d5fab23 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,9 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" + +[compat] +Documenter = "1" + +[sources] +ParallelTestRunner = {path = ".."} diff --git a/docs/deploy.jl b/docs/deploy.jl new file mode 100644 index 0000000..86c2031 --- /dev/null +++ b/docs/deploy.jl @@ -0,0 +1,9 @@ +using Documenter + +deploydocs(; + repo="github.com/JuliaTesting/ParallelTestRunner.jl", + devbranch="main", + # Only push previews if all the relevant environment variables are non-empty. This is an + # attempt to work around https://github.com/JuliaDocs/Documenter.jl/issues/2048. + push_preview = all(!isempty, (get(ENV, "GITHUB_TOKEN", ""), get(ENV, "DOCUMENTER_KEY", ""))), +) diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..e91cc2a --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,19 @@ +using Documenter +using ParallelTestRunner + +makedocs(; + modules=[ParallelTestRunner], + authors="Valentin Churavy and contributors", + repo="https://github.com/JuliaTesting/ParallelTestRunner.jl/blob/{commit}{path}#{line}", + sitename="ParallelTestRunner.jl", + format=Documenter.HTML(; + prettyurls=get(ENV, "CI", "false") == "true", + canonical="https://juliatesting.github.io/ParallelTestRunner.jl", + assets=String[], + ), + pages=[ + "Home" => "index.md", + "Advanced Usage" => "advanced.md", + "API Reference" => "api.md", + ], +) diff --git a/docs/src/advanced.md b/docs/src/advanced.md new file mode 100644 index 0000000..eef7ae8 --- /dev/null +++ b/docs/src/advanced.md @@ -0,0 +1,198 @@ +# Advanced Usage + +```@meta +CurrentModule = ParallelTestRunner +DocTestSetup = quote + using ParallelTestRunner +end +``` + +This page covers advanced features of `ParallelTestRunner` for customizing test execution. + +## Customizing the test suite + +By default, [`runtests`](@ref) automatically discovers all `.jl` files in your `test/` directory (excluding `runtests.jl` itself) using the `find_tests` function. +You can customize which tests to run by providing a custom `testsuite` dictionary: + +```julia +# Manually define your test suite +testsuite = Dict( + "basic" => quote + include("basic.jl") + end, + "advanced" => quote + include("advanced.jl") + end +) + +runtests(MyModule, ARGS; testsuite) +``` + +## Filtering Test Files + +You can also use [`find_tests`](@ref) to automatically discover test files and then filter or modify them. +This requires manually parsing arguments so that filtering is only applied when the user did not request specific tests to run: + +```julia +# Start with autodiscovered tests +testsuite = find_tests(pwd()) + +# Parse arguments +args = parse_args(ARGS) + +if filter_tests!(testsuite, args) + # Remove tests that shouldn't run on Windows + if Sys.iswindows() + delete!(testsuite, "ext/specialfunctions") + end +end + +runtests(MyModule, args; testsuite) +``` + +The [`filter_tests!`](@ref) function returns `true` if no positional arguments were provided (allowing additional filtering) and `false` if the user specified specific tests (preventing further filtering). + +## Initialization Code + +Use the `init_code` keyword argument to [`runtests`](@ref) to provide code that runs before each test file. +This is useful for: +- Importing packages +- Defining constants, defaults or helper functions +- Setting up test infrastructure + +```julia +using ParallelTestRunner + +const init_code = quote + using Test + using MyPackage + + # Define a helper function available to all tests + function test_helper(x) + return x * 2 + end +end + +runtests(MyPackage, ARGS; init_code) +``` + +The `init_code` is evaluated in each test's sandbox module, so all definitions are available to your test files. + +## Custom Workers + +For tests that require specific environment variables or Julia flags, you can use the `test_worker` keyword argument to [`runtests`](@ref) to assign tests to custom workers: + +```julia +using ParallelTestRunner + +function test_worker(name) + if name == "needs_env_var" + # Create a worker with a specific environment variable + return addworker(; env = ["SPECIAL_ENV_VAR" => "42"]) + elseif name == "needs_threads" + # Create a worker with multiple threads + return addworker(; exeflags = ["--threads=4"]) + end + # Return nothing to use the default worker + return nothing +end + +testsuite = Dict( + "needs_env_var" => quote + @test ENV["SPECIAL_ENV_VAR"] == "42" + end, + "needs_threads" => quote + @test Base.Threads.nthreads() == 4 + end, + "normal_test" => quote + @test 1 + 1 == 2 + end +) + +runtests(MyPackage, ARGS; test_worker, testsuite) +``` + +The `test_worker` function receives the test name and should return either: +- A worker object (from [`addworker`](@ref)) for tests that need special configuration +- `nothing` to use the default worker pool + +## Custom Arguments + +If your package needs to accept its own command-line arguments in addition to `ParallelTestRunner`'s options, use [`parse_args`](@ref) with custom flags: + +```julia +using ParallelTestRunner + +# Parse arguments with custom flags +args = parse_args(ARGS; custom=["myflag", "another-flag"]) + +# Access custom flags +if args.custom["myflag"] !== nothing + println("Custom flag was set!") +end + +# Pass parsed args to runtests +runtests(MyPackage, args) +``` + +Custom flags are stored in the `custom` field of the `ParsedArgs` object, with values of `nothing` (not set) or `Some(value)` (set, with optional value). + +## Interactive use + +Arguments can also be passed via the standard [`Pkg.test`](https://pkgdocs.julialang.org/v1/api/#Pkg.test) interface for interactive control. +For example, here is how we could run the subset of test files that start with the name `test_cool_feature` in i) verbose mode, and ii) with a specific number of Julia threads enabled: + +```bash +# Start julia in an environment where `MyPackage.jl` is available +julia --project +``` +```julia-repl +julia> using Pkg + +# No need to start a fresh session to change threading +julia> Pkg.test("MyModule"; test_args=`--verbose test_cool_feature`, julia_args=`--threads=auto`); +``` + +Alternatively, arguments can be passed directly from the command line with a shell alias like the one below: + +```julia-repl +jltest --threads=auto -- --verbose test_cool_feature +``` + +Shell alias: + +```bash +function jltest { + julia=(julia) + + # certain arguments (like those beginnning with a +) need to come first + if [[ $# -gt 0 && "$1" = +* ]]; then + julia+=("$1") + shift + fi + + "${julia[@]}" --startup-file=no --project -e "using Pkg; Pkg.API.test(; test_args=ARGS)" "$@" +} +``` + +## Best Practices + +1. **Keep tests isolated**: Each test file runs in its own module, so avoid relying on global state between tests. + +1. **Use `init_code` for common setup**: Instead of duplicating setup code in each test file, use `init_code` to share common initialization. + +1. **Filter tests appropriately**: Use [`filter_tests!`](@ref) to respect user-specified test filters while allowing additional programmatic filtering. + +1. **Handle platform differences**: Use conditional logic in your test suite setup to handle platform-specific tests: + + ```julia + testsuite = find_tests(pwd()) + if Sys.iswindows() + delete!(testsuite, "unix_specific_test") + end + ``` + +1. **Load balance the test files**: `ParallelTestRunner` runs the tests files in parallel, ideally all test files should run for _roughly_ the same time for better performance. + Having few long-running test files and other short-running ones hinders scalability. + +1. **Use custom workers sparingly**: Custom workers add overhead. Only use them when tests genuinely require different configurations. diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..9f7b8aa --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,46 @@ +# API Reference + +```@meta +CurrentModule = ParallelTestRunner +DocTestSetup = quote + using ParallelTestRunner +end +``` + +## Main Functions + +```@docs +runtests +``` + +## Test Discovery + +```@docs +find_tests +``` + +## Argument Parsing + +```@docs +parse_args +filter_tests! +``` + +## Worker Management + +```@docs +addworker +addworkers +``` + +## Configuration + +```@docs +default_njobs +``` + +## Internal Types + +```@docs +WorkerTestSet +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..ea35b40 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,157 @@ +# ParallelTestRunner.jl + +```@meta +CurrentModule = ParallelTestRunner +DocTestSetup = quote + using ParallelTestRunner +end +``` + +[ParallelTestRunner.jl](https://github.com/JuliaTesting/ParallelTestRunner.jl) is a simple parallel test runner for Julia tests with automatic test discovery. +It runs each test file concurrently in isolated worker processes, providing real-time progress output and efficient resource management. + +## Quick Start + +### Basic Setup + +1. **Remove existing `include` statements** from your test files. + `ParallelTestRunner` will automatically discover and run all test files. + +2. **Update your `test/runtests.jl`**: + + ```julia + using MyPackage + using ParallelTestRunner + + runtests(MyPackage, ARGS) + ``` + +That's it! `ParallelTestRunner` will automatically: +- Discover all `.jl` files in your `test/` directory (excluding `runtests.jl`) +- Run them in parallel across multiple worker processes +- Display real-time progress with timing and memory statistics + +### Running Tests + +Run tests using the standard Julia package testing interface: + +```bash +julia --project -e 'using Pkg; Pkg.test("MyPackage")' +``` + +Or from within Julia: + +```julia +using Pkg +Pkg.test("MyPackage") +``` + +## Command Line Options + +You can pass various options to the `runtests.jl` script to control test execution: + +```bash +julia --project test/runtests.jl [OPTIONS] [TESTS...] +``` + +### Available Options + +- `--help`: Show usage information and exit +- `--list`: List all available test files and exit +- `--verbose`: Print more detailed information during test execution (including start times for each test) +- `--quickfail`: Stop the entire test run as soon as any test fails +- `--jobs=N`: Use `N` worker processes (default: based on CPU threads and available memory) +- `TESTS...`: Filter test files by name, matched using `startswith` + +### Examples + +```bash +# List all available tests +julia --project test/runtests.jl --list + +# Run only tests matching "integration" +julia --project test/runtests.jl integration + +# Run with verbose output and 4 workers +julia --project test/runtests.jl --verbose --jobs=4 + +# Run with quick-fail enabled +julia --project test/runtests.jl --quickfail +``` + +### Using with Pkg.test + +You can also pass arguments through `Pkg.test`: + +```julia +using Pkg +Pkg.test("MyPackage"; test_args=`--verbose --jobs=4 integration`) +``` + +## Features + +### Automatic Test Files Discovery + +`ParallelTestRunner` automatically discovers all `.jl` files in your `test/` directory and subdirectories, excluding `runtests.jl`. + +### Parallel Execution + +Tests run concurrently in isolated worker processes, each inside own module. +`ParallelTestRunner` records historical tests duration for each package, so that in subsequent runs long-running tests are executed first, to improve load balancing. + +### Real-time Progress + +The test runner provides real-time output showing: +- Test name and worker assignment +- Execution time +- GC time and percentage +- Memory allocation +- RSS (Resident Set Size) memory usage + +Example output: + +``` +Test | Time (s) | GC (s) | GC % | Alloc (MB) | RSS (MB) | +basic (1) | 0.12 | 0.01 | 8.3 | 5.23 | 125.45 | +integration (2) | 2.45 | 0.15 | 6.1 | 45.67 | 234.12 | +``` + +### Graceful Interruption + +Press `Ctrl+C` to interrupt the test run. The framework will: +- Clean up running tests +- Display a summary of completed tests +- Exit gracefully + +## Test File Structure + +Your test files should be standard Julia test files using the `Test` standard library: + +```julia +using Test +using MyPackage + +@testset "MyPackage tests" begin + @test 1 + 1 == 2 + @test MyPackage.my_function(42) == 84 +end +``` + +Each test file runs in its own isolated module, so you don't need to worry about test pollution between files. + +## Packages using ParallelTestRunner.jl + +There are a few packages already [using `ParallelTestRunner.jl`](https://github.com/search?q=%22using+ParallelTestRunner%22+language%3AJulia++NOT+is%3Aarchived+NOT+is%3Afork+path%3A%2F%5Etest%5C%2Fruntests.jl%2F&type=code) to parallelize their tests, you can look at their setups if you need inspiration to move your packages as well: + +* [`ApproxFun.jl`](https://github.com/JuliaApproximation/ApproxFun.jl/blob/master/test/runtests.jl) +* [`BlockArrays.jl`](https://github.com/JuliaArrays/BlockArrays.jl/blob/master/test/runtests.jl) +* [`CuNESSie.jl`](https://github.com/tkemmer/CuNESSie.jl/blob/master/test/runtests.jl) +* [`Enzyme.jl`](https://github.com/EnzymeAD/Enzyme.jl/blob/main/test/runtests.jl) +* [`GPUArrays.jl`](https://github.com/JuliaGPU/GPUArrays.jl/blob/master/test/runtests.jl) +* [`GPUCompiler.jl`](https://github.com/JuliaGPU/GPUCompiler.jl/blob/master/test/runtests.jl) +* [`HyperHessians.jl`](https://github.com/KristofferC/HyperHessians.jl/blob/master/test/runtests.jl) +* [`Metal.jl`](https://github.com/JuliaGPU/Metal.jl/blob/main/test/runtests.jl) +* [`WCS.jl`](https://github.com/JuliaAstro/WCS.jl/blob/master/test/runtests.jl) + +## Inspiration +Based on [@maleadt](https://github.com/maleadt) test infrastructure for [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl). diff --git a/src/ParallelTestRunner.jl b/src/ParallelTestRunner.jl index 9b590f5..9f945d8 100644 --- a/src/ParallelTestRunner.jl +++ b/src/ParallelTestRunner.jl @@ -410,9 +410,15 @@ const WORKER_IDS = Dict{Int32, Int32}() worker_id(wrkr) = WORKER_IDS[wrkr.proc_pid] """ - addworkers(X; kwargs...) + addworkers(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing) Add `X` worker processes. +To add a single worker, use [`addworker`](@ref). + +## Arguments +- `env`: Vector of environment variable pairs to set for the worker process. +- `exename`: Custom executable to use for the worker process. +- `exeflags`: Custom flags to pass to the worker process. """ addworkers(X; kwargs...) = [addworker(; kwargs...) for _ in 1:X] @@ -420,6 +426,7 @@ addworkers(X; kwargs...) = [addworker(; kwargs...) for _ in 1:X] addworker(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing) Add a single worker process. +To add multiple workers, use [`addworkers`](@ref). ## Arguments - `env`: Vector of environment variable pairs to set for the worker process. @@ -623,11 +630,11 @@ Run Julia tests in parallel across multiple worker processes. - `mod`: The module calling runtests - `ARGS`: Command line arguments array, typically from `Base.ARGS`. When you run the tests with `Pkg.test`, this can be changed with the `test_args` keyword argument. If the caller - needs to accept args too, consider using `parse_args` to parse the arguments first. + needs to accept args too, consider using [`parse_args`](@ref) to parse the arguments first. Several keyword arguments are also supported: -- `testsuite`: Dictionary mapping test names to expressions to execute (default: `find_tests(pwd())`). +- `testsuite`: Dictionary mapping test names to expressions to execute (default: [`find_tests(pwd())`](@ref)). By default, automatically discovers all `.jl` files in the test directory. - `init_code`: Code use to initialize each test's sandbox module (e.g., import auxiliary packages, define constants, etc). @@ -642,17 +649,17 @@ Several keyword arguments are also supported: - `--verbose`: Print more detailed information during test execution - `--quickfail`: Stop the entire test run as soon as any test fails - `--jobs=N`: Use N worker processes (default: based on CPU threads and available memory) -- `TESTS...`: Filter tests by name, matched using `startswith` +- `TESTS...`: Filter test files by name, matched using `startswith` ## Behavior - Automatically discovers all `.jl` files in the test directory (excluding `runtests.jl`) -- Sorts tests by file size (largest first) for load balancing +- Sorts test files by runtime (longest-running are started first) for load balancing - Launches worker processes with appropriate Julia flags for testing - Monitors memory usage and recycles workers that exceed memory limits - Provides real-time progress output with timing and memory statistics - Handles interruptions gracefully (Ctrl+C) -- Returns nothing, but throws `Test.FallbackTestSetException` if any tests fail +- Returns `nothing`, but throws `Test.FallbackTestSetException` if any tests fail ## Examples