mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 12:28:23 +00:00
fix(tests): stop blocking the event loop in installer tests
The fake release server runs inside the test process, but the tests drove install.sh with Bun.spawnSync. spawnSync blocks the event loop, so the child's HTTP request back into Bun.serve was never serviced and the test deadlocked until the 5s timeout, reporting an empty stderr. This passed on bun 1.2.x and hangs on bun 1.4.0, which is the version CI pins, so the tests only failed in CI. Drive install.sh with Bun.spawn and await the child in every test that reads from the fake server. Synchronous tests that never touch the server (--help, --print-asset, validation) keep using spawnSync. Verified with bun 1.4.0 (195 pass) and bun 1.2.22 (10 pass).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { createHash } from "crypto";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { releaseArchiveName } from "../src/utils/standalone-update";
|
||||
@@ -93,6 +93,25 @@ function runInstaller(args: string[], env: Record<string, string> = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The fake release server lives in this process, so tests that download from it
|
||||
* must not block the event loop: Bun.spawnSync would deadlock on the request the
|
||||
* child makes back into this process. Always await the child instead.
|
||||
*/
|
||||
async function runInstallerAsync(args: string[], env: Record<string, string> = {}) {
|
||||
const proc = Bun.spawn(["sh", INSTALL_SCRIPT, ...args], {
|
||||
env: { ...process.env, ...env },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
proc.exited,
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
]);
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
describe("install.sh", () => {
|
||||
test("is valid POSIX shell", () => {
|
||||
const result = Bun.spawnSync(["sh", "-n", INSTALL_SCRIPT], { stderr: "pipe" });
|
||||
@@ -144,7 +163,7 @@ describe("install.sh", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("installs the executable from a release archive", () => {
|
||||
test("installs the executable from a release archive", async () => {
|
||||
const dir = tempDir("routstrd-install-e2e-");
|
||||
const installDir = join(dir, "bin");
|
||||
const asset = releaseArchiveName(VERSION, process.platform, process.arch);
|
||||
@@ -155,7 +174,7 @@ describe("install.sh", () => {
|
||||
checksums: `${sha256Hex(archive)} ${asset}\n`,
|
||||
});
|
||||
|
||||
const result = runInstaller([
|
||||
const result = await runInstallerAsync([
|
||||
"--dir",
|
||||
installDir,
|
||||
"--api-base-url",
|
||||
@@ -164,7 +183,7 @@ describe("install.sh", () => {
|
||||
`${origin}/dl`,
|
||||
]);
|
||||
|
||||
expect(result.stderr.toString()).toContain(`Installed routstrd v${VERSION}`);
|
||||
expect(result.stderr).toContain(`Installed routstrd v${VERSION}`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const target = join(installDir, "routstrd");
|
||||
@@ -173,7 +192,7 @@ describe("install.sh", () => {
|
||||
expect(version.stdout.toString().trim()).toBe(VERSION);
|
||||
});
|
||||
|
||||
test("installs a specific version without querying the API", () => {
|
||||
test("installs a specific version without querying the API", async () => {
|
||||
const dir = tempDir("routstrd-install-pinned-");
|
||||
const installDir = join(dir, "bin");
|
||||
const asset = releaseArchiveName(VERSION, process.platform, process.arch);
|
||||
@@ -184,7 +203,7 @@ describe("install.sh", () => {
|
||||
checksums: `${sha256Hex(archive)} ${asset}\n`,
|
||||
});
|
||||
|
||||
const result = runInstaller([
|
||||
const result = await runInstallerAsync([
|
||||
"--version",
|
||||
VERSION,
|
||||
"--dir",
|
||||
@@ -199,7 +218,7 @@ describe("install.sh", () => {
|
||||
expect(statSync(join(installDir, "routstrd")).isFile()).toBe(true);
|
||||
});
|
||||
|
||||
test("refuses to install when the checksum does not match", () => {
|
||||
test("refuses to install when the checksum does not match", async () => {
|
||||
const dir = tempDir("routstrd-install-tampered-");
|
||||
const installDir = join(dir, "bin");
|
||||
const asset = releaseArchiveName(VERSION, process.platform, process.arch);
|
||||
@@ -210,7 +229,7 @@ describe("install.sh", () => {
|
||||
checksums: `${sha256Hex(new TextEncoder().encode("tampered"))} ${asset}\n`,
|
||||
});
|
||||
|
||||
const result = runInstaller([
|
||||
const result = await runInstallerAsync([
|
||||
"--dir",
|
||||
installDir,
|
||||
"--api-base-url",
|
||||
@@ -220,18 +239,18 @@ describe("install.sh", () => {
|
||||
]);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr.toString()).toContain("checksum mismatch");
|
||||
expect(result.stderr).toContain("checksum mismatch");
|
||||
expect(() => statSync(join(installDir, "routstrd"))).toThrow();
|
||||
});
|
||||
|
||||
test("reports a missing asset for the current platform", () => {
|
||||
test("reports a missing asset for the current platform", async () => {
|
||||
const dir = tempDir("routstrd-install-missing-");
|
||||
const origin = serveFakeRelease({
|
||||
asset: releaseArchiveName(VERSION, process.platform, process.arch),
|
||||
omitAsset: true,
|
||||
});
|
||||
|
||||
const result = runInstaller([
|
||||
const result = await runInstallerAsync([
|
||||
"--dir",
|
||||
join(dir, "bin"),
|
||||
"--api-base-url",
|
||||
@@ -241,16 +260,16 @@ describe("install.sh", () => {
|
||||
]);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr.toString()).toContain("may not include a build");
|
||||
expect(result.stderr).toContain("may not include a build");
|
||||
});
|
||||
|
||||
test("reports a release without SHA256SUMS", () => {
|
||||
test("reports a release without SHA256SUMS", async () => {
|
||||
const dir = tempDir("routstrd-install-nosums-");
|
||||
const asset = releaseArchiveName(VERSION, process.platform, process.arch);
|
||||
const archive = buildArchive(dir, VERSION);
|
||||
const origin = serveFakeRelease({ asset, archive, checksums: "" });
|
||||
|
||||
const result = runInstaller([
|
||||
const result = await runInstallerAsync([
|
||||
"--dir",
|
||||
join(dir, "bin"),
|
||||
"--api-base-url",
|
||||
@@ -260,10 +279,10 @@ describe("install.sh", () => {
|
||||
]);
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stderr.toString()).toContain("does not contain");
|
||||
expect(result.stderr).toContain("does not contain");
|
||||
});
|
||||
|
||||
test("honours ROUTSTRD_INSTALL_DIR when --dir is absent", () => {
|
||||
test("honours ROUTSTRD_INSTALL_DIR when --dir is absent", async () => {
|
||||
const dir = tempDir("routstrd-install-env-");
|
||||
const installDir = join(dir, "bin");
|
||||
const asset = releaseArchiveName(VERSION, process.platform, process.arch);
|
||||
@@ -274,7 +293,7 @@ describe("install.sh", () => {
|
||||
checksums: `${sha256Hex(archive)} ${asset}\n`,
|
||||
});
|
||||
|
||||
const result = runInstaller(
|
||||
const result = await runInstallerAsync(
|
||||
[
|
||||
"--api-base-url",
|
||||
origin,
|
||||
@@ -286,7 +305,7 @@ describe("install.sh", () => {
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const installed = join(installDir, "routstrd");
|
||||
chmodSync(installed, 0o755);
|
||||
expect(statSync(installed).isFile()).toBe(true);
|
||||
expect(statSync(installed).mode & 0o111).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user