Skip to content

Commit af66fd9

Browse files
wan9chiclaude
andauthored
ci: run Windows tests from a Linux-built archive (#443)
## Summary Build Windows test binaries once on a Linux runner, upload them as a nextest archive, then run that archive on Windows. Why this is faster: - Linux is a faster place to compile the Windows test binaries, especially with the cached xwin MSVC CRT / Windows SDK restored. - Ignored tests are isolated into one Windows job, so Node setup happens once instead of on every default-test shard. - Default Windows tests run from the archive across six run-only partitions. - The old `VT_SHARD_INDEX` / `VT_SHARD_TOTAL` harness sharding is removed in favor of nextest partitioning. ```mermaid flowchart LR subgraph Before["Before: every Windows shard did the expensive work"] B1["Windows shard x6"] --> B2["Compile tests on Windows"] B2 --> B3["Run default tests"] B3 --> B4["Setup Node"] B4 --> B5["Run ignored tests"] end subgraph After["After: build once, run the archive"] A1["Linux runner"] --> A2["Build Windows test archive"] A2 --> A3["Upload nextest archive"] A3 --> A4["Windows default shards x6<br/>No Node setup"] A3 --> A5["Windows ignored job<br/>Node setup once"] end ``` ## Timing I triggered CI on `main` first, then triggered this PR immediately after. | Run | Wall time | | --- | ---: | | [`main`](https://github.com/voidzero-dev/vite-task/actions/runs/27454819697) | 4m05s | | [this PR](https://github.com/voidzero-dev/vite-task/actions/runs/27454820773) | 2m36s | Net: **1m29s faster**, about **36% less wall time**. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5f05678 commit af66fd9

3 files changed

Lines changed: 150 additions & 104 deletions

File tree

.github/workflows/ci.yml

Lines changed: 146 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -78,86 +78,23 @@ jobs:
7878
cargo_cmd: cargo-zigbuild
7979
build_target: x86_64-unknown-linux-gnu.2.17
8080
shard: linux-gnu
81-
scope: ''
82-
run_env: ''
8381
- os: namespace-profile-mac-default
8482
target: aarch64-apple-darwin
8583
cargo_cmd: cargo
8684
build_target: aarch64-apple-darwin
8785
shard: macos-arm64
88-
scope: ''
89-
run_env: ''
9086
- os: namespace-profile-mac-default
9187
target: x86_64-apple-darwin
9288
cargo_cmd: cargo
9389
build_target: x86_64-apple-darwin
9490
shard: macos-x64
95-
scope: ''
96-
run_env: ''
97-
# Windows e2e fixtures dominate wall-clock (60s per PTY step vs 20s on
98-
# Unix). Coverage is partitioned by crate: the e2e shards run
99-
# `-p vite_task_bin` and the non-e2e shard runs
100-
# `--workspace --exclude vite_task_bin`; the union is the workspace
101-
# by construction. The e2e_snapshots harness self-shards via
102-
# VT_SHARD_INDEX/VT_SHARD_TOTAL across the 5 e2e jobs.
103-
- os: windows-latest
104-
target: x86_64-pc-windows-msvc
105-
cargo_cmd: cargo
106-
build_target: x86_64-pc-windows-msvc
107-
shard: windows-e2e-1
108-
scope: '-p vite_task_bin'
109-
run_env: 'VT_SHARD_INDEX=1 VT_SHARD_TOTAL=5'
110-
- os: windows-latest
111-
target: x86_64-pc-windows-msvc
112-
cargo_cmd: cargo
113-
build_target: x86_64-pc-windows-msvc
114-
shard: windows-e2e-2
115-
scope: '-p vite_task_bin'
116-
run_env: 'VT_SHARD_INDEX=2 VT_SHARD_TOTAL=5'
117-
- os: windows-latest
118-
target: x86_64-pc-windows-msvc
119-
cargo_cmd: cargo
120-
build_target: x86_64-pc-windows-msvc
121-
shard: windows-e2e-3
122-
scope: '-p vite_task_bin'
123-
run_env: 'VT_SHARD_INDEX=3 VT_SHARD_TOTAL=5'
124-
- os: windows-latest
125-
target: x86_64-pc-windows-msvc
126-
cargo_cmd: cargo
127-
build_target: x86_64-pc-windows-msvc
128-
shard: windows-e2e-4
129-
scope: '-p vite_task_bin'
130-
run_env: 'VT_SHARD_INDEX=4 VT_SHARD_TOTAL=5'
131-
- os: windows-latest
132-
target: x86_64-pc-windows-msvc
133-
cargo_cmd: cargo
134-
build_target: x86_64-pc-windows-msvc
135-
shard: windows-e2e-5
136-
scope: '-p vite_task_bin'
137-
run_env: 'VT_SHARD_INDEX=5 VT_SHARD_TOTAL=5'
138-
- os: windows-latest
139-
target: x86_64-pc-windows-msvc
140-
cargo_cmd: cargo
141-
build_target: x86_64-pc-windows-msvc
142-
shard: windows-non-e2e
143-
scope: '--workspace --exclude vite_task_bin'
144-
run_env: ''
14591
runs-on: ${{ matrix.os }}
14692
steps:
14793
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
14894

14995
- name: Update submodules
15096
run: git submodule update --init --recursive
15197

152-
- name: Setup Dev Drive
153-
uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0
154-
if: runner.os == 'Windows'
155-
with:
156-
drive-size: 10GB
157-
env-mapping: |
158-
CARGO_HOME,{{ DEV_DRIVE }}/.cargo
159-
RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup
160-
16198
- uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17
16299
with:
163100
save-cache: ${{ github.ref_name == 'main' }}
@@ -177,13 +114,13 @@ jobs:
177114
if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }}
178115

179116
- name: Build tests
180-
run: ${{ matrix.cargo_cmd }} test --no-run ${{ matrix.scope }} --target ${{ matrix.build_target }}
117+
run: ${{ matrix.cargo_cmd }} test --no-run --target ${{ matrix.build_target }}
181118

182119
# Default `cargo test` runs only tests that need nothing beyond the
183120
# Rust toolchain; this step verifies that contract before Node.js
184121
# and pnpm enter the picture.
185122
- name: Run tests
186-
run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }}
123+
run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }}
187124

188125
# x86_64-apple-darwin runs on arm64 runner under Rosetta; install x64 Node
189126
# so fspy's x86_64 preload dylib can be injected into spawned node procs.
@@ -192,7 +129,148 @@ jobs:
192129
architecture: ${{ matrix.target == 'x86_64-apple-darwin' && 'x64' || '' }}
193130

194131
- name: Run ignored tests
195-
run: ${{ matrix.run_env }} ${{ matrix.cargo_cmd }} test ${{ matrix.scope }} --target ${{ matrix.build_target }} -- --ignored
132+
run: ${{ matrix.cargo_cmd }} test --target ${{ matrix.build_target }} -- --ignored
133+
134+
# Windows tests are cross-compiled on a fast Linux runner with cargo-xwin
135+
# (clang-cl + lld-link against the xwin-downloaded MSVC CRT/Windows SDK)
136+
# and packed into a portable nextest archive. The Windows runners then only
137+
# download the archive and run it: no Rust toolchain or compilation on the
138+
# slow runners. Six nextest partitions avoid most repeated setup while keeping
139+
# enough parallelism for the Windows-heavy default test workload. The much
140+
# smaller Node-backed ignored suite runs once in its own job, avoiding
141+
# duplicate Windows Node setup.
142+
build-windows-tests:
143+
needs: detect-changes
144+
if: needs.detect-changes.outputs.code-changed == 'true'
145+
name: Build Windows tests
146+
runs-on: namespace-profile-linux-x64-default
147+
env:
148+
XWIN_ACCEPT_LICENSE: '1'
149+
# The MSVC STL from xwin's VS17 manifest static-asserts a minimum Clang
150+
# version newer than what runner images ship. Microsoft's documented
151+
# escape hatch lets Detours (the only C++ in the workspace, and far older
152+
# than any STL/clang concern here) compile with the system clang-cl.
153+
# cargo-xwin folds a pre-set CXXFLAGS into the env it generates.
154+
CXXFLAGS: -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
155+
steps:
156+
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
157+
158+
- name: Update submodules
159+
run: git submodule update --init --recursive
160+
161+
# llvm-tools provides llvm-ar in the toolchain's rustlib bin dir;
162+
# cargo-xwin symlinks the MSVC-style llvm-lib/llvm-dlltool (used by cc-rs
163+
# to archive Detours) and lld-link from there when the runner image has
164+
# no system LLVM.
165+
- uses: oxc-project/setup-rust@3d6fb132fbe7cdcb66bf8ec193911c2945369d12 # v1.0.17
166+
with:
167+
save-cache: ${{ github.ref_name == 'main' }}
168+
cache-key: windows-cross
169+
components: llvm-tools
170+
171+
- run: rustup target add x86_64-pc-windows-msvc
172+
173+
- uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
174+
with:
175+
tool: cargo-nextest,cargo-xwin
176+
177+
# Downloading and unpacking the MSVC CRT/Windows SDK dominates this
178+
# job's wall time on a cold cache; the unpacked result is immutable for a
179+
# given manifest version, so cache it. Bump the key when bumping the
180+
# xwin version.
181+
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
182+
id: xwin-cache
183+
with:
184+
path: ~/.cache/cargo-xwin
185+
key: cargo-xwin-msvc-17
186+
187+
- name: Build test archive
188+
run: |
189+
# cargo-xwin resolves the rustflags configured in .cargo/config.toml,
190+
# appends its -Lnative Windows SDK paths, and re-exports the result as
191+
# CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS, so the plain cargo
192+
# that nextest invokes cross-compiles exactly like `cargo xwin` would.
193+
eval "$(cargo xwin env --target x86_64-pc-windows-msvc | grep '^export ')"
194+
# `cargo xwin env` renders its intended *removal* of RUSTFLAGS as
195+
# `export RUSTFLAGS="";` — actually remove it so it cannot shadow the
196+
# target-specific rustflags set above.
197+
unset RUSTFLAGS
198+
cargo nextest archive --workspace --target x86_64-pc-windows-msvc --archive-file windows-tests.tar.zst
199+
200+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
201+
with:
202+
name: windows-test-archive
203+
path: windows-tests.tar.zst
204+
retention-days: 7
205+
206+
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
207+
if: steps.xwin-cache.outputs.cache-hit != 'true'
208+
with:
209+
path: ~/.cache/cargo-xwin
210+
key: cargo-xwin-msvc-17
211+
212+
test-windows:
213+
needs: build-windows-tests
214+
name: Test (${{ matrix.shard }})
215+
strategy:
216+
fail-fast: false
217+
matrix:
218+
include:
219+
- shard: windows-1
220+
partition: count:1/6
221+
mode: default
222+
- shard: windows-2
223+
partition: count:2/6
224+
mode: default
225+
- shard: windows-3
226+
partition: count:3/6
227+
mode: default
228+
- shard: windows-4
229+
partition: count:4/6
230+
mode: default
231+
- shard: windows-5
232+
partition: count:5/6
233+
mode: default
234+
- shard: windows-6
235+
partition: count:6/6
236+
mode: default
237+
- shard: windows-ignored
238+
partition: ''
239+
mode: ignored
240+
runs-on: windows-latest
241+
env:
242+
PARTITION: ${{ matrix.partition }}
243+
steps:
244+
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
245+
246+
# The Detours submodule is needed at run time: fspy_detours_sys's
247+
# bindings test re-generates bindgen bindings against the checked-out
248+
# headers.
249+
- name: Update submodules
250+
run: git submodule update --init --recursive
251+
252+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
253+
with:
254+
name: windows-test-archive
255+
256+
- uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
257+
with:
258+
tool: cargo-nextest
259+
260+
# The default (non-ignored) test set needs nothing beyond the test
261+
# binaries themselves; this step verifies that contract before Node.js
262+
# and pnpm enter the picture. `cargo-nextest` is invoked directly so the
263+
# job never depends on the runner's Rust toolchain.
264+
- name: Run tests
265+
if: matrix.mode == 'default'
266+
run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --partition "$PARTITION"
267+
268+
- uses: oxc-project/setup-node@ab97f03642370d79a7e96dd286bd02a1be40e0ba # v1.3.0
269+
if: matrix.mode == 'ignored'
270+
271+
- name: Run ignored tests
272+
if: matrix.mode == 'ignored'
273+
run: cargo-nextest nextest run --archive-file windows-tests.tar.zst --workspace-remap . --run-ignored ignored-only
196274

197275
test-musl:
198276
needs: detect-changes
@@ -287,6 +365,8 @@ jobs:
287365
- clippy
288366
- test
289367
- test-musl
368+
- build-windows-tests
369+
- test-windows
290370
- fmt
291371
steps:
292372
- run: exit 1

crates/fspy/tests/oxlint.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use test_log::test;
77

88
/// Get the packages/tools/.bin directory path
99
fn tools_bin_dir() -> std::path::PathBuf {
10-
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
10+
// Resolve CARGO_MANIFEST_DIR at run time, not via `env!`: a compile-time
11+
// path can point at a different checkout than the one executing the test.
12+
let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
13+
manifest_dir
1114
.parent()
1215
.unwrap()
1316
.parent()

crates/vite_task_bin/tests/e2e_snapshots/main.rs

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -581,28 +581,6 @@ fn run_case(
581581
Ok(())
582582
}
583583

584-
/// Parses `VT_SHARD_INDEX` (1..=total) and `VT_SHARD_TOTAL` env vars for CI
585-
/// sharding. Both unset means "run all trials". Both set selects one shard via
586-
/// round-robin. Exactly one set is a CI misconfiguration and panics.
587-
fn parse_shard_env() -> Option<(usize, usize)> {
588-
let index = std::env::var("VT_SHARD_INDEX").ok();
589-
let total = std::env::var("VT_SHARD_TOTAL").ok();
590-
match (index, total) {
591-
(None, None) => None,
592-
(Some(i), Some(t)) => {
593-
let index: usize = i.parse().expect("VT_SHARD_INDEX must be a positive integer");
594-
let total: usize = t.parse().expect("VT_SHARD_TOTAL must be a positive integer");
595-
assert!(total > 0, "VT_SHARD_TOTAL must be > 0");
596-
assert!(
597-
(1..=total).contains(&index),
598-
"VT_SHARD_INDEX must be in 1..={total}, got {index}"
599-
);
600-
Some((index, total))
601-
}
602-
_ => panic!("VT_SHARD_INDEX and VT_SHARD_TOTAL must both be set or both unset"),
603-
}
604-
}
605-
606584
#[expect(clippy::disallowed_types, reason = "Path required for CARGO_MANIFEST_DIR path traversal")]
607585
fn main() {
608586
let tmp_dir = tempfile::tempdir().unwrap();
@@ -635,8 +613,6 @@ fn main() {
635613
args.test_threads = Some(1);
636614
}
637615

638-
let shard = parse_shard_env();
639-
640616
let tests: Vec<libtest_mimic::Trial> = fixture_paths
641617
.into_iter()
642618
.flat_map(|fixture_path| {
@@ -687,18 +663,5 @@ fn main() {
687663
})
688664
.collect();
689665

690-
let tests = match shard {
691-
Some((index, total)) => {
692-
let count = tests.len();
693-
tests
694-
.into_iter()
695-
.enumerate()
696-
.filter(|(i, _)| i * total / count + 1 == index)
697-
.map(|(_, t)| t)
698-
.collect()
699-
}
700-
None => tests,
701-
};
702-
703666
libtest_mimic::run(&args, tests).exit();
704667
}

0 commit comments

Comments
 (0)