diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db5b4f0..fc7a7d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,13 +56,11 @@ jobs: - name: Unit Test run: pnpm run test:unit - # ======== E2E: Windows + macOS, no Linux ======== - # These are the platforms the extension is actually used on, and the only - # ones upstream rstest runs its ported VS Code suites on. Linux is excluded - # deliberately: the Extension Host needs xvfb there, and inotify reports a - # non-atomic file rewrite as separate truncate/write events, so fixture edits - # in the watch-mode suites race the watcher in a way no user hits. Do not add - # a Linux E2E job back without also making every fixture edit atomic. + # ======== E2E: Windows + macOS + Linux ======== + # Windows and macOS are the platforms users run the extension on. + # Linux is here because rstack-ecosystem-ci runs every suite on + # ubuntu-latest (rstackjs/rstack-editor#40). The Extension Host needs a + # display there; `e2e/run.mjs` re-execs under `xvfb-run` when DISPLAY is unset. e2e: name: E2E (${{ matrix.os }}) runs-on: ${{ matrix.os }} @@ -70,7 +68,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest] + os: [windows-latest, macos-latest, ubuntu-latest] steps: - name: Checkout @@ -94,6 +92,17 @@ jobs: - name: Build run: pnpm run build + # The Electron runtime libraries plus xvfb. ubuntu-latest carries all + # five today, so this is a no-op there; naming them keeps the job working + # on images that lack them (`t64` is the Ubuntu 24.04 spelling). + - name: Install Extension Host Dependencies + if: runner.os == 'Linux' + run: | + dpkg -s libasound2t64 libgbm1 libgtk-3-0t64 libnss3 xvfb >/dev/null 2>&1 || { + sudo apt-get update + sudo apt-get install -y --no-install-recommends libasound2t64 libgbm1 libgtk-3-0t64 libnss3 xvfb + } + # `@vscode/test-electron` downloads a full VS Code into # `packages/vscode/.vscode-test`. Cache only the immutable distribution # directories, keyed on the extension manifest (which carries diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index dfeda2c..21eac39 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -62,3 +62,4 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - E2E fixtures install exact versions of published npm packages (not workspace links): the extension must work against what users actually install. Renovate updates the exact toolchain pins; generated fixture lockfiles and `node_modules` remain disposable and uncommitted. - Prefer running the E2E slice that covers the change over the full chain: `pnpm test:e2e ` (or the `test:e2e:` 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=` filters lint suites. - Run E2E locally as `VSCODE_CLI=1 pnpm test:e2e `. 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. +- On Linux, `e2e/run.mjs` re-execs itself under `xvfb-run -a` when `DISPLAY` is unset, so the whole chain runs against one X server and `pnpm test:e2e` is the same command everywhere. That makes `xvfb` a prerequisite on Linux; CI installs it with the Electron runtime libraries on ubuntu-latest. diff --git a/packages/vscode/e2e/run.mjs b/packages/vscode/e2e/run.mjs index d525b56..0f71938 100644 --- a/packages/vscode/e2e/run.mjs +++ b/packages/vscode/e2e/run.mjs @@ -10,6 +10,10 @@ // up-to-date fixture, and unknown names throw there), then the entries run // sequentially — each `compile`d entry launches its own VS Code via // `@vscode/test-electron`. +// +// On Linux that VS Code needs an X display, so this file re-execs itself under +// `xvfb-run` when there is none — running the whole chain, not just the +// entries, under one server. import { spawnSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -59,6 +63,29 @@ const packageRoot = path.resolve( '..', ); +// The Extension Host needs an X display. `xvfb-run` exports DISPLAY into the +// child, so the re-exec'd process fails this check — it cannot recurse. +if (process.platform === 'linux' && !process.env.DISPLAY) { + const result = spawnSync( + 'xvfb-run', + [ + '-a', + process.execPath, + fileURLToPath(import.meta.url), + ...process.argv.slice(2), + ], + { cwd: packageRoot, stdio: 'inherit' }, + ); + if (result.error) { + if (/** @type {NodeJS.ErrnoException} */ (result.error).code === 'ENOENT') { + console.error('[e2e] xvfb-run not found; install the xvfb package'); + process.exit(1); + } + throw result.error; + } + process.exit(result.status ?? 1); +} + /** * @param {string} command * @param {string[]} args