From f62b933665cb261b08bc8958727be0146dea9f70 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 12 Jul 2026 05:29:14 -0400 Subject: [PATCH] Add arch axis and group alias to root test-group matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root test-group matrix (test/test_groups.toml -> grouped-tests.yml) could express a group's version and OS axes but not its CPU architecture, so a package that wants a 32-bit lane had to bypass the matrix and hand-add a `tests.yml` job to its CI, defeating the point of declaring the matrix once in test_groups.toml. Add two root-matrix-only per-group keys: * `arch` — string or list of Julia architectures; the group runs once per arch (empty = the runner's native arch). Emitted as a per-cell `arch` field that grouped-tests.yml forwards to tests.yml's `julia-arch` (empty falls back to runner.arch). * `group` — the GROUP env dispatched to the package's runtests.jl, defaulting to the section name. A section named e.g. "Core 32-bit" can dispatch the "Core" body (so its folder/core file resolves in SciMLTesting folder-discovery mode) while carrying its own arch/os axis and CI job name. grouped-tests.yml forwards `julia-arch: matrix.arch` and appends the arch to the job name when set. Existing test_groups.toml files are unaffected: no `arch`/`group` keys means arch is "" (native) and the section name is the dispatched group, exactly as before. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01EcQkauMQ4KSytnTpJpXUZu --- .github/workflows/grouped-tests.yml | 3 +- scripts/compute_affected_sublibraries.jl | 48 +++++++++++++---- test/runtests.jl | 65 ++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/.github/workflows/grouped-tests.yml b/.github/workflows/grouped-tests.yml index 5744bc0..2eb53e1 100644 --- a/.github/workflows/grouped-tests.yml +++ b/.github/workflows/grouped-tests.yml @@ -104,13 +104,14 @@ jobs: fail-fast: false matrix: include: ${{ fromJson(needs.detect.outputs.matrix) }} - name: "${{ matrix.group }} (julia ${{ matrix.version }}, ${{ join(matrix.runner, ' ') }})" + name: "${{ matrix.group }} (julia ${{ matrix.version }}, ${{ join(matrix.runner, ' ') }}${{ matrix.arch != '' && format(', {0}', matrix.arch) || '' }})" uses: "SciML/.github/.github/workflows/tests.yml@v1" with: project: "." group: "${{ matrix.group }}" group-env-name: "${{ inputs.group-env-name }}" julia-version: "${{ matrix.version }}" + julia-arch: "${{ matrix.arch }}" runner: ${{ toJson(matrix.runner) }} timeout-minutes: ${{ matrix.timeout }} num-threads: "${{ matrix.num_threads }}" diff --git a/scripts/compute_affected_sublibraries.jl b/scripts/compute_affected_sublibraries.jl index 90a00b1..ea29afb 100644 --- a/scripts/compute_affected_sublibraries.jl +++ b/scripts/compute_affected_sublibraries.jl @@ -29,6 +29,17 @@ # group runs once per OS, e.g. ["ubuntu-latest","windows-latest", # "macos-latest"]). Empty -> use `runner`. Don't combine with a # custom `runner`; if both set, the OS axis wins. +# arch — string or array of Julia CPU architectures (root matrix only; +# the group runs once per arch, e.g. ["x64","x86"] to add a +# 32-bit lane). Empty -> the runner's native arch. Forwarded to +# tests.yml's `julia-arch`, so a section like `arch = ["x86"]` +# with `os = ["ubuntu-latest"]` yields a single 32-bit cell. +# group — string; the GROUP env value dispatched to the package's +# runtests.jl for this section, defaulting to the section name. +# Lets a section named e.g. "Core 32-bit" run the "Core" body +# (so its group folder / core file resolves) while carrying its +# own distinct arch/os axis and its own CI job name. Root matrix +# only. # timeout — integer, job timeout in minutes (default: 120) # num_threads — integer, JULIA_NUM_THREADS (default: 1) # local_only — boolean (default: false). When true, the group is skipped @@ -67,7 +78,7 @@ # # With the --root-matrix flag the output is the ROOT package's group matrix, # read from /test/test_groups.toml (NOT under lib/), as a JSON array of -# {group, version, runner, timeout, num_threads, continue_on_error}. This is not +# {group, version, runner, arch, timeout, num_threads, continue_on_error}. This is not # diff-filtered (the root package runs all its groups every push/PR) and needs # no lib/ directory, so ordinary single packages can use it too. When no # test/test_groups.toml exists the default is a single "Core" group on @@ -152,6 +163,8 @@ struct TestGroupConfig local_only::Bool continue_on_error::Bool os::Vector{String} # root matrix only: OS axis (group runs once per os); empty = use `runner` + arch::Vector{String} # root matrix only: arch axis (group runs once per arch); empty = runner's native arch + dispatch_group::String # root matrix only: GROUP env dispatched to runtests; empty = section name end function parse_test_group(config::AbstractDict) @@ -163,7 +176,12 @@ function parse_test_group(config::AbstractDict) local_only = Bool(get(config, "local_only", false)) continue_on_error = Bool(get(config, "continue_on_error", false)) os = convert(Vector{String}, get(config, "os", String[])) - return TestGroupConfig(versions, runner, timeout, num_threads, local_only, continue_on_error, os) + arch_raw = get(config, "arch", String[]) + arch = arch_raw isa Vector ? convert(Vector{String}, arch_raw) : String[String(arch_raw)] + dispatch_group = String(get(config, "group", "")) + return TestGroupConfig( + versions, runner, timeout, num_threads, local_only, continue_on_error, os, arch, dispatch_group, + ) end function load_test_groups(lib_dir::String, pkg::String) @@ -173,7 +191,7 @@ function load_test_groups(lib_dir::String, pkg::String) return Dict{String, TestGroupConfig}(name => parse_test_group(config) for (name, config) in toml) end return Dict{String, TestGroupConfig}( - k => TestGroupConfig(v, "ubuntu-latest", 120, 1, false, false, String[]) for (k, v) in DEFAULT_TEST_GROUPS + k => TestGroupConfig(v, "ubuntu-latest", 120, 1, false, false, String[], String[], "") for (k, v) in DEFAULT_TEST_GROUPS ) end @@ -195,7 +213,7 @@ function load_root_test_groups(repo_root::String) isempty(groups) || return groups end return Dict{String, TestGroupConfig}( - k => TestGroupConfig(v, "ubuntu-latest", 120, 1, false, false, String[]) for (k, v) in DEFAULT_ROOT_GROUPS + k => TestGroupConfig(v, "ubuntu-latest", 120, 1, false, false, String[], String[], "") for (k, v) in DEFAULT_ROOT_GROUPS ) end @@ -381,18 +399,25 @@ function build_root_matrix(repo_root::String) entries = [] for group_name in sort!(collect(keys(groups))) config = groups[group_name] + dispatch = isempty(config.dispatch_group) ? group_name : config.dispatch_group runners = isempty(config.os) ? Any[config.runner] : Any[o for o in config.os] + # Empty arch = one cell on the runner's native arch (emitted as "", which + # tests.yml falls back from to runner.arch). A non-empty list adds a cell + # per arch, e.g. ["x64", "x86"] for a 32-bit lane. + arches = isempty(config.arch) ? String[""] : config.arch vers = group_name == "QA" ? QA_VERSIONS : config.versions for ver in vers for runner in runners - push!( - entries, - (; - group = group_name, version = ver, runner = runner, - timeout = config.timeout, num_threads = config.num_threads, - continue_on_error = config.continue_on_error, + for arch in arches + push!( + entries, + (; + group = dispatch, version = ver, runner = runner, arch = arch, + timeout = config.timeout, num_threads = config.num_threads, + continue_on_error = config.continue_on_error, + ) ) - ) + end end end end @@ -406,6 +431,7 @@ function print_root_matrix(entries) print("{\"group\":\"", entry.group, "\",\"version\":\"", entry.version, "\",\"runner\":") json_value(entry.runner) print( + ",\"arch\":\"", entry.arch, "\"", ",\"timeout\":", entry.timeout, ",\"num_threads\":", entry.num_threads, ",\"continue_on_error\":", entry.continue_on_error ? "true" : "false", "}", ) diff --git a/test/runtests.jl b/test/runtests.jl index aaf5a4b..dad6b96 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -313,6 +313,71 @@ end @test length(gpu) == 1 && only(gpu).runner == ["self-hosted", "Linux", "X64", "gpu"] end +@testset "root matrix: arch axis and group alias (32-bit lane)" begin + d = mktempdir() + mkpath(joinpath(d, "test")) + write( + joinpath(d, "test", "test_groups.toml"), """ + [Core] + versions = ["lts", "1"] + os = ["ubuntu-latest", "windows-latest"] + + ["Core 32-bit"] + group = "Core" + versions = ["1"] + os = ["ubuntu-latest"] + arch = "x86" + + [QA] + versions = ["1"] + """ + ) + m = build_root_matrix(d) + # Native Core cells carry an empty arch (tests.yml falls back to runner.arch). + native = filter(e -> e.group == "Core" && e.arch == "", m) + @test length(native) == 4 # 2 versions × 2 OSes + @test Set((e.version, e.runner) for e in native) == + Set((v, o) for v in ["lts", "1"] for o in ["ubuntu-latest", "windows-latest"]) + # The aliased "Core 32-bit" section dispatches GROUP=Core but adds exactly one + # x86 cell on ubuntu, so runtests.jl's folder resolves to the Core body. + x86 = filter(e -> e.arch == "x86", m) + @test length(x86) == 1 + @test only(x86).group == "Core" && only(x86).version == "1" && only(x86).runner == "ubuntu-latest" + # QA stays native. + @test all(e -> e.arch == "", filter(e -> e.group == "QA", m)) +end + +@testset "root matrix: arch as a list fans out per arch" begin + d = mktempdir() + mkpath(joinpath(d, "test")) + write( + joinpath(d, "test", "test_groups.toml"), """ + [Core] + versions = ["1"] + arch = ["x64", "x86"] + """ + ) + m = build_root_matrix(d) + @test Set(e.arch for e in m) == Set(["x64", "x86"]) + @test length(m) == 2 +end + +@testset "root matrix: --root-matrix CLI emits arch field" begin + d = mktempdir() + mkpath(joinpath(d, "test")) + write( + joinpath(d, "test", "test_groups.toml"), """ + ["Core 32-bit"] + group = "Core" + versions = ["1"] + arch = "x86" + """ + ) + out = read(pipeline(IOBuffer(""), `$(Base.julia_cmd()) $SCRIPT $d --root-matrix`), String) + @test occursin("\"arch\":\"x86\"", out) + @test occursin("\"group\":\"Core\"", out) +end + @testset "root matrix faithfully reproduces OrdinaryDiffEq's embedded matrix" begin # ODE's root CI.yml is 17 groups × [lts,1,pre] minus excludes (AD->lts only, # ODEInterfaceRegression->lts only). QA is centrally clamped to v1 only (see