Skip to content

Commit 8b99b0f

Browse files
committed
ci: shard CLI snapshot tests across parallel jobs
1 parent 22d5642 commit 8b99b0f

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)