Skip to content

Commit 109fca0

Browse files
authored
chore(ci): add snap test sharding with --shard=X/Y support (#1189)
Add Vitest-style --shard option to the snap test runner to split tests across multiple CI runners. The new cli-snap-test job runs 3 shards per OS (9 total matrix entries), reducing snap test wall-clock time by ~65%. The cli-e2e-test job now runs only unit tests, while snap tests are handled exclusively by the sharded cli-snap-test job.
1 parent 0860129 commit 109fca0

File tree

2 files changed

+166
-27
lines changed

2 files changed

+166
-27
lines changed

.github/workflows/ci.yml

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ jobs:
253253
- name: Run vp check
254254
run: vp check
255255

256+
- name: Run unit tests
257+
run: RUST_BACKTRACE=1 pnpm test:unit
258+
env:
259+
RUST_MIN_STACK: 8388608
260+
256261
- name: Test global package install (powershell)
257262
if: ${{ matrix.os == 'windows-latest' }}
258263
shell: pwsh
@@ -384,21 +389,6 @@ jobs:
384389
vp env use --unset
385390
node --version
386391
387-
- name: Install Playwright browsers
388-
run: pnpx playwright install chromium
389-
390-
- name: Run CLI snapshot tests
391-
run: |
392-
RUST_BACKTRACE=1 pnpm test
393-
if ! git diff --quiet; then
394-
echo "::error::Snapshot diff detected. Run 'pnpm -F vite-plus snap-test' locally and commit the updated snap.txt files."
395-
git diff --stat
396-
git diff
397-
exit 1
398-
fi
399-
env:
400-
RUST_MIN_STACK: 8388608
401-
402392
# Upgrade tests (merged from separate job to avoid duplicate build)
403393
- name: Test upgrade (bash)
404394
shell: bash
@@ -572,6 +562,109 @@ jobs:
572562
pnpm bootstrap-cli:ci
573563
vp --version
574564
565+
cli-snap-test:
566+
name: CLI snap test (${{ matrix.target }}, ${{ matrix.shard }}/${{ matrix.shardTotal }})
567+
needs:
568+
- download-previous-rolldown-binaries
569+
strategy:
570+
fail-fast: false
571+
matrix:
572+
include:
573+
- os: namespace-profile-linux-x64-default
574+
target: x86_64-unknown-linux-gnu
575+
shard: 1
576+
shardTotal: 3
577+
- os: namespace-profile-linux-x64-default
578+
target: x86_64-unknown-linux-gnu
579+
shard: 2
580+
shardTotal: 3
581+
- os: namespace-profile-linux-x64-default
582+
target: x86_64-unknown-linux-gnu
583+
shard: 3
584+
shardTotal: 3
585+
- os: namespace-profile-mac-default
586+
target: aarch64-apple-darwin
587+
shard: 1
588+
shardTotal: 3
589+
- os: namespace-profile-mac-default
590+
target: aarch64-apple-darwin
591+
shard: 2
592+
shardTotal: 3
593+
- os: namespace-profile-mac-default
594+
target: aarch64-apple-darwin
595+
shard: 3
596+
shardTotal: 3
597+
- os: windows-latest
598+
target: x86_64-pc-windows-msvc
599+
shard: 1
600+
shardTotal: 3
601+
- os: windows-latest
602+
target: x86_64-pc-windows-msvc
603+
shard: 2
604+
shardTotal: 3
605+
- os: windows-latest
606+
target: x86_64-pc-windows-msvc
607+
shard: 3
608+
shardTotal: 3
609+
runs-on: ${{ matrix.os }}
610+
steps:
611+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
612+
- uses: ./.github/actions/clone
613+
614+
- name: Setup Dev Drive
615+
if: runner.os == 'Windows'
616+
uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0
617+
with:
618+
drive-size: 12GB
619+
drive-format: ReFS
620+
env-mapping: |
621+
CARGO_HOME,{{ DEV_DRIVE }}/.cargo
622+
RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup
623+
624+
- uses: oxc-project/setup-rust@d286d43bc1f606abbd98096666ff8be68c8d5f57 # v1.0.0
625+
with:
626+
save-cache: ${{ github.ref_name == 'main' }}
627+
cache-key: cli-snap-test-${{ matrix.target }}
628+
target-dir: ${{ runner.os == 'Windows' && format('{0}/target', env.DEV_DRIVE) || '' }}
629+
630+
- uses: oxc-project/setup-node@fdbf0dfd334c4e6d56ceeb77d91c76339c2a0885 # v1.0.4
631+
632+
- name: Install docs dependencies
633+
run: pnpm -C docs install --frozen-lockfile
634+
635+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
636+
with:
637+
name: rolldown-binaries
638+
path: ./rolldown/packages/rolldown/src
639+
merge-multiple: true
640+
641+
- name: Build with upstream
642+
uses: ./.github/actions/build-upstream
643+
with:
644+
target: ${{ matrix.target }}
645+
646+
- name: Install Global CLI vp
647+
run: |
648+
pnpm bootstrap-cli:ci
649+
if [[ "$RUNNER_OS" == "Windows" ]]; then
650+
echo "$USERPROFILE\.vite-plus\bin" >> $GITHUB_PATH
651+
else
652+
echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH
653+
fi
654+
655+
- name: Run CLI snapshot tests (shard ${{ matrix.shard }}/${{ matrix.shardTotal }})
656+
run: |
657+
RUST_BACKTRACE=1 pnpm -F ./packages/cli snap-test-local --shard=${{ matrix.shard }}/${{ matrix.shardTotal }}
658+
RUST_BACKTRACE=1 pnpm -F ./packages/cli snap-test-global --shard=${{ matrix.shard }}/${{ matrix.shardTotal }}
659+
if ! git diff --quiet; then
660+
echo "::error::Snapshot diff detected. Run 'pnpm -F vite-plus snap-test' locally and commit the updated snap.txt files."
661+
git diff --stat
662+
git diff
663+
exit 1
664+
fi
665+
env:
666+
RUST_MIN_STACK: 8388608
667+
575668
cli-e2e-test-musl:
576669
name: CLI E2E test (Linux x64 musl)
577670
needs:
@@ -731,6 +824,7 @@ jobs:
731824
- lint
732825
- cli-e2e-test
733826
- cli-e2e-test-musl
827+
- cli-snap-test
734828
steps:
735829
- run: exit 1
736830
# Thank you, next https://github.com/vercel/next.js/blob/canary/.github/workflows/build_and_test.yml#L379

packages/tools/src/snap-test.ts

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,45 @@ function expandHome(p: string): string {
6666
return p.startsWith('~') ? path.join(homedir(), p.slice(1)) : p;
6767
}
6868

69+
function parseShard(value: string): { index: number; total: number } {
70+
const match = value.match(/^(\d+)\/(\d+)$/);
71+
if (!match) {
72+
throw new Error(
73+
`Invalid --shard format: "${value}". Expected format: --shard=<index>/<total> (e.g., --shard=1/3)`,
74+
);
75+
}
76+
const index = Number(match[1]);
77+
const total = Number(match[2]);
78+
if (total < 1) {
79+
throw new Error(`Invalid --shard total: ${total}. Must be >= 1`);
80+
}
81+
if (index < 1 || index > total) {
82+
throw new Error(`Invalid --shard index: ${index}. Must be between 1 and ${total}`);
83+
}
84+
return { index, total };
85+
}
86+
87+
function selectShard<T>(items: T[], index: number, total: number): T[] {
88+
const chunkSize = Math.ceil(items.length / total);
89+
const start = (index - 1) * chunkSize;
90+
return items.slice(start, start + chunkSize);
91+
}
92+
93+
const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests';
94+
6995
export async function snapTest() {
7096
const { positionals, values } = parseArgs({
7197
allowPositionals: true,
7298
args: process.argv.slice(3),
7399
options: {
74100
dir: { type: 'string' },
75101
'bin-dir': { type: 'string' },
102+
shard: { type: 'string' },
76103
},
77104
});
78105

79106
const filter = positionals[0] ?? ''; // Optional filter to run specific test cases
107+
const shard = values.shard ? parseShard(values.shard) : undefined;
80108

81109
// Create a unique temporary directory for testing
82110
// On macOS, `tmpdir()` is a symlink. Resolve it so that we can replace the resolved cwd in outputs.
@@ -85,6 +113,9 @@ export async function snapTest() {
85113
const systemTmpDir = fs.realpathSync(tmpdir());
86114
const tempTmpDir = `${systemTmpDir}/vite-plus-test-${randomUUID().replaceAll('-', '')}`;
87115
fs.mkdirSync(tempTmpDir, { recursive: true });
116+
// Pre-create the npm global prefix directory so tests using npm global
117+
// operations (link, outdated -g, etc.) don't fail with ENOENT.
118+
fs.mkdirSync(path.join(tempTmpDir, NPM_GLOBAL_PREFIX_DIR, 'lib'), { recursive: true });
88119

89120
// Clean up stale .node-version and package.json in the system temp directory.
90121
// vite-plus walks up the directory tree to resolve Node.js versions, so leftover
@@ -141,10 +172,10 @@ export async function snapTest() {
141172

142173
const casesDir = path.resolve(values.dir || 'snap-tests');
143174

144-
const serialTasks: (() => Promise<void>)[] = [];
145-
const parallelTasks: (() => Promise<void>)[] = [];
175+
// Collect valid test case names (sorted for deterministic sharding)
176+
const validCaseNames: string[] = [];
146177
const missingStepsJson: string[] = [];
147-
for (const caseName of fs.readdirSync(casesDir)) {
178+
for (const caseName of fs.readdirSync(casesDir).toSorted()) {
148179
if (caseName.startsWith('.')) {
149180
continue;
150181
}
@@ -158,13 +189,7 @@ export async function snapTest() {
158189
continue;
159190
}
160191
if (caseName.includes(filter)) {
161-
const steps: Steps = JSON.parse(readFileSync(stepsPath, 'utf-8'));
162-
const task = () => runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir']);
163-
if (steps.serial) {
164-
serialTasks.push(task);
165-
} else {
166-
parallelTasks.push(task);
167-
}
192+
validCaseNames.push(caseName);
168193
}
169194
}
170195

@@ -174,15 +199,35 @@ export async function snapTest() {
174199
);
175200
}
176201

202+
// Apply sharding to select a subset of test cases
203+
const selectedCases = shard
204+
? selectShard(validCaseNames, shard.index, shard.total)
205+
: validCaseNames;
206+
207+
const serialTasks: (() => Promise<void>)[] = [];
208+
const parallelTasks: (() => Promise<void>)[] = [];
209+
for (const caseName of selectedCases) {
210+
const stepsPath = path.join(casesDir, caseName, 'steps.json');
211+
const steps: Steps = JSON.parse(readFileSync(stepsPath, 'utf-8'));
212+
const task = () => runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir']);
213+
if (steps.serial) {
214+
serialTasks.push(task);
215+
} else {
216+
parallelTasks.push(task);
217+
}
218+
}
219+
177220
const totalCount = serialTasks.length + parallelTasks.length;
178221
if (totalCount > 0) {
179222
const cpuCount = cpus().length;
223+
const shardInfo = shard ? `, shard ${shard.index}/${shard.total}` : '';
180224
console.log(
181-
'Running %d test cases (%d serial + %d parallel, concurrency limit %d)',
225+
'Running %d test cases (%d serial + %d parallel, concurrency limit %d%s)',
182226
totalCount,
183227
serialTasks.length,
184228
parallelTasks.length,
185229
cpuCount,
230+
shardInfo,
186231
);
187232
await runWithConcurrencyLimit(serialTasks, 1);
188233
await runWithConcurrencyLimit(parallelTasks, cpuCount);
@@ -316,7 +361,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
316361
// Skip `vp install` inside `vp migrate` — snap tests don't need real installs
317362
VP_SKIP_INSTALL: '1',
318363
// make sure npm install global packages to the temporary directory
319-
NPM_CONFIG_PREFIX: path.join(tempTmpDir, 'npm-global-lib-for-snap-tests'),
364+
NPM_CONFIG_PREFIX: path.join(tempTmpDir, NPM_GLOBAL_PREFIX_DIR),
320365

321366
// A test case can override/unset environment variables above.
322367
// For example, VP_CLI_TEST/CI can be unset to test the real-world outputs.

0 commit comments

Comments
 (0)