Skip to content

Commit 4d22393

Browse files
committed
test(vscode): inject an unreleased toolchain into the E2E fixtures
RSTACK_E2E_TOOLCHAIN_OVERRIDES="name=<package dir>" packs each package into <fixture>/.toolchain/ and adds a file: override to every fixture's pnpm-workspace.yaml, so rstack-ecosystem-ci can run the suites against unreleased builds. Setup fails if the lockfile still holds a registry version; the yaml is not restored (disposable checkouts).
1 parent 6cf439b commit 4d22393

3 files changed

Lines changed: 113 additions & 18 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ packages/vscode/e2e/fixtures/*/pnpm-lock.yaml
157157
packages/vscode/e2e/lint/fixtures/pnpm-lock.yaml
158158
packages/vscode/e2e/lint/fixtures/*/pnpm-lock.yaml
159159
packages/vscode/e2e/rstest/fixtures/*/pnpm-lock.yaml
160+
packages/vscode/e2e/**/.toolchain/
160161

161162
# Build-time copy of the workspace root LICENSE (see rslib.config.mts)
162163
packages/vscode/LICENSE

‎packages/vscode/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
7070
- Prefer running the E2E slice that covers the change over the full chain: `pnpm test:e2e <slice ...>` (or the `test:e2e:<slice>` aliases). Slices are declared in the `SLICES` table in `e2e/run.mjs` (name, fixtures, entry) — the package.json scripts are thin forwards and carry no slice knowledge. `RSTACK_LINT_E2E_SUITES=<name,...>` filters lint suites.
7171
- Run E2E locally as `VSCODE_CLI=1 pnpm test:e2e <slice ...>`. Without it, the launched VS Code overwrites the extension host's `PATH` with a login-shell snapshot; on a machine whose login-shell `node` is below the runtime floor, the User Node preflight (correctly) refuses and every fmt test times out waiting for a server. CI is unaffected — its PATH `node` is new enough either way. The heavier alternative, `--force-disable-user-env` in `e2e/runTest.ts`, was deliberately not taken: it would change env fidelity for every slice.
7272
- Linux E2E runs only in rstack-ecosystem-ci, which relies on `e2e/run.mjs` re-execing itself under `xvfb-run -a` when `DISPLAY` is unset (the whole chain runs against one X server, so `pnpm test:e2e` is the same command everywhere; `xvfb` is a prerequisite on Linux). This repo's CI runs E2E on Windows and macOS only, so the Linux path is not exercised here.
73+
- `RSTACK_E2E_TOOLCHAIN_OVERRIDES="<name>=<package dir>,…"` packs each package into `<fixture>/.toolchain/` and adds a `file:` override to every fixture's `pnpm-workspace.yaml`, so rstack-ecosystem-ci can run the suites against unreleased builds; setup fails if the lockfile still holds a registry version. The yaml is not restored — the env is for disposable checkouts; locally, `git checkout -- packages/vscode/e2e`. Rationale in the `setupFixtures.mjs` header.

‎packages/vscode/e2e/setupFixtures.mjs‎

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// The fixtures install **exact published npm versions** of
44
// `@rslint/core` / `@rstest/core` / `rstack` — the extension resolves all three
55
// from the project, so a fixture that linked this repo's own node_modules would
6-
// test nothing. `rstack@0.7.2` itself pins `@rslint/core@0.9.0` exactly, so the
6+
// test nothing. `rstack@0.8.2` itself pins `@rslint/core@0.9.5` exactly, so the
77
// Rstack fixture pins its lint core transitively. Each fixture is its own
88
// independent install. Exact toolchain pins make installs reproducible
99
// without committed lockfiles, and Renovate bumps those pins.
@@ -22,8 +22,21 @@
2222
//
2323
// Idempotent: pnpm is a no-op when the fixture is already up to date, so
2424
// `test:e2e` can always run it.
25+
//
26+
// `RSTACK_E2E_TOOLCHAIN_OVERRIDES="@rstest/core=/abs/rstest/packages/core,…"`
27+
// installs unreleased builds instead, for rstack-ecosystem-ci (#40). Its root
28+
// `pnpm-workspace.yaml` override cannot reach the fixtures, which are their own
29+
// workspace roots, so each package is packed into `<fixture>/.toolchain/` and
30+
// the fixture yaml gets a relative `file:` override: the dependency-recovery
31+
// suites copy a fixture to tmpdir and reinstall with `--frozen-lockfile`, and
32+
// pnpm records `file:` relative to the lockfile. After install the lockfile
33+
// must resolve each overridden package through the tarball, never the
34+
// registry: build and pin can share a version number, which proves nothing.
35+
// The yaml is changed in place and never restored: the env is meant for
36+
// disposable checkouts (ecosystem CI); locally,
37+
// `git checkout -- packages/vscode/e2e` undoes it.
2538
import { spawnSync } from 'node:child_process';
26-
import { existsSync } from 'node:fs';
39+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2740
import path from 'node:path';
2841
import { fileURLToPath } from 'node:url';
2942

@@ -69,19 +82,86 @@ export const FIXTURE_NAMES = Object.keys(FIXTURES);
6982

7083
const pnpmCommand = 'pnpm';
7184

85+
/** `name=<absolute package dir>` entries, comma-separated; unset → none. */
86+
const overrides = (process.env.RSTACK_E2E_TOOLCHAIN_OVERRIDES ?? '')
87+
.split(',')
88+
.filter((entry) => entry.trim())
89+
.map((entry) => {
90+
const [pkg = '', dir = ''] = entry
91+
.split(/=(.*)/)
92+
.map((part) => part.trim());
93+
// Without this, a missing `=` packs `packages/vscode` itself.
94+
if (
95+
!pkg ||
96+
!path.isAbsolute(dir) ||
97+
!existsSync(path.join(dir, 'package.json'))
98+
) {
99+
throw new Error(
100+
`RSTACK_E2E_TOOLCHAIN_OVERRIDES: expected name=<absolute package dir>, got ${entry}`,
101+
);
102+
}
103+
return { pkg, dir, file: `${pkg.replace(/^@/, '').replace('/', '-')}.tgz` };
104+
});
105+
106+
/**
107+
* @param {string[]} args
108+
* @param {string} cwd
109+
*/
110+
const pnpm = (args, cwd) => {
111+
const result = spawnSync(pnpmCommand, args, {
112+
cwd,
113+
stdio: 'inherit',
114+
env: process.env,
115+
// On Windows, pnpm is a .cmd shim, and Node refuses to spawn batch
116+
// files without a shell (CVE-2024-27980 hardening) — EINVAL otherwise.
117+
shell: process.platform === 'win32',
118+
});
119+
if (result.error) {
120+
throw result.error;
121+
}
122+
if (result.status !== 0) {
123+
throw new Error(
124+
`pnpm ${args[0]} failed in ${cwd} (exit code ${String(result.status)})`,
125+
);
126+
}
127+
};
128+
129+
/** @param {string} cwd */
130+
const injectOverrides = (cwd) => {
131+
const yamlPath = path.join(cwd, 'pnpm-workspace.yaml');
132+
// A second run in the same checkout must not append a duplicate key.
133+
if (readFileSync(yamlPath, 'utf8').includes('.toolchain/')) {
134+
return;
135+
}
136+
mkdirSync(path.join(cwd, '.toolchain'), { recursive: true });
137+
const lines = ['# RSTACK_E2E_TOOLCHAIN_OVERRIDES: .toolchain/', 'overrides:'];
138+
for (const { pkg, dir, file } of overrides) {
139+
pnpm(['pack', '--out', path.join(cwd, '.toolchain', file)], dir);
140+
lines.push(` '${pkg}': 'file:.toolchain/${file}'`);
141+
}
142+
lines.push('');
143+
appendFileSync(yamlPath, `\n${lines.join('\n')}`);
144+
};
145+
146+
// Fixtures that do not depend on an overridden package are legitimate no-ops;
147+
// only a run where no fixture used any override is an error.
148+
let resolvedThroughTarball = 0;
149+
72150
/** @param {string} name */
73151
const install = (name) => {
74152
const cwd = FIXTURES[name];
75153
if (!existsSync(path.join(cwd, 'package.json'))) {
76154
throw new Error(`E2E fixture ${name} has no package.json at ${cwd}`);
77155
}
156+
if (overrides.length > 0) {
157+
injectOverrides(cwd);
158+
}
78159
console.log(`[e2e] installing fixture: ${name}`);
79160
// Keep pnpm's default isolated layout. In the rstack fixture the tool cores
80161
// are transitive dependencies beside rstack in the virtual store, matching
81162
// the layout users get rather than masking resolution bugs with public
82163
// hoisting.
83-
const result = spawnSync(
84-
pnpmCommand,
164+
pnpm(
85165
[
86166
'install',
87167
// Fixtures pin exact toolchain versions rather than committing lockfiles;
@@ -98,21 +178,29 @@ const install = (name) => {
98178
// build scripts as-is.
99179
'--config.dangerouslyAllowAllBuilds=true',
100180
],
101-
{
102-
cwd,
103-
stdio: 'inherit',
104-
env: process.env,
105-
// On Windows, pnpm is a .cmd shim, and Node refuses to spawn batch
106-
// files without a shell (CVE-2024-27980 hardening) — EINVAL otherwise.
107-
shell: process.platform === 'win32',
108-
},
181+
cwd,
109182
);
110-
if (result.error) {
111-
throw result.error;
112-
}
113-
if (result.status !== 0) {
114-
throw new Error(
115-
`pnpm install failed for the ${name} fixture (exit code ${String(result.status)})`,
183+
// A registry resolution is `name@1.2.3`, the tarball `name@file:…`; the
184+
// lockfile also covers transitive ones (the rstack fixture's @rstest/core).
185+
const lockfile =
186+
overrides.length > 0
187+
? readFileSync(path.join(cwd, 'pnpm-lock.yaml'), 'utf8')
188+
: '';
189+
for (const { pkg } of overrides) {
190+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
191+
// The lookbehind keeps `rstack` from matching `create-rstack@1.0.0`.
192+
if (new RegExp(`(?<![\\w@/.-])${escaped}@\\d`).test(lockfile)) {
193+
throw new Error(
194+
`\`${pkg}\` still resolves to a registry version in the ${name} fixture`,
195+
);
196+
}
197+
if (!lockfile.includes(`${pkg}@file:`)) {
198+
console.log(`[e2e] ${name}: ${pkg} not in graph, override unused`);
199+
continue;
200+
}
201+
resolvedThroughTarball += 1;
202+
console.log(
203+
`[e2e] ${name}: ${pkg} resolved through .toolchain (no registry version in pnpm-lock.yaml)`,
116204
);
117205
}
118206
};
@@ -128,6 +216,11 @@ const main = () => {
128216
}
129217
install(name);
130218
}
219+
if (overrides.length > 0 && resolvedThroughTarball === 0) {
220+
throw new Error(
221+
`RSTACK_E2E_TOOLCHAIN_OVERRIDES: no fixture in this run resolved ${overrides.map(({ pkg }) => pkg).join(', ')} through a tarball; the override did nothing`,
222+
);
223+
}
131224
};
132225

133226
main();

0 commit comments

Comments
 (0)