TestShards.jl

Your test suite runs in one CI job and takes twenty minutes. TestShards runs it in eight jobs and takes three, balanced by how long each file actually took last time.

You wrap runtests.jl in one macro and call one workflow. There is no list of test files to maintain, no naming convention to follow, and Pkg.test() on your machine keeps doing exactly what it did before.

using MyPackage, TestShards

TestShards.@shard begin
    include("core/a.jl")
    for f in readdir(joinpath(@__DIR__, "solver"); join = true)
        include(f)
    end
end
jobs:
  test:
    permissions:
      contents: write
    uses: QAtlasHub/TestShards.jl/.github/workflows/sharded-tests.yml@main
    with:
      shards: 8
    secrets:
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Each include is one shardable piece — including the ones the for loop produces. That is the whole adoption: no shard-planning step, no coverage-merge job, no job that records timings.

Start here

  • Getting started — install, wire up CI, every workflow input, and how to reproduce one shard on your machine.

Then, as you need them:

  • Units — what gets split, and keeping order-dependent files together.
  • Balancing — the timing history, how many shards your suite can actually use.
  • Records — what a run reports, and attaching evidence to a testset.
  • Guarantees — what cannot silently go wrong, and the one rule your suite must follow.
  • Composing — letting another tool own the testset a unit runs in.
  • API — every exported and public function.

What a run gives you

One coverage uploadthe shards' counters are merged once, not uploaded N times
One ordered recordevery unit, every testset, in suite order — not eight interleaved logs
A completeness checkthe run fails if a unit ran nowhere, or ran twice
A diagnosiswhat is limiting your suite, and what follows from it

How the splitting differs

A unit is whatever runtests.jl includes. @shard shadows include inside its block, so the interception happens at the call. A file produced by for f in readdir(...) shards exactly like a literal include("a.jl") — nothing to register, nothing to keep in sync.

Balance comes from measurement. Each green run on the default branch records how long every unit took, and the next run bin-packs from those numbers rather than from a file count.

Every run reconciles what ran against what was observed, because splitting a suite introduces a failure a green badge cannot show you: a unit that ran in no shard.

what defines a piecehow it balancesadding a test file
A hand-written job matrixa list in the workflowyou do, by handedit the list — or it silently never runs
A path or naming conventiona directory or filename rulefile countmust be named to match
ReTestItems.jl, ParallelTestRunner.jla @testitem / a fileworker processes inside one jobnothing
TestShardswhatever runtests.jl includesmeasured runtimenothing

Scope

This splits a suite across CI jobs. It does not run tests in parallel within a job — that is ParallelTestRunner.jl and ReTestItems.jl's business, and the two compose. What limits each is different: worker processes are bounded by one runner's cores, jobs by what your account will schedule at once.