From 4d847c8895fbe88771dd0b539468d4c711c2c366 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 28 Jul 2026 11:24:12 -0400 Subject: [PATCH 1/2] Migrate test infrastructure to Pkg workspaces Replace the legacy `[extras]`/`[targets]` mechanism with the Julia 1.12 workspace feature, matching TensorKit.jl and MPSKit.jl: - Root `Project.toml` declares `[workspace] projects = ["test", "docs"]` and no longer carries test-only deps or compat bounds. - New `test/Project.toml` (`MatrixAlgebraKitTests`) owns the test deps and resolves the parent through `[sources]`. `SafeTestsets` is dropped; it was listed but used nowhere. - `docs/Project.toml` gains a name, `[sources]`, and a Documenter bound, so `docs/make.jl` no longer needs to `Pkg.develop` the parent. All workspace members share a single root `Manifest.toml`, and `julia --project=test test/runtests.jl` now works directly. Julia compat stays at 1.10: older Pkg ignores `[workspace]`/`[sources]` and builds the test environment from `test/Project.toml`, so `Pkg.test()` and CI are unaffected. Verified green on both 1.12 and 1.10. Also fixes three latent issues found along the way: - The Buildkite skips in `runtests.jl` used bare names, but discovered test keys are path-relative, so all five were silently no-ops. They are now path-qualified. `common/exponential` and `common/projections` are deliberately not skipped -- unlike the others they carry GPU coverage. - `test/exponential.jl` sat at the test root, and CI builds its matrix from top-level directories, so it never ran on GitHub. Moved to `test/common/`. - Removed `test/utilities.jl`, which was never included or called. Adds `test/README.md` documenting the suite layout, the shared TestSuite module, and how to select tests with ParallelTestRunner, plus dependabot entries for the new member projects. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 5 +- Project.toml | 32 +----- docs/Project.toml | 8 ++ docs/make.jl | 1 - test/Project.toml | 43 ++++++++ test/README.md | 175 +++++++++++++++++++++++++++++++ test/{ => common}/exponential.jl | 0 test/runtests.jl | 11 +- test/utilities.jl | 3 - 9 files changed, 238 insertions(+), 40 deletions(-) create mode 100644 test/Project.toml create mode 100644 test/README.md rename test/{ => common}/exponential.jl (100%) delete mode 100644 test/utilities.jl diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a49032f8a..f1168e0e2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,10 @@ updates: schedule: interval: "weekly" - package-ecosystem: "julia" - directory: "/" + directories: # workspace members each need their own entry + - "/" + - "/test" + - "/docs" schedule: interval: "weekly" groups: # uncomment to group all julia package updates into a single PR diff --git a/Project.toml b/Project.toml index bf0b6b4f6..afbc3531d 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,9 @@ uuid = "6c742aac-3347-4629-af66-fc926824e5e4" version = "0.6.9" authors = ["Jutho Haegeman , Lukas Devos, Katharine Hyatt and contributors"] +[workspace] +projects = ["test", "docs"] + [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" @@ -27,41 +30,12 @@ MatrixAlgebraKitMooncakeExt = "Mooncake" [compat] AMDGPU = "2" -Aqua = "0.6, 0.7, 0.8" CUDA = "6" ChainRulesCore = "1" -ChainRulesTestUtils = "1" Enzyme = "0.13.148" -EnzymeTestUtils = "0.2.5" GenericLinearAlgebra = "0.3.19, 0.4" GenericSchur = "0.5.6" LinearAlgebra = "1" PrecompileTools = "1" Mooncake = "0.5.27" -ParallelTestRunner = "2" -Random = "1" -SafeTestsets = "0.1" -StableRNGs = "1" -Test = "1" -TestExtras = "0.3.2" -Zygote = "0.7" julia = "1.10" - -[extras] -AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e" -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" -Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" -EnzymeTestUtils = "12d8515a-0907-448a-8884-5fe00fdf1c5a" -Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" -ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" -Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" - -[targets] -test = ["Aqua", "SafeTestsets", "Test", "TestExtras", "ChainRulesCore", "ChainRulesTestUtils", "Random", "StableRNGs", "Zygote", "CUDA", "AMDGPU", "GenericLinearAlgebra", "GenericSchur", "Mooncake", "Enzyme", "EnzymeTestUtils", "ParallelTestRunner"] diff --git a/docs/Project.toml b/docs/Project.toml index 54011b9f4..a4cfeeb30 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,11 @@ +name = "MatrixAlgebraKitDocs" + [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" + +[sources] +MatrixAlgebraKit = {path = ".."} + +[compat] +Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index a92d1d8b0..6c447b393 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,7 +2,6 @@ if Base.active_project() != joinpath(@__DIR__, "Project.toml") using Pkg Pkg.activate(@__DIR__) - Pkg.develop(PackageSpec(; path = joinpath(@__DIR__, ".."))) Pkg.resolve() Pkg.instantiate() end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 000000000..d5842641b --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,43 @@ +name = "MatrixAlgebraKitTests" + +[deps] +AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +EnzymeTestUtils = "12d8515a-0907-448a-8884-5fe00fdf1c5a" +GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a" +GenericSchur = "c145ed77-6b09-5dd9-b285-bf645a82121e" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" +Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[sources] +MatrixAlgebraKit = {path = ".."} + +[compat] +AMDGPU = "2" +Aqua = "0.6, 0.7, 0.8" +CUDA = "6" +ChainRulesCore = "1" +ChainRulesTestUtils = "1" +Enzyme = "0.13.148" +EnzymeTestUtils = "0.2.5" +GenericLinearAlgebra = "0.3.19, 0.4" +GenericSchur = "0.5.6" +LinearAlgebra = "1" +Mooncake = "0.5.27" +ParallelTestRunner = "2" +Random = "1" +StableRNGs = "1" +Test = "1" +TestExtras = "0.3.2" +Zygote = "0.7" diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..18eb02a1d --- /dev/null +++ b/test/README.md @@ -0,0 +1,175 @@ +# MatrixAlgebraKit.jl test suite + +Tests are driven by [ParallelTestRunner.jl](https://github.com/JuliaTesting/ParallelTestRunner.jl). +Every `.jl` file under `test/` is auto-discovered and executed in its own worker process, so test +files share no state and must be self-contained. + +The test environment is a [Pkg workspace](https://pkgdocs.julialang.org/dev/workspaces/) member: +`test/Project.toml` declares the test dependencies and resolves the parent package through +`[sources]`, while the whole workspace shares a single `Manifest.toml` at the repository root. + +## Running tests + +```julia +# Standard — works on all supported Julia versions +using Pkg; Pkg.test() + +# With arguments (see "Selecting tests" below) +using Pkg; Pkg.test("MatrixAlgebraKit"; test_args = ["decompositions", "--fast"]) +``` + +```bash +# Direct invocation — requires Julia 1.12+ (workspace support) +julia --project=test test/runtests.jl +``` + +The direct form is the convenient one for local iteration: it reuses the already-instantiated +workspace environment instead of building a fresh temporary one on every run. + +## Selecting tests + +Each discovered test is keyed by its path relative to `test/`, with the `.jl` extension stripped — +for example `decompositions/svd`, `common/truncate`, `mooncake/qr`. Positional arguments are matched +against those keys with `startswith`, so a directory name selects a whole group and a longer path +selects a single file: + +```bash +julia --project=test test/runtests.jl --list # print all discovered test keys + +julia --project=test test/runtests.jl decompositions # one group +julia --project=test test/runtests.jl decompositions/svd # one file +julia --project=test test/runtests.jl mooncake enzyme # union of several selectors +``` + +Available flags: + +| Flag | Effect | +|------|--------| +| `--list` | List all discovered tests and exit | +| `--fast` | Reduced test matrix (see below) | +| `--jobs=N` | Use `N` worker processes (default: derived from CPU count and available memory) | +| `--verbose` | More detailed progress output | +| `--quickfail` | Abort the whole run as soon as one test errors | +| `--help` | Usage information | + +Note that passing an explicit selector **disables** the automatic OS- and CI-based filtering in +`runtests.jl` — `filter_tests!` returns `false` once positional arguments are present, so you get +exactly the tests you asked for. + +### Fast mode (`--fast`) + +Sets a `fast_tests = true` constant in every test sandbox. Files branch on +`@isdefined(fast_tests) && fast_tests` to shrink their element-type matrix, typically from +`(Float32, Float64, ComplexF32, ComplexF64)` plus generic floats down to `(Float64, ComplexF64)` and +one generic float. CI uses this for draft pull requests. + +## Test groups + +The top-level directories under `test/` are the test groups, and they are exactly the matrix jobs of +the CI workflow — `.github/workflows/Tests.yml` delegates to +`QuantumKitHub/QuantumKitHubActions/.github/workflows/TestGroups.yml`, which discovers groups by +listing those directories. Adding a new directory therefore adds a new CI job automatically; a test +file placed directly in `test/` would belong to no group and never run in CI. + +| Group | Contents | +|-------|----------| +| `common` | Algorithm selection and defaults, truncation strategies, projections, matrix exponential, Aqua code-quality checks | +| `decompositions` | `qr`, `lq`, `svd`, `eig`, `eigh`, `gen_eig`, `schur`, `polar`, `orthnull` on CPU and GPU array types | +| `chainrules` | ChainRulesCore rules, exercised through ChainRulesTestUtils and Zygote | +| `mooncake` | Mooncake AD rules | +| `enzyme` | Enzyme AD rules, exercised through EnzymeTestUtils | +| `genericlinearalgebra` | `MatrixAlgebraKitGenericLinearAlgebraExt` | +| `genericschur` | `MatrixAlgebraKitGenericSchurExt` | + +Two directories are *not* groups: `testsuite/` holds the shared implementation described below and +is excluded both from discovery (in `runtests.jl`) and from CI (`exclude: '["testsuite"]'`). + +## `testsuite/` — the shared test suite + +`test/testsuite/TestSuite.jl` defines the `TestSuite` module, a backend-parametric suite modelled on +GPUArrays.jl and intended to be reusable by packages that build on MatrixAlgebraKit. It contains the +actual assertions; the files under the group directories are thin drivers that pick element types +and array types and call into it. + +Because it is excluded from discovery, it is loaded explicitly by each test file that needs it: + +```julia +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite +``` + +The suite exposes `test_*` entry points, one per operation, taking a *matrix type* and a size: + +```julia +TestSuite.test_qr(Matrix{Float64}, (54, 37)) +TestSuite.test_qr(CuMatrix{ComplexF64}, (54, 37); test_pivoted = false, test_blocksize = false) +TestSuite.test_qr(Diagonal{Float64, CuVector{Float64}}, 54) +TestSuite.test_qr_algs(Matrix{Float64}, (54, 37), (Householder(; positive = true),)) +``` + +The AD entry points (`test_chainrules`, `test_mooncake_*`, `test_enzyme_*`) instead take a scalar +element type and a size, plus `atol`/`rtol` keywords: + +```julia +TestSuite.test_chainrules(ComplexF64, (19, 17); atol = tol, rtol = tol) +``` + +Supporting infrastructure in the module: + +- `instantiate_matrix(AT, size)` — builds a random matrix of the requested type, with methods for + `Array`, `CuArray`, `ROCArray`, `Diagonal`, and `Diagonal` wrapping GPU vectors. This is the single + hook a new backend needs to implement. +- `rng` / `seed_rng!(seed)` — a shared `StableRNG(123)`. Drivers call `TestSuite.seed_rng!(123)` + before each parameter combination so results are reproducible independent of test ordering. +- `precision(T)` — the default tolerance, `sqrt(eps(real(T)))`. +- Predicates used throughout the assertions: `isleftnull`, `isrightnull`, `isleftcomplete`, + `isrightcomplete`, `has_positive_diagonal`. +- `instantiate_unitary`, `instantiate_rank_deficient_matrix` — inputs with prescribed structure. + +`test/linearmap.jl` is a further helper, defining a `LinearMap` wrapper that is deliberately *not* an +`AbstractMatrix`, used to check the generic code paths. It is excluded from discovery and included +directly where needed. + +## Hardware and platform gating + +GPU tests are guarded at runtime rather than by dependency availability: CUDA and AMDGPU are +unconditional dependencies of the test environment, and the tests themselves are wrapped in +`CUDA.functional()` / `AMDGPU.functional()` blocks. Running the suite on a machine without a GPU +therefore skips them silently. + +`runtests.jl` applies two further filters, both only when no explicit selector was given: + +- **`BUILDKITE=true`** (the GPU CI runners) drops the files that carry no GPU coverage — + `common/algorithms`, `common/codequality`, `common/truncate`, `decompositions/gen_eig`, and the + `chainrules` group — so GPU runners spend their time on GPU code paths. +- **macOS CI** drops the `mooncake` and `chainrules` groups; **macOS and Windows CI** drop the + `enzyme` group. These reflect AD backends that are unsupported or unreliable on those platforms. + +## Adding a test + +Create a `.jl` file inside one of the group directories. It is picked up automatically, runs in a +fresh worker process, and must therefore load everything it needs itself: + +```julia +using MatrixAlgebraKit +using Test +using CUDA, AMDGPU + +@isdefined(TestSuite) || include("../testsuite/TestSuite.jl") +using .TestSuite + +if @isdefined(fast_tests) && fast_tests + eltypes = (Float64, ComplexF64) +else + eltypes = (Float32, Float64, ComplexF32, ComplexF64) +end + +for T in eltypes + TestSuite.seed_rng!(123) + TestSuite.test_myop(Matrix{T}, (54, 37)) + CUDA.functional() && TestSuite.test_myop(CuMatrix{T}, (54, 37)) +end +``` + +Add assertions that should be shared with downstream packages to `test/testsuite/` and call them +from the driver, rather than writing them inline. diff --git a/test/exponential.jl b/test/common/exponential.jl similarity index 100% rename from test/exponential.jl rename to test/common/exponential.jl diff --git a/test/runtests.jl b/test/runtests.jl index cc2b4b1da..14384eae3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,7 +8,6 @@ testsuite = find_tests(@__DIR__) filter!(!(startswith("testsuite") ∘ first), testsuite) # remove utils -delete!(testsuite, "utilities") delete!(testsuite, "linearmap") # Parse arguments @@ -21,11 +20,11 @@ if filter_tests!(testsuite, args) # don't run all tests on GPU, only the GPU specific ones is_buildkite = get(ENV, "BUILDKITE", "false") == "true" if is_buildkite - delete!(testsuite, "algorithms") - delete!(testsuite, "truncate") - delete!(testsuite, "gen_eig") - delete!(testsuite, "chainrules") - delete!(testsuite, "codequality") + delete!(testsuite, "common/algorithms") + delete!(testsuite, "common/codequality") + delete!(testsuite, "common/truncate") + delete!(testsuite, "decompositions/gen_eig") + filter!(p -> !startswith(first(p), "chainrules/"), testsuite) else is_apple_ci = Sys.isapple() && get(ENV, "CI", "false") == "true" is_windows_ci = Sys.iswindows() && get(ENV, "CI", "false") == "true" diff --git a/test/utilities.jl b/test/utilities.jl deleted file mode 100644 index 61518b556..000000000 --- a/test/utilities.jl +++ /dev/null @@ -1,3 +0,0 @@ -function isapproxone(A) - return (size(A, 1) == size(A, 2)) && (A ≈ MatrixAlgebraKit.one!(similar(A))) -end From eb1e92771ebbf75c4e165dc2c6b0e5915975086a Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 28 Jul 2026 12:37:16 -0400 Subject: [PATCH 2/2] remove duplicate compat entries --- test/Project.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/Project.toml b/test/Project.toml index d5842641b..a518221ac 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -24,17 +24,9 @@ Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" MatrixAlgebraKit = {path = ".."} [compat] -AMDGPU = "2" Aqua = "0.6, 0.7, 0.8" -CUDA = "6" -ChainRulesCore = "1" ChainRulesTestUtils = "1" -Enzyme = "0.13.148" EnzymeTestUtils = "0.2.5" -GenericLinearAlgebra = "0.3.19, 0.4" -GenericSchur = "0.5.6" -LinearAlgebra = "1" -Mooncake = "0.5.27" ParallelTestRunner = "2" Random = "1" StableRNGs = "1"