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
202 lines
5.9 KiB
TypeScript
202 lines
5.9 KiB
TypeScript
import childProcess from "node:child_process";
|
|
import events from "node:events";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import util from "node:util";
|
|
import { removeDir } from "@cloudflare/workers-utils";
|
|
import { stripAnsi } from "miniflare";
|
|
import treeKill from "tree-kill";
|
|
import dedent from "ts-dedent";
|
|
import { test as baseTest, inject, vi } from "vitest";
|
|
|
|
const debuglog = util.debuglog("vitest-pool-workers:test");
|
|
|
|
export const vitestConfig = (
|
|
cfOptions: Record<string, unknown> = {},
|
|
testOptions: Record<string, unknown> = {}
|
|
) => dedent /* javascript */ `
|
|
import { cloudflareTest } from "@cloudflare/vitest-pool-workers"
|
|
|
|
import { BaseSequencer } from "vitest/node";
|
|
|
|
class DeterministicSequencer extends BaseSequencer {
|
|
sort(files) {
|
|
return [...files].sort((a, b) => a.moduleId.localeCompare(b.moduleId));
|
|
}
|
|
}
|
|
|
|
export default {
|
|
plugins: [
|
|
cloudflareTest({
|
|
miniflare: {
|
|
compatibilityDate: "2025-12-02",
|
|
compatibilityFlags: ["nodejs_compat"],
|
|
},
|
|
...${JSON.stringify(cfOptions)}
|
|
})
|
|
],
|
|
test: {
|
|
sequence: { sequencer: DeterministicSequencer },
|
|
testTimeout: 90_000,
|
|
...${JSON.stringify(testOptions)}
|
|
}
|
|
};
|
|
`;
|
|
|
|
export function waitFor<T>(callback: Parameters<typeof vi.waitFor<T>>[0]) {
|
|
// The default timeout of `vi.waitFor()` is only 1s, which is a little
|
|
// short for some of these tests, especially on Windows.
|
|
return vi.waitFor(callback, { timeout: 10_000, interval: 500 });
|
|
}
|
|
|
|
async function seed(root: string, files: Record<string, string>) {
|
|
for (const [name, contents] of Object.entries(files)) {
|
|
const filePath = path.resolve(root, name);
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, contents);
|
|
}
|
|
}
|
|
|
|
export interface Process {
|
|
readonly stdout: string;
|
|
readonly stderr: string;
|
|
readonly exitCode: Promise<number>;
|
|
}
|
|
|
|
function wrap(proc: childProcess.ChildProcess): Process {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
proc.stdout?.setEncoding("utf8");
|
|
proc.stderr?.setEncoding("utf8");
|
|
proc.stdout?.on("data", (chunk) => {
|
|
if (debuglog.enabled) {
|
|
process.stdout.write(chunk);
|
|
}
|
|
stdout += chunk;
|
|
});
|
|
proc.stderr?.on("data", (chunk) => {
|
|
if (debuglog.enabled) {
|
|
process.stderr.write(chunk);
|
|
}
|
|
stderr += chunk;
|
|
});
|
|
const closePromise = events.once(proc, "close");
|
|
return {
|
|
get stdout() {
|
|
return stripAnsi(stdout);
|
|
},
|
|
get stderr() {
|
|
return stripAnsi(stderr);
|
|
},
|
|
get exitCode() {
|
|
return closePromise.then(
|
|
([exitCode, signal]) => exitCode ?? signal ?? -1
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line jest/expect-expect, jest/no-disabled-tests -- base test fixture definition, not an actual test
|
|
export const test = baseTest.extend<{
|
|
tmpPath: string;
|
|
seed: (files: Record<string, string>) => Promise<void>;
|
|
vitestRun: (options?: {
|
|
flags?: string[];
|
|
maxBuffer?: number;
|
|
}) => Promise<Process>;
|
|
vitestDev: (options?: { flags?: string[]; maxBuffer?: number }) => Process;
|
|
}>({
|
|
// Fixture for creating a temporary directory
|
|
// eslint-disable-next-line no-empty-pattern -- Vitest requires the 1st argument to use object destructuring
|
|
async tmpPath({}, use) {
|
|
const tmpPoolInstallationPath = inject("tmpPoolInstallationPath");
|
|
const tmpPathBase = path.join(tmpPoolInstallationPath, "test-");
|
|
const tmpPath = await fs.mkdtemp(tmpPathBase);
|
|
await use(tmpPath);
|
|
await removeDir(tmpPath);
|
|
},
|
|
// Fixture for seeding data in the temporary directory
|
|
async seed({ tmpPath }, use) {
|
|
await use((files) => seed(tmpPath, files));
|
|
},
|
|
// Fixture for a starting single-shot `vitest run` process
|
|
async vitestRun({ tmpPath }, use) {
|
|
const tmpPoolInstallationPath = inject("tmpPoolInstallationPath");
|
|
|
|
await use(async ({ flags = [], maxBuffer } = {}) => {
|
|
// eslint-disable-next-line workers-sdk/no-unsafe-command-execution -- test helper
|
|
const proc = childProcess.exec(
|
|
`pnpm exec vitest run --root="${tmpPath}" ` + flags.join(" "),
|
|
{
|
|
cwd: tmpPoolInstallationPath,
|
|
env: getNoCIEnv(),
|
|
maxBuffer,
|
|
}
|
|
);
|
|
const wrapped = wrap(proc);
|
|
await wrapped.exitCode;
|
|
return wrapped;
|
|
});
|
|
},
|
|
// Fixture for a starting long-running `vitest dev` process
|
|
async vitestDev({ tmpPath }, use) {
|
|
const tmpPoolInstallationPath = inject("tmpPoolInstallationPath");
|
|
|
|
// In case this process stops unexpectedly, kill all child processes
|
|
const processes = new Set<childProcess.ChildProcess>();
|
|
const killAllProcesses = () => {
|
|
for (const proc of processes) {
|
|
if (proc.pid) {
|
|
proc.stdout?.destroy();
|
|
proc.stderr?.destroy();
|
|
proc.stdin?.destroy();
|
|
treeKill(proc.pid, "SIGKILL");
|
|
}
|
|
}
|
|
};
|
|
process.on("exit", killAllProcesses);
|
|
|
|
await use(({ flags = [], maxBuffer } = {}) => {
|
|
// eslint-disable-next-line workers-sdk/no-unsafe-command-execution -- test helper
|
|
const proc = childProcess.exec(
|
|
`pnpm exec vitest dev --root="${tmpPath}" ` + flags.join(" "),
|
|
{
|
|
cwd: tmpPoolInstallationPath,
|
|
env: getNoCIEnv(),
|
|
maxBuffer,
|
|
}
|
|
);
|
|
processes.add(proc);
|
|
proc.on("exit", () => {
|
|
if (proc) {
|
|
processes.delete(proc);
|
|
}
|
|
});
|
|
return wrap(proc);
|
|
});
|
|
|
|
killAllProcesses();
|
|
process.off("exit", killAllProcesses);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Get a copy of the process.env that will not be interpreted by Vitest as running in CI.
|
|
*
|
|
* This is important for the snapshot update tests to execute correctly.
|
|
* Vitest uses the `std-env` library's `isCI()` call to determine this.
|
|
* Since we currently use GitHub Actions to run our CI jobs, this is what we are turning off here.
|
|
* If we change CI provider then we should update this.
|
|
*/
|
|
function getNoCIEnv(): typeof process.env {
|
|
const env = { ...process.env };
|
|
env.CI = undefined;
|
|
env.GITHUB_ACTIONS = undefined;
|
|
// Suppress Node.js deprecation warnings in spawned processes to prevent
|
|
// them from appearing in stderr (which breaks tests that assert stderr is empty)
|
|
env.NODE_OPTIONS = [env.NODE_OPTIONS, "--no-deprecation"]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
return env;
|
|
}
|