70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
944 lines
27 KiB
TypeScript
944 lines
27 KiB
TypeScript
import childProcess, { execSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import rl from "node:readline";
|
|
import stream from "node:stream";
|
|
import { stripVTControlCharacters } from "node:util";
|
|
import { removeDir } from "@fixture/shared/src/fs-helpers";
|
|
import { fetch } from "undici";
|
|
/* eslint-disable workers-sdk/no-vitest-import-expect -- complex test with .each patterns */
|
|
import {
|
|
afterAll,
|
|
afterEach,
|
|
assert,
|
|
describe as baseDescribe,
|
|
beforeAll,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from "vitest";
|
|
/* eslint-enable workers-sdk/no-vitest-import-expect */
|
|
import { wranglerEntryPath } from "../../shared/src/run-wrangler-long-lived";
|
|
import type pty from "@cdktf/node-pty-prebuilt-multiarch";
|
|
|
|
// These tests are failing with `Error: read EPIPE` on Windows in CI. There's still value running them on macOS and Linux.
|
|
if (process.platform === "win32") {
|
|
baseDescribe("interactive dev session tests", () => {
|
|
it.skip("skipped on Windows", () => {});
|
|
});
|
|
} else {
|
|
// Windows doesn't have a built-in way to get the CWD of a process by its ID.
|
|
// This functionality is provided by the Windows Driver Kit which is installed
|
|
// on GitHub actions Windows runners.
|
|
const tlistPath =
|
|
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\tlist.exe";
|
|
let windowsProcessCwdSupported = true;
|
|
if (!fs.existsSync(tlistPath)) {
|
|
windowsProcessCwdSupported = false;
|
|
const message = [
|
|
"=".repeat(80),
|
|
"Unable to find Windows Driver Kit, skipping zombie process tests... :(",
|
|
"=".repeat(80),
|
|
].join("\n");
|
|
console.error(message);
|
|
}
|
|
|
|
const pkgRoot = path.resolve(__dirname, "..");
|
|
const ptyOptions: pty.IPtyForkOptions = {
|
|
name: "xterm-color",
|
|
cols: 80,
|
|
rows: 30,
|
|
cwd: pkgRoot,
|
|
env: process.env as Record<string, string>,
|
|
};
|
|
|
|
// Check `node-pty` installed and working correctly, skipping tests if not
|
|
let nodePtySupported = true;
|
|
try {
|
|
const pty = await import("@cdktf/node-pty-prebuilt-multiarch");
|
|
const ptyProcess = pty.spawn(
|
|
process.execPath,
|
|
["-p", "'ran node'"],
|
|
ptyOptions
|
|
);
|
|
let output = "";
|
|
ptyProcess.onData((data) => (output += data));
|
|
const code = await new Promise<number>((resolve) =>
|
|
ptyProcess.onExit(({ exitCode }) => resolve(exitCode))
|
|
);
|
|
assert.strictEqual(code, 0);
|
|
assert(output.includes("ran node"));
|
|
} catch (e) {
|
|
nodePtySupported = false;
|
|
const message = [
|
|
"=".repeat(80),
|
|
"`node-pty` unsupported, skipping interactive dev session tests... :(",
|
|
"",
|
|
"Ensure its dependencies (https://github.com/microsoft/node-pty#dependencies)",
|
|
"are installed, then re-run `pnpm install` in the repository root.",
|
|
"",
|
|
"On Windows, make sure you have `Desktop development with C++`, `Windows SDK`,",
|
|
"`MSVC VS C++ build tools`, and `MSVC VS C++ Spectre-mitigated libs` Visual",
|
|
"Studio components installed.",
|
|
"",
|
|
e instanceof Error ? e.stack : String(e),
|
|
"=".repeat(80),
|
|
].join("\n");
|
|
console.error(message);
|
|
}
|
|
const describe = baseDescribe.runIf(nodePtySupported);
|
|
|
|
interface PtyProcess {
|
|
pty: pty.IPty;
|
|
stdout: string;
|
|
exitCode: number | null;
|
|
exitPromise: Promise<number>;
|
|
url: string;
|
|
}
|
|
const processes: PtyProcess[] = [];
|
|
afterEach(() => {
|
|
for (const p of processes.splice(0)) {
|
|
// If the process didn't exit cleanly, log its output for debugging
|
|
if (p.exitCode !== 0) console.log(p.stdout);
|
|
// If the process hasn't exited yet, kill it
|
|
if (p.exitCode === null) {
|
|
// `node-pty` throws if signal passed on Windows
|
|
if (process.platform === "win32") p.pty.kill();
|
|
else p.pty.kill("SIGKILL");
|
|
}
|
|
}
|
|
});
|
|
|
|
const readyRegexp = /Ready on (http:\/\/[a-z0-9.]+:[0-9]+)/;
|
|
async function startWranglerDev(
|
|
args: string[],
|
|
skipWaitingForReady = false,
|
|
env?: Record<string, string>
|
|
) {
|
|
const stdoutStream = new stream.PassThrough();
|
|
const stdoutInterface = rl.createInterface(stdoutStream);
|
|
|
|
let exitResolve: ((code: number) => void) | undefined;
|
|
const exitPromise = new Promise<number>(
|
|
(resolve) => (exitResolve = resolve)
|
|
);
|
|
|
|
const ptyOptionsWithEnv = {
|
|
...ptyOptions,
|
|
env: env ?? (process.env as Record<string, string>),
|
|
} satisfies pty.IPtyForkOptions;
|
|
|
|
const pty = await import("@cdktf/node-pty-prebuilt-multiarch");
|
|
const ptyProcess = pty.spawn(
|
|
process.execPath,
|
|
[
|
|
wranglerEntryPath,
|
|
...args,
|
|
"--ip=127.0.0.1",
|
|
"--port=0",
|
|
"--inspector-port=0",
|
|
],
|
|
ptyOptionsWithEnv
|
|
);
|
|
const result: PtyProcess = {
|
|
pty: ptyProcess,
|
|
stdout: "",
|
|
exitCode: null,
|
|
exitPromise,
|
|
url: "",
|
|
};
|
|
processes.push(result);
|
|
ptyProcess.onData((data) => {
|
|
result.stdout += data;
|
|
stdoutStream.write(data);
|
|
});
|
|
ptyProcess.onExit(({ exitCode }) => {
|
|
result.exitCode = exitCode;
|
|
exitResolve?.(exitCode);
|
|
stdoutStream.end();
|
|
});
|
|
|
|
if (!skipWaitingForReady) {
|
|
let readyMatch: RegExpMatchArray | null = null;
|
|
for await (const line of stdoutInterface) {
|
|
if (
|
|
(readyMatch = readyRegexp.exec(stripVTControlCharacters(line))) !==
|
|
null
|
|
)
|
|
break;
|
|
}
|
|
assert(readyMatch !== null, "Expected ready message");
|
|
result.url = readyMatch[1];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
interface Process {
|
|
pid: string;
|
|
cmd: string;
|
|
}
|
|
function getProcesses(): Process[] {
|
|
if (process.platform === "win32") {
|
|
return childProcess
|
|
.execSync("tasklist /fo csv", { encoding: "utf8" })
|
|
.trim()
|
|
.split("\r\n")
|
|
.slice(1)
|
|
.map((line) => {
|
|
const [cmd, pid] = line.replaceAll('"', "").split(",");
|
|
return { pid, cmd };
|
|
});
|
|
} else {
|
|
return childProcess
|
|
.execSync("ps -e | awk '{print $1,$4}'", { encoding: "utf8" })
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const [pid, cmd] = line.split(" ");
|
|
return { pid, cmd };
|
|
});
|
|
}
|
|
}
|
|
function getProcessCwd(pid: string | number) {
|
|
if (process.platform === "win32") {
|
|
if (windowsProcessCwdSupported) {
|
|
return (
|
|
childProcess
|
|
.spawnSync(tlistPath, [String(pid)], { encoding: "utf8" })
|
|
.stdout.match(/^\s*CWD:\s*(.+)\\$/m)?.[1] ?? ""
|
|
);
|
|
} else {
|
|
return "";
|
|
}
|
|
} else {
|
|
return childProcess
|
|
.execSync(`lsof -p ${pid} | awk '$4=="cwd" {print $9}'`, {
|
|
encoding: "utf8",
|
|
})
|
|
.trim();
|
|
}
|
|
}
|
|
function getStartedWorkerdProcesses(): Process[] {
|
|
return getProcesses().filter(
|
|
({ cmd, pid }) =>
|
|
cmd.includes("workerd") && getProcessCwd(pid) === pkgRoot
|
|
);
|
|
}
|
|
|
|
const devScripts = [
|
|
{ args: ["dev"], expectedBody: "body" },
|
|
{ args: ["pages", "dev", "public"], expectedBody: "<p>body</p>" },
|
|
];
|
|
const exitKeys = [
|
|
{ name: "CTRL-C", key: "\x03" },
|
|
{ name: "x", key: "x" },
|
|
];
|
|
|
|
describe.each(devScripts)("wrangler $args", ({ args, expectedBody }) => {
|
|
it.each(exitKeys)("cleanly exits with $name", async ({ key }) => {
|
|
const beginProcesses = getStartedWorkerdProcesses();
|
|
|
|
const wrangler = await startWranglerDev(args);
|
|
const duringProcesses = getStartedWorkerdProcesses();
|
|
|
|
// Check dev server working correctly
|
|
const res = await fetch(wrangler.url);
|
|
expect((await res.text()).trim()).toBe(expectedBody);
|
|
|
|
// Check key cleanly exits dev server
|
|
wrangler.pty.write(key);
|
|
expect(await wrangler.exitPromise).toBe(0);
|
|
const endProcesses = getStartedWorkerdProcesses();
|
|
|
|
// Check no hanging workerd processes
|
|
if (process.platform !== "win32" || windowsProcessCwdSupported) {
|
|
expect(beginProcesses.length).toBe(endProcesses.length);
|
|
expect(duringProcesses.length).toBeGreaterThan(beginProcesses.length);
|
|
}
|
|
});
|
|
describe("--show-interactive-dev-session", () => {
|
|
it("should show hotkeys when interactive", async () => {
|
|
const wrangler = await startWranglerDev(args);
|
|
wrangler.pty.kill();
|
|
expect(wrangler.stdout).toContain("open a browser");
|
|
expect(wrangler.stdout).toContain("open devtools");
|
|
expect(wrangler.stdout).toContain("clear console");
|
|
expect(wrangler.stdout).toContain("to exit");
|
|
expect(wrangler.stdout).toContain("open local explorer");
|
|
expect(wrangler.stdout).not.toContain("rebuild container");
|
|
});
|
|
it("should not show hotkeys with --show-interactive-dev-session=false", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
...args,
|
|
"--show-interactive-dev-session=false",
|
|
]);
|
|
wrangler.pty.kill();
|
|
expect(wrangler.stdout).not.toContain("open a browser");
|
|
expect(wrangler.stdout).not.toContain("open devtools");
|
|
expect(wrangler.stdout).not.toContain("clear console");
|
|
expect(wrangler.stdout).not.toContain("to exit");
|
|
expect(wrangler.stdout).not.toContain("rebuild container");
|
|
expect(wrangler.stdout).not.toContain("open local explorer");
|
|
});
|
|
});
|
|
});
|
|
|
|
it.each(exitKeys)("multiworker cleanly exits with $name", async ({ key }) => {
|
|
const beginProcesses = getStartedWorkerdProcesses();
|
|
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
"wrangler.a.jsonc",
|
|
"-c",
|
|
"wrangler.b.jsonc",
|
|
]);
|
|
const duringProcesses = getStartedWorkerdProcesses();
|
|
|
|
// Check dev server working correctly
|
|
const res = await fetch(wrangler.url);
|
|
expect((await res.text()).trim()).toBe("hello from a & hello from b");
|
|
|
|
// Check key cleanly exits dev server
|
|
wrangler.pty.write(key);
|
|
expect(await wrangler.exitPromise).toBe(0);
|
|
const endProcesses = getStartedWorkerdProcesses();
|
|
|
|
// Check no hanging workerd processes
|
|
if (process.platform !== "win32" || windowsProcessCwdSupported) {
|
|
expect(beginProcesses.length).toBe(endProcesses.length);
|
|
expect(duringProcesses.length).toBeGreaterThan(beginProcesses.length);
|
|
}
|
|
});
|
|
|
|
const isCINonLinux =
|
|
process.platform !== "linux" && process.env.CI === "true";
|
|
|
|
function isDockerRunning() {
|
|
try {
|
|
execSync("docker ps", { stdio: "ignore" });
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Indicates whether the test is being run locally (not in CI) AND docker is currently not running on the system */
|
|
const isLocalWithoutDockerRunning =
|
|
process.env.CI !== "true" && !isDockerRunning();
|
|
|
|
if (isLocalWithoutDockerRunning) {
|
|
console.warn(
|
|
"The tests are running locally but there is no docker instance running on the system, skipping containers tests"
|
|
);
|
|
}
|
|
|
|
baseDescribe.skipIf(
|
|
isCINonLinux ||
|
|
// If the tests are being run locally and docker is not running we just skip this test
|
|
isLocalWithoutDockerRunning
|
|
)("containers", () => {
|
|
// it seems like if we spam the container too often, it freezes up and crashes
|
|
const WAITFOR_OPTIONS = { timeout: 2000, interval: 500 };
|
|
baseDescribe("container dev", { retry: 0, timeout: 90000 }, () => {
|
|
let tmpDir: string;
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(tmpdir(), "wrangler-container-"));
|
|
fs.cpSync(
|
|
path.resolve(__dirname, "../", "container-app"),
|
|
path.join(tmpDir),
|
|
{
|
|
recursive: true,
|
|
}
|
|
);
|
|
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
afterAll(() => {
|
|
removeDir(tmpDir, { fireAndForget: true });
|
|
});
|
|
|
|
it("should print rebuild containers hotkey", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
wrangler.pty.kill();
|
|
expect(wrangler.stdout).toContain("rebuild container");
|
|
});
|
|
|
|
it("should rebuild a container when the hotkey is pressed", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
await fetch(wrangler.url + "/start");
|
|
|
|
// wait container to be ready
|
|
await vi.waitFor(async () => {
|
|
const status = await fetch(wrangler.url + "/status", {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
expect(await status.json()).toBe(true);
|
|
}, WAITFOR_OPTIONS);
|
|
|
|
await vi.waitFor(async () => {
|
|
const res = await fetch(wrangler.url + "/fetch", {
|
|
// Sometimes this fetch can hang if the container is not ready
|
|
// The default timeout is longer than the timeout on the `waitFor()` which results in the test failing.
|
|
// So abort this request sooner to allow it to retry.
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
|
|
expect(await res.text()).toBe(
|
|
"Hello World! Have an env var! I'm an env var!"
|
|
);
|
|
}, WAITFOR_OPTIONS);
|
|
const output = wrangler.stdout;
|
|
// Extract the Docker image name from the output
|
|
const imageNameMatch = output.match(
|
|
/cloudflare-dev\/fixturetestcontainer:[a-f0-9]+/
|
|
);
|
|
expect(imageNameMatch).not.toBe(null);
|
|
const imageName = imageNameMatch![0];
|
|
expect(
|
|
JSON.parse(
|
|
execSync(
|
|
`docker image inspect ${imageName} --format "{{ json .RepoTags }}"`,
|
|
{ encoding: "utf8" }
|
|
)
|
|
).length
|
|
).toBeGreaterThanOrEqual(1);
|
|
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "container", "simple-node-app.js"),
|
|
`const { createServer } = require("http");
|
|
|
|
const server = createServer(function (req, res) {
|
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
res.write("Blah! " + process.env.MESSAGE);
|
|
res.end();
|
|
});
|
|
|
|
server.listen(8080, function () {
|
|
console.log("Server listening on port 8080");
|
|
});`,
|
|
"utf-8"
|
|
);
|
|
|
|
// Clear the captured stdout so we can match on log output
|
|
// produced AFTER the rebuild hotkey is pressed.
|
|
wrangler.stdout = "";
|
|
wrangler.pty.write("r");
|
|
|
|
// Wait for the rebuild and workerd reload to actually
|
|
// finish before asserting on `/status`. Without this,
|
|
// the old workerd (which still reports the container as
|
|
// running even after `docker rm`) can satisfy /status
|
|
// requests during the rebuild window, and requests can
|
|
// hang while miniflare is reloading — both cause flakes
|
|
// on slow CI.
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(wrangler.stdout).toMatch(/Local server updated and ready/);
|
|
},
|
|
{ timeout: 30_000, interval: 500 }
|
|
);
|
|
|
|
await vi.waitFor(async () => {
|
|
const status = await fetch(wrangler.url + "/status", {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
expect(await status.json()).toBe(false);
|
|
}, WAITFOR_OPTIONS);
|
|
|
|
await fetch(wrangler.url + "/start");
|
|
await vi.waitFor(async () => {
|
|
const status = await fetch(wrangler.url + "/status", {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
expect(await status.json()).toBe(true);
|
|
}, WAITFOR_OPTIONS);
|
|
await vi.waitFor(async () => {
|
|
const res = await fetch(wrangler.url + "/fetch", {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
expect(await res.text()).toBe("Blah! I'm an env var!");
|
|
}, WAITFOR_OPTIONS);
|
|
|
|
// Verify that the old image tag has been deleted after rebuild
|
|
expect(() => {
|
|
execSync(`docker image inspect ${imageName}`, { encoding: "utf8" });
|
|
}).toThrow();
|
|
|
|
wrangler.pty.kill();
|
|
});
|
|
|
|
it("should clean up any containers that were started", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
await fetch(wrangler.url + "/start");
|
|
|
|
// wait container to be ready
|
|
await vi.waitFor(async () => {
|
|
const ids = getContainerIds();
|
|
expect(ids.length).toBe(1);
|
|
}, WAITFOR_OPTIONS);
|
|
|
|
// ctrl + c
|
|
wrangler.pty.write("\x03");
|
|
await new Promise<void>((resolve) => {
|
|
wrangler.pty.onExit(() => resolve());
|
|
});
|
|
|
|
// `docker rm -f` of the running container after the SIGINT
|
|
// teardown can take several seconds on a busy CI runner, so
|
|
// allow more than the default 5s `vi.waitFor` timeout.
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(getContainerIds()).toHaveLength(0);
|
|
},
|
|
{ timeout: 10_000, interval: 500 }
|
|
);
|
|
});
|
|
});
|
|
|
|
baseDescribe(
|
|
"container dev where image build takes a long time",
|
|
{ retry: 0, timeout: 90000 },
|
|
() => {
|
|
let tmpDir: string;
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(
|
|
path.join(tmpdir(), "wrangler-container-sleep-")
|
|
);
|
|
fs.cpSync(
|
|
path.resolve(__dirname, "../", "container-app"),
|
|
path.join(tmpDir),
|
|
{
|
|
recursive: true,
|
|
}
|
|
);
|
|
const tmpDockerFilePath = path.join(tmpDir, "Dockerfile");
|
|
fs.rmSync(tmpDockerFilePath);
|
|
fs.renameSync(
|
|
path.join(tmpDir, "DockerfileWithLongSleep"),
|
|
tmpDockerFilePath
|
|
);
|
|
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
removeDir(tmpDir, { fireAndForget: true });
|
|
});
|
|
|
|
it("should allow quitting while the image is building", async () => {
|
|
const wrangler = await startWranglerDev(
|
|
["dev", "-c", path.join(tmpDir, "wrangler.jsonc")],
|
|
true
|
|
);
|
|
|
|
// Use a generous timeout to accommodate slow CI runners —
|
|
// 10s was occasionally tight when the docker daemon was
|
|
// under contention from other parallel fixtures.
|
|
const waitForOptions = { timeout: 30_000, interval: 500 };
|
|
|
|
// wait for long sleep instruction to start
|
|
await vi.waitFor(async () => {
|
|
expect(wrangler.stdout).toContain("RUN sleep 50000");
|
|
}, waitForOptions);
|
|
|
|
wrangler.pty.write("q");
|
|
|
|
await vi.waitFor(async () => {
|
|
expect(wrangler.stdout).toMatch(/CANCELED \[.*?\] RUN sleep 50000/);
|
|
}, waitForOptions);
|
|
});
|
|
|
|
it("should rebuilding while the image is building", async () => {
|
|
const wrangler = await startWranglerDev(
|
|
["dev", "-c", path.join(tmpDir, "wrangler.jsonc")],
|
|
true
|
|
);
|
|
|
|
// We assert on wrangler's own deterministic log line, which
|
|
// is emitted synchronously when wrangler picks up a new
|
|
// `containerBuildId` and starts the rebuild flow. We
|
|
// intentionally do NOT match "This (no-op) build takes
|
|
// forever..." or buildkit's `CANCELED` line — both come
|
|
// from docker buildkit's TTY output and are unreliable:
|
|
// the first is redrawn an unpredictable number of times
|
|
// per build, and the second is only emitted when buildkit
|
|
// has time to flush its cancellation status before the
|
|
// docker CLI is killed by the abort. Relying on either of
|
|
// those made earlier versions of this test flaky on slow
|
|
// CI (see
|
|
// https://github.com/cloudflare/workers-sdk/actions/runs/24924588002).
|
|
const PREPARING_RE = /⎔ Preparing container image\(s\)/;
|
|
const SLEEP_RE = /RUN sleep 50000/;
|
|
|
|
const waitForOptions = { timeout: 30_000, interval: 500 };
|
|
|
|
// 1. Wait for the initial build to reach the long sleep
|
|
// instruction — this is the "while the image is building"
|
|
// state that the test name refers to.
|
|
await vi.waitFor(() => {
|
|
expect(wrangler.stdout).toMatch(SLEEP_RE);
|
|
}, waitForOptions);
|
|
|
|
// 2. First `r`: should trigger another rebuild while the
|
|
// initial build is still in its long-sleep phase. Clear
|
|
// the captured stdout so the assertion below only sees
|
|
// output produced after the press.
|
|
wrangler.stdout = "";
|
|
wrangler.pty.write("r");
|
|
|
|
await vi.waitFor(() => {
|
|
expect(wrangler.stdout).toMatch(PREPARING_RE);
|
|
}, waitForOptions);
|
|
|
|
// 3. Wait for the second build to also reach the long sleep
|
|
// instruction so the next `r` press is genuinely "while
|
|
// the image is building".
|
|
await vi.waitFor(() => {
|
|
expect(wrangler.stdout).toMatch(SLEEP_RE);
|
|
}, waitForOptions);
|
|
|
|
// 4. Second `r`: should again trigger another rebuild.
|
|
// Clear stdout again so the assertion only sees the new
|
|
// Preparing log.
|
|
wrangler.stdout = "";
|
|
wrangler.pty.write("r");
|
|
|
|
await vi.waitFor(() => {
|
|
expect(wrangler.stdout).toMatch(PREPARING_RE);
|
|
}, waitForOptions);
|
|
|
|
wrangler.pty.kill();
|
|
});
|
|
}
|
|
);
|
|
|
|
baseDescribe("multi-containers dev", { retry: 0, timeout: 50000 }, () => {
|
|
let tmpDir: string;
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(
|
|
path.join(tmpdir(), "wrangler-multi-containers-")
|
|
);
|
|
fs.cpSync(
|
|
path.resolve(__dirname, "../", "multi-containers-app"),
|
|
path.join(tmpDir),
|
|
{
|
|
recursive: true,
|
|
}
|
|
);
|
|
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
afterAll(() => {
|
|
removeDir(tmpDir, { fireAndForget: true });
|
|
});
|
|
|
|
it("should print build logs for all the containers", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(wrangler.stdout).toContain('"name": "simple-node-app-a"');
|
|
expect(wrangler.stdout).toContain('"name": "simple-node-app-b"');
|
|
},
|
|
{ timeout: 10_000 }
|
|
);
|
|
wrangler.pty.kill();
|
|
});
|
|
|
|
it("should rebuild all the containers when the hotkey is pressed", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const text = await (
|
|
await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) })
|
|
).text();
|
|
expect(text).toBe(
|
|
'Response from A: "Hello from Container A" Response from B: "Hello from Container B"'
|
|
);
|
|
},
|
|
{ timeout: 30_000, interval: 1000 }
|
|
);
|
|
|
|
const tmpDockerfileAPath = path.join(tmpDir, "DockerfileA");
|
|
const dockerFileAContent = fs.readFileSync(tmpDockerfileAPath, "utf8");
|
|
fs.writeFileSync(
|
|
tmpDockerfileAPath,
|
|
dockerFileAContent.replace(
|
|
'"Hello from Container A"',
|
|
'"Hello World from Container A"'
|
|
),
|
|
"utf-8"
|
|
);
|
|
|
|
const tmpDockerfileBPath = path.join(tmpDir, "DockerfileB");
|
|
const dockerFileBContent = fs.readFileSync(tmpDockerfileBPath, "utf8");
|
|
fs.writeFileSync(
|
|
tmpDockerfileBPath,
|
|
dockerFileBContent.replace(
|
|
'"Hello from Container B"',
|
|
'"Hello from the B Container"'
|
|
),
|
|
"utf-8"
|
|
);
|
|
|
|
wrangler.pty.write("r");
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const text = await (
|
|
await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) })
|
|
).text();
|
|
expect(text).toBe(
|
|
'Response from A: "Hello World from Container A" Response from B: "Hello from the B Container"'
|
|
);
|
|
},
|
|
{ timeout: 30_000, interval: 1000 }
|
|
);
|
|
|
|
fs.writeFileSync(tmpDockerfileAPath, dockerFileAContent, "utf-8");
|
|
fs.writeFileSync(tmpDockerfileBPath, dockerFileBContent, "utf-8");
|
|
|
|
wrangler.pty.kill();
|
|
});
|
|
|
|
it("should clean up any containers that were started", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "wrangler.jsonc"),
|
|
]);
|
|
// wait container to be ready
|
|
await vi.waitFor(
|
|
async () => {
|
|
const text = await (
|
|
await fetch(wrangler.url, { signal: AbortSignal.timeout(3_000) })
|
|
).text();
|
|
expect(text).toBe(
|
|
'Response from A: "Hello from Container A" Response from B: "Hello from Container B"'
|
|
);
|
|
},
|
|
{ timeout: 30_000, interval: 1000 }
|
|
);
|
|
const ids = getContainerIds();
|
|
expect(ids.length).toBe(2);
|
|
|
|
// ctrl + c
|
|
wrangler.pty.write("\x03");
|
|
await new Promise<void>((resolve) => {
|
|
wrangler.pty.onExit(() => resolve());
|
|
});
|
|
// `docker rm -f` of multiple running containers after the
|
|
// SIGINT teardown can take several seconds on a busy CI
|
|
// runner, so allow more than the default 5s `vi.waitFor`
|
|
// timeout.
|
|
await vi.waitFor(
|
|
() => {
|
|
const remainingIds = getContainerIds();
|
|
expect(remainingIds).toHaveLength(0);
|
|
},
|
|
{ timeout: 10_000, interval: 500 }
|
|
);
|
|
});
|
|
});
|
|
|
|
baseDescribe(
|
|
"multi-workers containers dev",
|
|
{ retry: 0, timeout: 50000 },
|
|
() => {
|
|
let tmpDir: string;
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(
|
|
path.join(tmpdir(), "wrangler-multi-workers-containers-")
|
|
);
|
|
fs.cpSync(
|
|
path.resolve(__dirname, "../", "multi-workers-containers-app"),
|
|
path.join(tmpDir),
|
|
{
|
|
recursive: true,
|
|
}
|
|
);
|
|
|
|
const ids = getContainerIds();
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const ids = getContainerIds();
|
|
|
|
if (ids.length > 0) {
|
|
execSync("docker rm -f " + ids.join(" "), {
|
|
encoding: "utf8",
|
|
});
|
|
}
|
|
});
|
|
afterAll(() => {
|
|
removeDir(tmpDir, { fireAndForget: true });
|
|
});
|
|
|
|
it("should be able to interact with both workers, rebuild the containers with the hotkey and all containers should be cleaned up at the end", async () => {
|
|
const wrangler = await startWranglerDev([
|
|
"dev",
|
|
"-c",
|
|
path.join(tmpDir, "workerA/wrangler.jsonc"),
|
|
"-c",
|
|
path.join(tmpDir, "workerB/wrangler.jsonc"),
|
|
]);
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const json = await (
|
|
await fetch(wrangler.url, {
|
|
signal: AbortSignal.timeout(5_000),
|
|
})
|
|
).json();
|
|
expect(json).toEqual({
|
|
containerAText: "Hello from Container A",
|
|
containerBText: "Hello from Container B",
|
|
});
|
|
},
|
|
{ timeout: 10_000, interval: 1000 }
|
|
);
|
|
|
|
const tmpDockerfileAPath = path.join(tmpDir, "workerA/Dockerfile");
|
|
const dockerFileAContent = fs.readFileSync(
|
|
tmpDockerfileAPath,
|
|
"utf8"
|
|
);
|
|
fs.writeFileSync(
|
|
tmpDockerfileAPath,
|
|
dockerFileAContent.replace(
|
|
'"Hello from Container A"',
|
|
'"Hello World from Container A"'
|
|
),
|
|
"utf-8"
|
|
);
|
|
|
|
const tmpDockerfileBPath = path.join(tmpDir, "workerB/Dockerfile");
|
|
const dockerFileBContent = fs.readFileSync(
|
|
tmpDockerfileBPath,
|
|
"utf8"
|
|
);
|
|
fs.writeFileSync(
|
|
tmpDockerfileBPath,
|
|
dockerFileBContent.replace(
|
|
'"Hello from Container B"',
|
|
'"Hello from the B Container"'
|
|
),
|
|
"utf-8"
|
|
);
|
|
|
|
wrangler.pty.write("r");
|
|
|
|
await vi.waitFor(
|
|
async () => {
|
|
const json = await (
|
|
await fetch(wrangler.url, {
|
|
signal: AbortSignal.timeout(5_000),
|
|
})
|
|
).json();
|
|
expect(json).toEqual({
|
|
containerAText: "Hello World from Container A",
|
|
containerBText: "Hello from the B Container",
|
|
});
|
|
},
|
|
{ timeout: 30_000, interval: 1000 }
|
|
);
|
|
|
|
fs.writeFileSync(tmpDockerfileAPath, dockerFileAContent, "utf-8");
|
|
fs.writeFileSync(tmpDockerfileBPath, dockerFileBContent, "utf-8");
|
|
|
|
wrangler.pty.kill("SIGINT");
|
|
});
|
|
}
|
|
);
|
|
|
|
/** gets any containers that were created by running this fixture */
|
|
const getContainerIds = () => {
|
|
// note the -a to include stopped containers
|
|
const allContainers = execSync(`docker ps -a --format json`)
|
|
.toString()
|
|
.split("\n")
|
|
.filter((line) => line.trim());
|
|
if (allContainers.length === 0) {
|
|
return [];
|
|
}
|
|
const jsonOutput = allContainers.map((line) => JSON.parse(line));
|
|
|
|
const matches = jsonOutput.map((container) => {
|
|
if (container.Image.includes("cloudflare-dev/fixturetestcontainer")) {
|
|
return container.ID;
|
|
}
|
|
});
|
|
return matches.filter(Boolean);
|
|
};
|
|
});
|
|
}
|