diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd6cc8624..af4a597c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,70 +78,16 @@ jobs: cargo_cmd: cargo-zigbuild build_target: x86_64-unknown-linux-gnu.2.17 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 @@ -149,15 +95,6 @@ jobs: - name: Update submodules run: git submodule update --init --recursive - - name: Setup Dev Drive - uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0 - if: runner.os == 'Windows' - with: - drive-size: 10GB - env-mapping: | - CARGO_HOME,{{ DEV_DRIVE }}/.cargo - RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup - - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 with: save-cache: ${{ github.ref_name == 'main' }} @@ -177,13 +114,13 @@ jobs: if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} - name: Build tests - run: ${{ matrix.cargo_cmd }} test --no-run ${{ matrix.scope }} --target ${{ matrix.build_target }} + run: ${{ matrix.cargo_cmd }} test --no-run --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.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} + run: ${{ matrix.cargo_cmd }} test --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. @@ -192,7 +129,126 @@ jobs: architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }} - name: Run ignored tests - run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} -- --ignored + run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored + + # Windows tests are cross-compiled on a fast Linux runner with cargo-xwin + # (clang-cl + lld-link against the xwin-downloaded MSVC CRT/Windows SDK) + # and packed into a portable nextest archive. The test-windows shards then + # only download the archive and run it — no Rust toolchain or compilation + # on the slow Windows runners. The run is split across shards via nextest's + # count partitioning; with compilation gone, per-shard fixed overhead + # (checkout, Node setup) rivals test execution time, so four shards is the + # measured sweet spot of wall-clock vs runner minutes. + build-windows-tests: + needs: detect-changes + if: needs.detect-changes.outputs.code-changed == 'true' + name: Build Windows tests + runs-on: namespace-profile-linux-x64-default + env: + XWIN_ACCEPT_LICENSE: '1' + # The MSVC STL from xwin's VS17 manifest static-asserts a minimum Clang + # version newer than what runner images ship. Microsoft's documented + # escape hatch lets Detours (the only C++ in the workspace, and far older + # than any STL/clang concern here) compile with the system clang-cl. + # cargo-xwin folds a pre-set CXXFLAGS into the env it generates. + CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - name: Update submodules + run: git submodule update --init --recursive + + # llvm-tools provides llvm-ar in the toolchain's rustlib bin dir; + # cargo-xwin symlinks the MSVC-style llvm-lib/llvm-dlltool (used by cc-rs + # to archive Detours) and lld-link from there when the runner image has + # no system LLVM. + - uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17 + with: + save-cache: ${{ github.ref_name == 'main' }} + cache-key: windows-cross + components: llvm-tools + + - run: rustup target add x86_64-pc-windows-msvc + + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10 + with: + tool: cargo-nextest,cargo-xwin + + # Downloading and unpacking the MSVC CRT/Windows SDK dominates this + # job's wall time (~10 minutes); the unpacked result is immutable for a + # given manifest version, so cache it. Bump the key when bumping the + # xwin version. + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: xwin-cache + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-msvc-17 + + - name: Build test archive + run: | + # cargo-xwin resolves the rustflags configured in .cargo/config.toml, + # appends its -Lnative Windows SDK paths, and re-exports the result as + # CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS, so the plain cargo + # that nextest invokes cross-compiles exactly like `cargo xwin` would. + eval "$(cargo xwin env --target x86_64-pc-windows-msvc | grep '^export ')" + # `cargo xwin env` renders its intended *removal* of RUSTFLAGS as + # `export RUSTFLAGS="";` — actually remove it so it cannot shadow the + # target-specific rustflags set above. + unset RUSTFLAGS + cargo nextest archive --workspace --target x86_64-pc-windows-msvc --archive-file windows-tests.tar.zst + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-test-archive + path: windows-tests.tar.zst + retention-days: 7 + + - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + if: steps.xwin-cache.outputs.cache-hit != 'true' + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-msvc-17 + + test-windows: + needs: build-windows-tests + name: Test (windows-${{ matrix.partition }}) + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3, 4] + runs-on: windows-latest + env: + PARTITION: count:${{ matrix.partition }}/4 + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + # The Detours submodule is needed at run time: fspy_detours_sys's + # bindings test re-generates bindgen bindings against the checked-out + # headers. + - name: Update submodules + run: git submodule update --init --recursive + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-test-archive + + - uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10 + with: + tool: cargo-nextest + + # The default (non-ignored) test set needs nothing beyond the test + # binaries themselves; this step verifies that contract before Node.js + # and pnpm enter the picture. `cargo-nextest` is invoked directly so the + # job never depends on the runner's Rust toolchain. + - name: Run tests + run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --partition "$PARTITION" + + - uses: oxc-project/setup-node@ab97f03642370d79a7e96dd286bd02a1be40e0ba # v1.3.0 + + # --no-tests=pass: a shard's partition of the (much smaller) ignored + # test set may legitimately come up empty. + - name: Run ignored tests + run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --partition "$PARTITION" --run-ignored ignored-only --no-tests pass test-musl: needs: detect-changes @@ -287,6 +343,8 @@ jobs: - clippy - test - test-musl + - build-windows-tests + - test-windows - fmt steps: - run: exit 1 diff --git a/crates/fspy/tests/oxlint.rs b/crates/fspy/tests/oxlint.rs index f2f9c7025..cda78b2dd 100644 --- a/crates/fspy/tests/oxlint.rs +++ b/crates/fspy/tests/oxlint.rs @@ -7,7 +7,12 @@ use test_log::test; /// Get the packages/tools/.bin directory path fn tools_bin_dir() -> std::path::PathBuf { - std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + // Resolve CARGO_MANIFEST_DIR at run time, not via `env!`: a compile-time + // path bakes in the build machine's checkout, which breaks when the test + // binary is cross-compiled and run on another machine (e.g. built on + // Linux with cargo-xwin, run on Windows from a nextest archive). + let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + manifest_dir .parent() .unwrap() .parent() diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index 2de549308..45adbada2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -547,28 +547,6 @@ 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(); @@ -598,8 +576,6 @@ fn main() { args.test_threads = Some(1); } - let shard = parse_shard_env(); - let tests: Vec = fixture_paths .into_iter() .flat_map(|fixture_path| { @@ -650,18 +626,5 @@ 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(); }