From 0172def252da694922214cd78917ef2b696c60fb Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 28 Oct 2026 18:02:52 -0400 Subject: [PATCH 1/3] CI: use the shared QuantumKitHubActions Tests workflow Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 73 ++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 356ce20a..819ca1bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: - 'release-' tags: '*' pull_request: + types: [opened, synchronize, reopened, closed] workflow_dispatch: concurrency: @@ -15,9 +16,7 @@ concurrency: jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - runs-on: ${{ matrix.os }} - timeout-minutes: 90 + name: "Tests" strategy: fail-fast: false matrix: @@ -26,51 +25,29 @@ jobs: - '1' # latest stable 1.x release of Julia os: - ubuntu-latest - - macOS-latest + - macos-latest - windows-latest - arch: - - x64 - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v3 - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest - env: - JULIA_NUM_THREADS: 4 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v7 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: lcov.info + # macOS and Windows only with the latest Julia version + exclude: + - {version: 'lts', os: macos-latest} + - {version: 'lts', os: windows-latest} + uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main" + with: + julia-version: "${{ matrix.version }}" + os: "${{ matrix.os }}" + nthreads: 4 + timeout-minutes: 90 + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-nightly: needs: test - name: Julia nightly - ${{ matrix.os }} - ${{ matrix.arch }} - runs-on: ${{ matrix.os }} - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - version: - - 'nightly' - os: - - ubuntu-latest - - macOS-latest - - windows-latest - arch: - - x64 - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v3 - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest - continue-on-error: true - env: - JULIA_NUM_THREADS: 4 + name: "Tests (nightly)" + uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main" + with: + julia-version: "nightly" + nthreads: 4 + timeout-minutes: 90 + coverage: false + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 41318954b8a946846fbf5639be327a348a854e34 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 28 Oct 2026 19:29:45 -0400 Subject: [PATCH 2/3] Split the tests into groups for the shared TestGroups workflow Run the tests with ParallelTestRunner.jl, with one folder per group under test/, and use the shared QuantumKitHubActions TestGroups workflow in CI. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 24 +--- Project.toml | 4 +- test/{ad.jl => ad/chainrules.jl} | 0 test/{ => ad}/enzyme.jl | 0 test/{ => ad}/mooncake.jl | 0 test/{ => core}/allocator.jl | 0 test/core/aqua.jl | 3 + test/{ => core}/auxiliary.jl | 0 test/{ => core}/macro_kwargs.jl | 0 test/{ => core}/methods.jl | 0 test/{ => core}/polynomials.jl | 0 test/{ => core}/tensor.jl | 0 test/{ => core}/tensoropt.jl | 0 test/{butensor.jl => extensions/bumper.jl} | 2 +- test/{ => extensions}/cutensor.jl | 2 +- test/{gpu.jl => extensions/gpuarrays.jl} | 1 + test/{ => extensions}/tblis.jl | 0 test/extensions/tblis_unsupported.jl | 11 ++ test/runtests.jl | 150 ++++++++------------- 19 files changed, 81 insertions(+), 116 deletions(-) rename test/{ad.jl => ad/chainrules.jl} (100%) rename test/{ => ad}/enzyme.jl (100%) rename test/{ => ad}/mooncake.jl (100%) rename test/{ => core}/allocator.jl (100%) create mode 100644 test/core/aqua.jl rename test/{ => core}/auxiliary.jl (100%) rename test/{ => core}/macro_kwargs.jl (100%) rename test/{ => core}/methods.jl (100%) rename test/{ => core}/polynomials.jl (100%) rename test/{ => core}/tensor.jl (100%) rename test/{ => core}/tensoropt.jl (100%) rename test/{butensor.jl => extensions/bumper.jl} (99%) rename test/{ => extensions}/cutensor.jl (99%) rename test/{gpu.jl => extensions/gpuarrays.jl} (99%) rename test/{ => extensions}/tblis.jl (100%) create mode 100644 test/extensions/tblis_unsupported.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 819ca1bf..59174aa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - 'release-' tags: '*' pull_request: - types: [opened, synchronize, reopened, closed] + types: [opened, synchronize, reopened, ready_for_review, closed] workflow_dispatch: concurrency: @@ -17,26 +17,14 @@ concurrency: jobs: test: name: "Tests" - strategy: - fail-fast: false - matrix: - version: - - 'lts' # julia lts - - '1' # latest stable 1.x release of Julia - os: - - ubuntu-latest - - macos-latest - - windows-latest - # macOS and Windows only with the latest Julia version - exclude: - - {version: 'lts', os: macos-latest} - - {version: 'lts', os: windows-latest} - uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/Tests.yml@main" + uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/TestGroups.yml@main" with: - julia-version: "${{ matrix.version }}" - os: "${{ matrix.os }}" + fast: ${{ github.event.pull_request.draft == true }} nthreads: 4 timeout-minutes: 90 + # Enzyme plays a large role here, so also test macOS and Windows on PRs, once ubuntu passes + extra-os-on-prs: true + staged: ${{ github.event_name == 'pull_request' }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Project.toml b/Project.toml index f82ce567..31c63a38 100644 --- a/Project.toml +++ b/Project.toml @@ -58,6 +58,7 @@ LinearAlgebra = "1.6" Logging = "1.6" Mooncake = "0.5" PackageExtensionCompat = "1" +ParallelTestRunner = "2" PrecompileTools = "1.1" Preferences = "1.4" PtrArrays = "1.2" @@ -86,10 +87,11 @@ EnzymeTestUtils = "12d8515a-0907-448a-8884-5fe00fdf1c5a" JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" TBLIS = "48530278-0828-4a49-9772-0f3830dfa1e9" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" cuTENSOR = "011b41b2-24ef-40a8-b3eb-fa098493e9e1" [targets] -test = ["Test", "Random", "DynamicPolynomials", "ChainRulesTestUtils", "ChainRulesCore", "cuRAND", "CUDACore", "cuTENSOR", "Aqua", "Logging", "Bumper", "Mooncake", "Enzyme", "EnzymeTestUtils", "Adapt", "JLArrays", "AMDGPU", "TBLIS"] +test = ["Test", "Random", "DynamicPolynomials", "ChainRulesTestUtils", "ChainRulesCore", "cuRAND", "CUDACore", "cuTENSOR", "Aqua", "Logging", "Bumper", "Mooncake", "Enzyme", "EnzymeTestUtils", "Adapt", "JLArrays", "AMDGPU", "TBLIS", "ParallelTestRunner"] diff --git a/test/ad.jl b/test/ad/chainrules.jl similarity index 100% rename from test/ad.jl rename to test/ad/chainrules.jl diff --git a/test/enzyme.jl b/test/ad/enzyme.jl similarity index 100% rename from test/enzyme.jl rename to test/ad/enzyme.jl diff --git a/test/mooncake.jl b/test/ad/mooncake.jl similarity index 100% rename from test/mooncake.jl rename to test/ad/mooncake.jl diff --git a/test/allocator.jl b/test/core/allocator.jl similarity index 100% rename from test/allocator.jl rename to test/core/allocator.jl diff --git a/test/core/aqua.jl b/test/core/aqua.jl new file mode 100644 index 00000000..8d9ee87e --- /dev/null +++ b/test/core/aqua.jl @@ -0,0 +1,3 @@ +using TensorOperations +using Aqua +Aqua.test_all(TensorOperations) diff --git a/test/auxiliary.jl b/test/core/auxiliary.jl similarity index 100% rename from test/auxiliary.jl rename to test/core/auxiliary.jl diff --git a/test/macro_kwargs.jl b/test/core/macro_kwargs.jl similarity index 100% rename from test/macro_kwargs.jl rename to test/core/macro_kwargs.jl diff --git a/test/methods.jl b/test/core/methods.jl similarity index 100% rename from test/methods.jl rename to test/core/methods.jl diff --git a/test/polynomials.jl b/test/core/polynomials.jl similarity index 100% rename from test/polynomials.jl rename to test/core/polynomials.jl diff --git a/test/tensor.jl b/test/core/tensor.jl similarity index 100% rename from test/tensor.jl rename to test/core/tensor.jl diff --git a/test/tensoropt.jl b/test/core/tensoropt.jl similarity index 100% rename from test/tensoropt.jl rename to test/core/tensoropt.jl diff --git a/test/butensor.jl b/test/extensions/bumper.jl similarity index 99% rename from test/butensor.jl rename to test/extensions/bumper.jl index ae658d09..6db1c986 100644 --- a/test/butensor.jl +++ b/test/extensions/bumper.jl @@ -1,7 +1,7 @@ @testset "@butensor dependency check" begin @test_throws ArgumentError begin ex = :(@butensor A[a, b, c, d] := B[a, b, c, d]) - macroexpand(Main, ex) + macroexpand(@__MODULE__, ex) end end diff --git a/test/cutensor.jl b/test/extensions/cutensor.jl similarity index 99% rename from test/cutensor.jl rename to test/extensions/cutensor.jl index 7cff6c64..f1997f42 100644 --- a/test/cutensor.jl +++ b/test/extensions/cutensor.jl @@ -1,7 +1,7 @@ @testset "@cutensor dependency check" begin @test_throws ArgumentError begin ex = :(@cutensor A[a, b, c, d] := B[a, b, c, d]) - macroexpand(Main, ex) + macroexpand(@__MODULE__, ex) end end diff --git a/test/gpu.jl b/test/extensions/gpuarrays.jl similarity index 99% rename from test/gpu.jl rename to test/extensions/gpuarrays.jl index 0a49ebb9..4a95e889 100644 --- a/test/gpu.jl +++ b/test/extensions/gpuarrays.jl @@ -6,6 +6,7 @@ using TupleTools using JLArrays using VectorInterface using CUDACore, AMDGPU +using cuTENSOR # contractions of `CuArray`s require the cuTENSOR extension test_result(a::AbstractArray, b::AbstractArray; kwargs...) = isapprox(collect(a), collect(b); kwargs...) diff --git a/test/tblis.jl b/test/extensions/tblis.jl similarity index 100% rename from test/tblis.jl rename to test/extensions/tblis.jl diff --git a/test/extensions/tblis_unsupported.jl b/test/extensions/tblis_unsupported.jl new file mode 100644 index 00000000..157af022 --- /dev/null +++ b/test/extensions/tblis_unsupported.jl @@ -0,0 +1,11 @@ +# the Windows build of `tblis_jll` aborts the process from inside the library, so +# the extension refuses to call into it and there is nothing else to test +using TensorOperations +using TBLIS +using Test + +A = randn(Float64, (3, 4)) +@test_throws ArgumentError tensoradd!( + zeros(Float64, (4, 3)), A, ((2, 1), ()), false, 1, 0, + TensorOperations.TBLISBackend() +) diff --git a/test/runtests.jl b/test/runtests.jl index 10f2728f..018b1ee2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,109 +1,69 @@ +using ParallelTestRunner using TensorOperations -using LinearAlgebra -using Test -using Random -Random.seed!(1234567) -using TensorOperations: IndexError -using TensorOperations: BaseCopy, BaseView, StridedNative, StridedBLAS -using TensorOperations: DefaultAllocator, ManualAllocator, BufferAllocator +testsuite = find_tests(@__DIR__) -precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 -precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-8 +# --fast is passed by the CI on draft PRs, but there are no slow tests to skip +args = parse_args(ARGS; custom = ["fast"]) +filter_tests!(testsuite, args) -# https://github.com/QuantumKitHub/TensorOperations.jl/issues/280: the generated code has to -# keep the user's `LineNumberNode`s -- Julia only emits a coverage counter for lines that a -# `LineNumberNode` points at -- and drop the ones synthesized by the parser, which would -# otherwise re-attribute the statements that follow them to a line in TensorOperations itself. -# Only `LineNumberNode`s in block-statement position matter for that: elsewhere (most notably -# the mandatory 2nd argument of a `:macrocall`) they are structurally required and left alone. -function statementlinenumbernodes(ex, acc = LineNumberNode[]) - if ex isa Expr - if ex.head === :block - for e in ex.args - e isa LineNumberNode && push!(acc, e) - end - end - foreach(e -> statementlinenumbernodes(e, acc), ex.args) - end - return acc +# don't run all tests on GPU, only the GPU specific ones +if get(ENV, "BUILDKITE", "false") == "true" + filter!(in(("extensions/cutensor", "extensions/gpuarrays")) ∘ first, testsuite) end +Sys.isapple() && get(ENV, "CI", "false") == "true" && delete!(testsuite, "ad/enzyme") +# the Windows build of TBLIS can only be tested for refusing to run +delete!(testsuite, Sys.iswindows() ? "extensions/tblis" : "extensions/tblis_unsupported") -# don't run all tests on GPU, only the GPU -# specific ones -is_buildkite = get(ENV, "BUILDKITE", "false") == "true" -if !is_buildkite - @testset "tensoropt" verbose = true begin - include("tensoropt.jl") - end - @testset "auxiliary" verbose = true begin - include("auxiliary.jl") - end - @testset "macro keywords" verbose = true begin - include("macro_kwargs.jl") - end - @testset "method syntax" verbose = true begin - include("methods.jl") - end - @testset "macro with index notation" verbose = true begin - include("tensor.jl") - end - @testset "allocator" verbose = true begin - include("allocator.jl") - end - @testset "ad" verbose = false begin - include("ad.jl") - end - @testset "mooncake" verbose = false begin - include("mooncake.jl") - end - is_apple_ci = Sys.isapple() && get(ENV, "CI", "false") == "true" - if !is_apple_ci - @testset "enzyme" verbose = false begin - include("enzyme.jl") - end - end +# The cuTENSOR and Bumper tests first check that the macros requiring them throw when they are +# not loaded, so they run on a fresh worker rather than one that may have loaded them already +const fresh_worker_tests = ("extensions/cutensor", "extensions/bumper") +const fresh_workers = Channel{ParallelTestRunner.PTRWorker}(Inf) +function test_worker(name) + name in fresh_worker_tests || return nothing + worker = addworker() + put!(fresh_workers, worker) + return worker end -# note: cuTENSOR should not be loaded before this point -# as there is a test which requires it to be loaded after -@testset "cuTENSOR extension" verbose = true begin - include("cutensor.jl") -end -@testset "GPUArrays" verbose = true begin - include("gpu.jl") -end +const init_code = quote + using TensorOperations + using LinearAlgebra + using Test + using Random + Random.seed!(1234567) -if !is_buildkite - # note: Bumper should not be loaded before this point - # as there is a test which requires it to be loaded after - @testset "Bumper extension" verbose = true begin - include("butensor.jl") - end + const is_buildkite = get(ENV, "BUILDKITE", "false") == "true" - if Sys.iswindows() - # the Windows build of `tblis_jll` aborts the process from inside the library, so - # the extension refuses to call into it and there is nothing else to test - using TBLIS - @testset "TBLIS extension (unsupported platform)" begin - A = randn(Float64, (3, 4)) - @test_throws ArgumentError tensoradd!( - zeros(Float64, (4, 3)), A, ((2, 1), ()), false, 1, 0, - TensorOperations.TBLISBackend() - ) - end - else - @testset "TBLIS extension" verbose = true begin - include("tblis.jl") - end - end + using TensorOperations: IndexError + using TensorOperations: BaseCopy, BaseView, StridedNative, StridedBLAS + using TensorOperations: DefaultAllocator, ManualAllocator, BufferAllocator - @testset "Polynomials" begin - include("polynomials.jl") - end + precision(::Type{<:Union{Float32, Complex{Float32}}}) = 1.0e-2 + precision(::Type{<:Union{Float64, Complex{Float64}}}) = 1.0e-8 - @testset "Aqua" verbose = true begin - using Aqua - Aqua.test_all(TensorOperations) + # https://github.com/QuantumKitHub/TensorOperations.jl/issues/280: the generated code has to + # keep the user's `LineNumberNode`s -- Julia only emits a coverage counter for lines that a + # `LineNumberNode` points at -- and drop the ones synthesized by the parser, which would + # otherwise re-attribute the statements that follow them to a line in TensorOperations itself. + # Only `LineNumberNode`s in block-statement position matter for that: elsewhere (most notably + # the mandatory 2nd argument of a `:macrocall`) they are structurally required and left alone. + function statementlinenumbernodes(ex, acc = LineNumberNode[]) + if ex isa Expr + if ex.head === :block + for e in ex.args + e isa LineNumberNode && push!(acc, e) + end + end + foreach(e -> statementlinenumbernodes(e, acc), ex.args) + end + return acc end end + +try + runtests(TensorOperations, args; testsuite, init_code, test_worker) +finally + close(fresh_workers) + foreach(ParallelTestRunner.Malt.stop, fresh_workers) +end From f18a0b794363b99eadb03f672eee69d8e6d13aab Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 29 Oct 2026 09:56:42 -0400 Subject: [PATCH 3/3] Run the Enzyme tests on macOS CI Co-Authored-By: Claude Opus 5.5 --- test/runtests.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 018b1ee2..739de840 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,7 +11,6 @@ filter_tests!(testsuite, args) if get(ENV, "BUILDKITE", "false") == "true" filter!(in(("extensions/cutensor", "extensions/gpuarrays")) ∘ first, testsuite) end -Sys.isapple() && get(ENV, "CI", "false") == "true" && delete!(testsuite, "ad/enzyme") # the Windows build of TBLIS can only be tested for refusing to run delete!(testsuite, Sys.iswindows() ? "extensions/tblis" : "extensions/tblis_unsupported")