diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9428c348..31af714a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: test: needs: detect-changes if: needs.detect-changes.outputs.code-changed == 'true' - name: Test + name: Test (${{ matrix.shard }}) strategy: fail-fast: false matrix: @@ -77,18 +77,71 @@ jobs: target: x86_64-unknown-linux-gnu cargo_cmd: cargo-zigbuild build_target: x86_64-unknown-linux-gnu.2.17 - - os: windows-latest - target: x86_64-pc-windows-msvc - cargo_cmd: cargo - build_target: x86_64-pc-windows-msvc + shard: linux-gnu + scope: '' + run_env: '' - os: namespace-profile-mac-default target: aarch64-apple-darwin cargo_cmd: cargo build_target: aarch64-apple-darwin + shard: macos-arm64 + scope: '' + run_env: '' - os: namespace-profile-mac-default target: x86_64-apple-darwin cargo_cmd: cargo build_target: x86_64-apple-darwin + shard: macos-x64 + scope: '' + run_env: '' + # Windows e2e fixtures dominate wall-clock (60s per PTY step vs 20s on + # Unix). Coverage is partitioned by crate: the e2e shards run + # `-p vite_task_bin` and the non-e2e shard runs + # `--workspace --exclude vite_task_bin`; the union is the workspace + # by construction. The e2e_snapshots harness self-shards via + # VT_SHARD_INDEX/VT_SHARD_TOTAL across the 5 e2e jobs. + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-e2e-1 + scope: '-p vite_task_bin' + run_env: 'VT_SHARD_INDEX=1 VT_SHARD_TOTAL=5' + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-e2e-2 + scope: '-p vite_task_bin' + run_env: 'VT_SHARD_INDEX=2 VT_SHARD_TOTAL=5' + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-e2e-3 + scope: '-p vite_task_bin' + run_env: 'VT_SHARD_INDEX=3 VT_SHARD_TOTAL=5' + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-e2e-4 + scope: '-p vite_task_bin' + run_env: 'VT_SHARD_INDEX=4 VT_SHARD_TOTAL=5' + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-e2e-5 + scope: '-p vite_task_bin' + run_env: 'VT_SHARD_INDEX=5 VT_SHARD_TOTAL=5' + - os: windows-latest + target: x86_64-pc-windows-msvc + cargo_cmd: cargo + build_target: x86_64-pc-windows-msvc + shard: windows-non-e2e + scope: '--workspace --exclude vite_task_bin' + run_env: '' runs-on: ${{ matrix.os }} steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 @@ -124,13 +177,13 @@ jobs: if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - name: Build tests - run: ${{ matrix.cargo_cmd }} test --no-run --target ${{ matrix.build_target }} + run: ${{ matrix.cargo_cmd }} test --no-run ${{ matrix.scope }} --target ${{ matrix.build_target }} # Default `cargo test` runs only tests that need nothing beyond the # Rust toolchain; this step verifies that contract before Node.js # and pnpm enter the picture. - name: Run tests - run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} + run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} # x86_64-apple-darwin runs on arm64 runner under Rosetta; install x64 Node # so fspy's x86_64 preload dylib can be injected into spawned node procs. @@ -139,7 +192,7 @@ jobs: architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }} - name: Run ignored tests - run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored + run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} -- --ignored test-musl: needs: detect-changes diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index 45adbada..2de54930 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -547,6 +547,28 @@ fn run_case( Ok(()) } +/// Parses `VT_SHARD_INDEX` (1..=total) and `VT_SHARD_TOTAL` env vars for CI +/// sharding. Both unset means "run all trials". Both set selects one shard via +/// round-robin. Exactly one set is a CI misconfiguration and panics. +fn parse_shard_env() -> Option<(usize, usize)> { + let index = std::env::var("VT_SHARD_INDEX").ok(); + let total = std::env::var("VT_SHARD_TOTAL").ok(); + match (index, total) { + (None, None) => None, + (Some(i), Some(t)) => { + let index: usize = i.parse().expect("VT_SHARD_INDEX must be a positive integer"); + let total: usize = t.parse().expect("VT_SHARD_TOTAL must be a positive integer"); + assert!(total > 0, "VT_SHARD_TOTAL must be > 0"); + assert!( + (1..=total).contains(&index), + "VT_SHARD_INDEX must be in 1..={total}, got {index}" + ); + Some((index, total)) + } + _ => panic!("VT_SHARD_INDEX and VT_SHARD_TOTAL must both be set or both unset"), + } +} + #[expect(clippy::disallowed_types, reason = "Path required for CARGO_MANIFEST_DIR path traversal")] fn main() { let tmp_dir = tempfile::tempdir().unwrap(); @@ -576,6 +598,8 @@ fn main() { args.test_threads = Some(1); } + let shard = parse_shard_env(); + let tests: Vec = fixture_paths .into_iter() .flat_map(|fixture_path| { @@ -626,5 +650,18 @@ fn main() { }) .collect(); + let tests = match shard { + Some((index, total)) => { + let count = tests.len(); + tests + .into_iter() + .enumerate() + .filter(|(i, _)| i * total / count + 1 == index) + .map(|(_, t)| t) + .collect() + } + None => tests, + }; + libtest_mimic::run(&args, tests).exit(); }