Skip to content

Commit 59bb353

Browse files
authored
ci: shard CLI snapshot tests across parallel jobs (#2657)
The CLI snapshot suite runs in one job per platform. Split each platform into three parallel jobs to reduce test execution time. Linux and macOS use `VP_SNAP_SHARD=INDEX/TOTAL` to distribute the ordered trials across jobs. Local runs still execute the full suite by default. Windows uses the existing `nextest` runner with `--partition hash:INDEX/TOTAL` and shares the prebuilt archive across jobs. Measured results compare the [original run on September 8](https://github.com/voidzero-dev/vite-plus/actions/runs/34229849906) with the [sharded run on September 10](https://github.com/voidzero-dev/vite-plus/actions/runs/34494568955): | Platform | Snapshot step before | Longest shard step after | Step time reduction | Snapshot job span before → after | | --- | --- | --- | --- | --- | | Linux x64 | 7m39s | 3m12s | 58.2% | 17m59s → 5m59s | | macOS ARM64 | 5m25s | 2m21s | 56.6% | 13m04s → 5m27s | | Windows x64 | 6m48s | 2m48s | 58.8% | 9m21s → 6m06s | Snapshot times measure the `Run PTY snapshot tests` step. Job span measures the first job start to the last job completion per platform, including setup and staggered starts. All nine shards pass. These are single runs from different commits, with different build-cache states. The total job gains therefore do not measure sharding alone. Each shard repeats setup and uses a separate runner.
1 parent 341cce0 commit 59bb353

5 files changed

Lines changed: 66 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -924,15 +924,17 @@ jobs:
924924
# flavors on Linux and macOS: the local flavor uses the packages/cli build
925925
# from this job, the global flavor reuses the installed release binary via
926926
# VP_SNAP_GLOBAL_VP (no vp_global_cli compile; build-upstream builds or
927-
# cache-restores it). One leg per OS: the suite is not sharded. Windows
928-
# runs in cli-snapshot-test-windows via the cross-compiled archive.
927+
# cache-restores it). Three shards per OS split the trials across runners.
928+
# Windows runs in cli-snapshot-test-windows via the cross-compiled archive.
929929
cli-snapshot-test:
930-
name: CLI snapshot test (${{ matrix.target }})
930+
name: CLI snapshot test (${{ matrix.target }}, shard ${{ matrix.shard }}/3)
931931
needs:
932932
- download-previous-rolldown-binaries
933933
strategy:
934934
fail-fast: false
935935
matrix:
936+
os: [namespace-profile-linux-x64-default, namespace-profile-mac-default]
937+
shard: [1, 2, 3]
936938
include:
937939
- os: namespace-profile-linux-x64-default
938940
target: x86_64-unknown-linux-gnu
@@ -1020,18 +1022,23 @@ jobs:
10201022
cargo test -p vp_cli_snapshots
10211023
env:
10221024
RUST_BACKTRACE: '1'
1025+
VP_SNAP_SHARD: ${{ matrix.shard }}/3
10231026

10241027
# Runs the PTY snapshot suite (crates/vp_cli_snapshots) on Windows with
10251028
# BOTH vp flavors, without a Rust toolchain on the runner: the test binary
10261029
# and vpt arrive cross-compiled in the nextest archive from
10271030
# build-windows-tests, the global vp comes prebuilt from build-windows-cli,
10281031
# and the JS CLI is built here (skip-native) for the local flavor.
10291032
cli-snapshot-test-windows:
1030-
name: CLI snapshot test (Windows)
1033+
name: CLI snapshot test (Windows, shard ${{ matrix.shard }}/3)
10311034
needs:
10321035
- download-previous-rolldown-binaries
10331036
- build-windows-cli
10341037
- build-windows-tests
1038+
strategy:
1039+
fail-fast: false
1040+
matrix:
1041+
shard: [1, 2, 3]
10351042
# Runs on the Namespace Windows runner. This is a PTY snapshot suite that
10361043
# opens a pseudo-console (ConPTY) and spawns vp into it. It previously stayed
10371044
# on GitHub-hosted windows-latest because Namespace's Windows runners ran
@@ -1129,7 +1136,7 @@ jobs:
11291136
export VP_SNAP_PWSH_BIN="$(cygpath -w "$(command -v pwsh.exe)")"
11301137
# --no-fail-fast: on a snapshot suite every diff is diagnostic
11311138
# signal; cancelling on the first failure hides the rest.
1132-
cargo-nextest nextest run --archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast
1139+
cargo-nextest nextest run --archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast --partition hash:${{ matrix.shard }}/3
11331140
env:
11341141
RUST_BACKTRACE: '1'
11351142
# Keep Windows env parity with the `test` recipe in justfile.

crates/vp_cli_snapshots/tests/cli_snapshots/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ cases). Prerequisites: both flavors need `cargo build -p vp_global_cli`
5656
older than `src`, so a forgotten rebuild never silently tests stale
5757
local-CLI code.
5858

59+
CI runs three shards per platform. Linux and macOS use `VP_SNAP_SHARD=1/3`
60+
(then `2/3` and `3/3`) to distribute the ordered trials round-robin. Sharding
61+
happens before name filtering, so filtered runs keep the same assignment.
62+
Leave the variable unset to run the whole suite. Windows uses the existing
63+
nextest runner with `--partition hash:1/3` (then `2/3` and `3/3`).
64+
5965
Environment overrides, mainly for CI:
6066

6167
| Variable | Effect |

crates/vp_cli_snapshots/tests/cli_snapshots/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
mod exit_code;
1919
mod flavor;
2020
mod redact;
21+
mod shard;
2122

2223
use std::{
2324
collections::{BTreeMap, hash_map::DefaultHasher},
@@ -1814,6 +1815,11 @@ fn main() {
18141815
}
18151816
}
18161817

1818+
if let Some(shard) = std::env::var_os("VP_SNAP_SHARD") {
1819+
let shard = shard.to_str().expect("VP_SNAP_SHARD must be valid UTF-8");
1820+
tests = shard::select(tests, shard).unwrap_or_else(|error| panic!("{error}"));
1821+
}
1822+
18171823
let conclusion = libtest_mimic::run(&args, tests);
18181824

18191825
// Report each case's wall time (slowest first). Skipped for `--list`,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// Split the deterministically ordered trials round-robin across CI jobs.
2+
/// Apply this before libtest filters so filtered runs keep the same assignment.
3+
pub fn select<T>(tests: Vec<T>, shard: &str) -> Result<Vec<T>, &'static str> {
4+
let (index, total) = shard
5+
.split_once('/')
6+
.and_then(|(index, total)| {
7+
Some((index.parse::<usize>().ok()?, total.parse::<usize>().ok()?))
8+
})
9+
.filter(|&(index, total)| index > 0 && index <= total)
10+
.ok_or("VP_SNAP_SHARD must be INDEX/TOTAL with 1 <= INDEX <= TOTAL")?;
11+
12+
Ok(tests.into_iter().skip(index - 1).step_by(total).collect())
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#![expect(clippy::disallowed_macros, reason = "standalone test uses std macros")]
2+
3+
#[path = "cli_snapshots/shard.rs"]
4+
mod shard;
5+
6+
#[test]
7+
fn shards_cover_every_trial_once_with_balanced_counts() {
8+
for test_count in [0, 1, 2, 8, 100] {
9+
for total in [1, 2, 3, 5] {
10+
let tests: Vec<_> = (0..test_count).collect();
11+
let shards: Vec<_> = (1..=total)
12+
.map(|index| shard::select(tests.clone(), &format!("{index}/{total}")).unwrap())
13+
.collect();
14+
let counts: Vec<_> = shards.iter().map(Vec::len).collect();
15+
assert!(counts.iter().max().unwrap() - counts.iter().min().unwrap() <= 1);
16+
17+
let mut combined: Vec<_> = shards.into_iter().flatten().collect();
18+
combined.sort_unstable();
19+
assert_eq!(combined, tests);
20+
}
21+
}
22+
}
23+
24+
#[test]
25+
fn invalid_shards_fail_instead_of_silently_skipping_tests() {
26+
for value in ["", "1", "0/3", "1/0", "4/3", "-1/3", "a/3", "1/a", "1/2/3"] {
27+
assert!(shard::select(vec!["test"], value).is_err(), "accepted {value:?}");
28+
}
29+
}

0 commit comments

Comments
 (0)