Skip to content

Commit 7071bd4

Browse files
committed
test: fix global snap test checkout resolution
1 parent ff0d4eb commit 7071bd4

5 files changed

Lines changed: 197 additions & 17 deletions

File tree

packages/cli/snap-tests-global/create-framework-shim-astro/snap.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
/// <reference types="astro/client" />
44

55
> cd my-astro-app && vp install -- --no-frozen-lockfile # install dependencies
6-
> cd my-astro-app && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors
6+
> cd my-astro-app && vp check --fix # fix generated formatting and ensure no errors
77
pass: Formatting completed for checked files (<variable>ms)
8-
pass: Found no warnings or lint errors in 6 files (<variable>ms, <variable> threads)
8+
pass: Found no warnings, lint errors, or type errors in 6 files (<variable>ms, <variable> threads)
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"ignoredPlatforms": ["win32"],
3+
"linkCheckoutPackages": true,
34
"commands": [
45
{
56
"command": "vp create astro --no-interactive -- my-astro-app --yes --skip-houston --template basics # create Astro app",
@@ -10,8 +11,6 @@
1011
"command": "cd my-astro-app && vp install -- --no-frozen-lockfile # install dependencies",
1112
"ignoreOutput": true
1213
},
13-
{
14-
"command": "cd my-astro-app && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors"
15-
}
14+
"cd my-astro-app && vp check --fix # fix generated formatting and ensure no errors"
1615
]
1716
}

packages/cli/snap-tests-global/create-framework-shim-vue/snap.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ declare module '*.vue' {
77
}
88

99
> cd vite-plus-application && vp install # install dependencies
10-
> cd vite-plus-application && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors
10+
> cd vite-plus-application && vp check --fix # fix generated formatting and ensure no errors
1111
pass: Formatting completed for checked files (<variable>ms)
12-
pass: Found no warnings or lint errors in 5 files (<variable>ms, <variable> threads)
12+
pass: Found no warnings, lint errors, or type errors in 5 files (<variable>ms, <variable> threads)
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"ignoredPlatforms": ["win32"],
3+
"linkCheckoutPackages": true,
34
"commands": [
45
{
56
"command": "vp create vite:application --no-interactive -- --template vue-ts # create Vue+TS app",
@@ -10,8 +11,6 @@
1011
"command": "cd vite-plus-application && vp install # install dependencies",
1112
"ignoreOutput": true
1213
},
13-
{
14-
"command": "cd vite-plus-application && sed -i.bak -e '/jsPlugins/d' -e '/rules:/d' -e '/options:/d' vite.config.ts && vp check --fix # fix generated formatting and ensure no errors"
15-
}
14+
"cd vite-plus-application && vp check --fix # fix generated formatting and ensure no errors"
1615
]
1716
}

packages/tools/src/snap-test.ts

Lines changed: 189 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { randomUUID } from 'node:crypto';
2-
import fs, { readFileSync } from 'node:fs';
2+
import fs from 'node:fs';
33
import fsPromises from 'node:fs/promises';
4-
import { open } from 'node:fs/promises';
54
import { cpus, homedir, tmpdir } from 'node:os';
65
import path from 'node:path';
76
import { setTimeout } from 'node:timers/promises';
@@ -92,6 +91,167 @@ function selectShard<T>(items: T[], index: number, total: number): T[] {
9291

9392
const NPM_GLOBAL_PREFIX_DIR = 'npm-global-lib-for-snap-tests';
9493

94+
function resolveGlobalCliScriptsDir(casesDir: string): string {
95+
const candidates = [
96+
// `packages/cli/snap-tests-global` -> `packages/cli/dist`
97+
path.join(path.dirname(casesDir), 'dist'),
98+
// Fallback for the common `pnpm -F vite-plus snap-test-global` cwd.
99+
path.resolve('dist'),
100+
];
101+
102+
const scriptsDir = candidates.find((dir) => fs.existsSync(path.join(dir, 'bin.js')));
103+
if (!scriptsDir) {
104+
throw new Error(
105+
`Unable to find built Vite+ CLI scripts for global snap tests. Tried:\n${candidates
106+
.map((dir) => `- ${dir}`)
107+
.join('\n')}`,
108+
);
109+
}
110+
111+
return scriptsDir;
112+
}
113+
114+
function resolveRepoRoot(casesDir: string): string {
115+
return path.resolve(path.dirname(casesDir), '..', '..');
116+
}
117+
118+
function resolveGlobalCliBinary(binDir: string): string {
119+
const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp';
120+
const binaryPath = path.join(path.resolve(expandHome(binDir)), binaryName);
121+
if (!fs.existsSync(binaryPath)) {
122+
throw new Error(`Unable to find global snap test vp binary at ${binaryPath}`);
123+
}
124+
125+
return fs.realpathSync(binaryPath);
126+
}
127+
128+
function resolveBuiltGlobalCliBinary(casesDir: string): string {
129+
const binaryName = process.platform === 'win32' ? 'vp.exe' : 'vp';
130+
const repoRoot = resolveRepoRoot(casesDir);
131+
const targetDirs = [
132+
...(process.env.CARGO_TARGET_DIR ? [process.env.CARGO_TARGET_DIR] : []),
133+
path.join(repoRoot, 'target'),
134+
];
135+
const candidates = targetDirs.flatMap((targetDir) => {
136+
const directCandidates = [
137+
path.join(targetDir, 'release', binaryName),
138+
path.join(targetDir, 'debug', binaryName),
139+
];
140+
if (!fs.existsSync(targetDir)) {
141+
return directCandidates;
142+
}
143+
144+
return [
145+
...directCandidates,
146+
...fs
147+
.readdirSync(targetDir, { withFileTypes: true })
148+
.filter((entry) => entry.isDirectory())
149+
.flatMap((entry) => [
150+
path.join(targetDir, entry.name, 'release', binaryName),
151+
path.join(targetDir, entry.name, 'debug', binaryName),
152+
]),
153+
];
154+
});
155+
const binaryPath = candidates.find((candidate) => fs.existsSync(candidate));
156+
if (!binaryPath) {
157+
throw new Error(
158+
`Unable to find built Vite+ global CLI binary for global snap tests. Tried:\n${candidates
159+
.map((candidate) => `- ${candidate}`)
160+
.join('\n')}\nRun \`cargo build -p vite_global_cli --release\` before snap-test-global.`,
161+
);
162+
}
163+
164+
return fs.realpathSync(binaryPath);
165+
}
166+
167+
function newestMtimeMs(filePath: string): number {
168+
const stats = fs.statSync(filePath);
169+
if (!stats.isDirectory()) {
170+
return stats.mtimeMs;
171+
}
172+
173+
return fs
174+
.readdirSync(filePath)
175+
.reduce(
176+
(newest, entry) => Math.max(newest, newestMtimeMs(path.join(filePath, entry))),
177+
stats.mtimeMs,
178+
);
179+
}
180+
181+
function fileContentsEqual(a: string, b: string): boolean {
182+
return fs.readFileSync(a).equals(fs.readFileSync(b));
183+
}
184+
185+
function assertGlobalCliBinaryMatchesCheckout(binDir: string, casesDir: string): void {
186+
const repoRoot = resolveRepoRoot(casesDir);
187+
const builtBinary = resolveBuiltGlobalCliBinary(casesDir);
188+
const sourcePaths = [
189+
path.join(repoRoot, 'Cargo.toml'),
190+
path.join(repoRoot, 'Cargo.lock'),
191+
path.join(repoRoot, 'crates', 'vite_global_cli', 'src'),
192+
path.join(repoRoot, 'crates', 'vite_shared', 'src'),
193+
];
194+
const shouldCheckMtime = process.env.GITHUB_ACTIONS !== 'true';
195+
const newestSourceMtime = shouldCheckMtime ? Math.max(...sourcePaths.map(newestMtimeMs)) : 0;
196+
if (shouldCheckMtime && fs.statSync(builtBinary).mtimeMs + 1000 < newestSourceMtime) {
197+
throw new Error(
198+
`Built Vite+ global CLI binary is older than the current checkout: ${builtBinary}\n` +
199+
'Run `cargo build -p vite_global_cli --release` before snap-test-global.',
200+
);
201+
}
202+
203+
const globalBinary = resolveGlobalCliBinary(binDir);
204+
if (fileContentsEqual(globalBinary, builtBinary)) {
205+
return;
206+
}
207+
208+
throw new Error(
209+
`Global snap tests would use a stale vp binary from ${globalBinary}.\n` +
210+
`Expected it to match the current checkout build at ${builtBinary}.\n` +
211+
'Run `pnpm bootstrap-cli` or `pnpm bootstrap-cli:ci` before snap-test-global.',
212+
);
213+
}
214+
215+
function replaceInstalledCheckoutPackages(rootDir: string, repoRoot: string): void {
216+
const stack = [rootDir];
217+
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
218+
const replacements = new Map([
219+
['node_modules/vite-plus', path.join(repoRoot, 'packages', 'cli')],
220+
['node_modules/vite', path.join(repoRoot, 'packages', 'core')],
221+
['node_modules/vitest', path.join(repoRoot, 'packages', 'test')],
222+
['node_modules/@voidzero-dev/vite-plus-core', path.join(repoRoot, 'packages', 'core')],
223+
['node_modules/@voidzero-dev/vite-plus-test', path.join(repoRoot, 'packages', 'test')],
224+
]);
225+
226+
while (stack.length > 0) {
227+
const dir = stack.pop()!;
228+
for (const [relativePackagePath, checkoutPackageDir] of replacements) {
229+
const candidate = path.join(dir, relativePackagePath);
230+
if (fs.existsSync(candidate) && fs.realpathSync(candidate) !== checkoutPackageDir) {
231+
fs.rmSync(candidate, { recursive: true, force: true });
232+
fs.symlinkSync(checkoutPackageDir, candidate, symlinkType);
233+
}
234+
}
235+
236+
const isNodeModulesPath = dir.split(path.sep).includes('node_modules');
237+
const isPnpmStorePath = dir.split(path.sep).includes('.pnpm');
238+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
239+
if (!entry.isDirectory() || entry.name === '.git' || entry.name === '.bin') {
240+
continue;
241+
}
242+
if (
243+
isNodeModulesPath &&
244+
!isPnpmStorePath &&
245+
entry.name !== '.pnpm' &&
246+
entry.name !== '@voidzero-dev'
247+
) {
248+
continue;
249+
}
250+
stack.push(path.join(dir, entry.name));
251+
}
252+
}
253+
}
254+
95255
export async function snapTest() {
96256
const { positionals, values } = parseArgs({
97257
allowPositionals: true,
@@ -203,13 +363,18 @@ export async function snapTest() {
203363
const selectedCases = shard
204364
? selectShard(validCaseNames, shard.index, shard.total)
205365
: validCaseNames;
366+
const globalCliScriptsDir = values['bin-dir'] ? resolveGlobalCliScriptsDir(casesDir) : undefined;
367+
if (values['bin-dir']) {
368+
assertGlobalCliBinaryMatchesCheckout(values['bin-dir'], casesDir);
369+
}
206370

207371
const serialTasks: (() => Promise<void>)[] = [];
208372
const parallelTasks: (() => Promise<void>)[] = [];
209373
for (const caseName of selectedCases) {
210374
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']);
375+
const steps: Steps = JSON.parse(fs.readFileSync(stepsPath, 'utf-8'));
376+
const task = () =>
377+
runTestCase(caseName, tempTmpDir, casesDir, values['bin-dir'], globalCliScriptsDir);
213378
if (steps.serial) {
214379
serialTasks.push(task);
215380
} else {
@@ -258,6 +423,11 @@ interface Steps {
258423
ignoredPlatforms?: (string | PlatformFilter)[];
259424
env: Record<string, string>;
260425
commands: (string | Command)[];
426+
/**
427+
* If true, installed Vite+ packages in the test project are relinked to the
428+
* current checkout after each successful command.
429+
*/
430+
linkCheckoutPackages?: boolean;
261431
/**
262432
* Commands to run after the test completes, regardless of success or failure.
263433
* Useful for cleanup tasks like killing background processes.
@@ -322,7 +492,13 @@ function shouldSkipPlatform(ignoredPlatforms: (string | PlatformFilter)[]): bool
322492
return false;
323493
}
324494

325-
async function runTestCase(name: string, tempTmpDir: string, casesDir: string, binDir?: string) {
495+
async function runTestCase(
496+
name: string,
497+
tempTmpDir: string,
498+
casesDir: string,
499+
binDir?: string,
500+
globalCliScriptsDir?: string,
501+
) {
326502
const steps: Steps = JSON.parse(
327503
await fsPromises.readFile(`${casesDir}/${name}/steps.json`, 'utf-8'),
328504
);
@@ -362,6 +538,9 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
362538
VP_SKIP_INSTALL: '1',
363539
// make sure npm install global packages to the temporary directory
364540
NPM_CONFIG_PREFIX: path.join(tempTmpDir, NPM_GLOBAL_PREFIX_DIR),
541+
// Global CLI snap tests execute the Rust binary from --bin-dir, but the JS
542+
// entry should come from this checkout instead of a stale ~/.vite-plus install.
543+
...(globalCliScriptsDir ? { VITE_GLOBAL_CLI_JS_SCRIPTS_DIR: globalCliScriptsDir } : {}),
365544

366545
// A test case can override/unset environment variables above.
367546
// For example, VP_CLI_TEST/CI can be unset to test the real-world outputs.
@@ -409,7 +588,7 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
409588
// it seems not to have stable ordering of stdout/stderr chunks.
410589
// To ensure stable ordering, we redirect outputs to a file instead.
411590
const outputStreamPath = path.join(caseTmpDir, 'output.log');
412-
const outputStream = await open(outputStreamPath, 'w');
591+
const outputStream = await fsPromises.open(outputStreamPath, 'w');
413592

414593
const exitCode = await Promise.race([
415594
execute(stripComments(cmd.command), [], {
@@ -431,8 +610,11 @@ async function runTestCase(name: string, tempTmpDir: string, casesDir: string, b
431610
]);
432611

433612
await outputStream.close();
613+
if (exitCode === 0 && globalCliScriptsDir && steps.linkCheckoutPackages) {
614+
replaceInstalledCheckoutPackages(caseTmpDir, resolveRepoRoot(casesDir));
615+
}
434616

435-
let output = readFileSync(outputStreamPath, 'utf-8');
617+
let output = fs.readFileSync(outputStreamPath, 'utf-8');
436618

437619
let commandLine = `> ${cmd.command}`;
438620
if (exitCode !== 0) {

0 commit comments

Comments
 (0)