@cloudflare/vitest-pool-workers
Version:
Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime
370 lines (366 loc) • 11.3 kB
JavaScript
// src/worker/index.ts
import assert3 from "node:assert";
import { Buffer as Buffer3 } from "node:buffer";
import events from "node:events";
import process from "node:process";
import * as vm from "node:vm";
import defines from "__VITEST_POOL_WORKERS_DEFINES";
import {
createWorkerEntrypointWrapper,
internalEnv,
maybeHandleRunRequest,
registerHandlerAndGlobalWaitUntil,
runInRunnerObject,
setEnv
} from "cloudflare:test-internal";
import * as devalue from "devalue";
// ../miniflare/src/workers/core/devalue.ts
import assert from "node:assert";
import { Buffer } from "node:buffer";
import { parse, stringify } from "devalue";
var ALLOWED_ARRAY_BUFFER_VIEW_CONSTRUCTORS = [
DataView,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array
];
var ALLOWED_ERROR_CONSTRUCTORS = [
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
Error
// `Error` last so more specific error subclasses preferred
];
var structuredSerializableReducers = {
ArrayBuffer(value) {
if (value instanceof ArrayBuffer) {
return [Buffer.from(value).toString("base64")];
}
},
ArrayBufferView(value) {
if (ArrayBuffer.isView(value)) {
return [
value.constructor.name,
value.buffer,
value.byteOffset,
value.byteLength
];
}
},
Error(value) {
for (const ctor of ALLOWED_ERROR_CONSTRUCTORS) {
if (value instanceof ctor && value.name === ctor.name) {
return [value.name, value.message, value.stack, value.cause];
}
}
if (value instanceof Error) {
return ["Error", value.message, value.stack, value.cause];
}
}
};
var structuredSerializableRevivers = {
ArrayBuffer(value) {
assert(Array.isArray(value));
const [encoded] = value;
assert(typeof encoded === "string");
const view = Buffer.from(encoded, "base64");
return view.buffer.slice(
view.byteOffset,
view.byteOffset + view.byteLength
);
},
ArrayBufferView(value) {
assert(Array.isArray(value));
const [name, buffer, byteOffset, byteLength] = value;
assert(typeof name === "string");
assert(buffer instanceof ArrayBuffer);
assert(typeof byteOffset === "number");
assert(typeof byteLength === "number");
const ctor = globalThis[name];
assert(ALLOWED_ARRAY_BUFFER_VIEW_CONSTRUCTORS.includes(ctor));
let length = byteLength;
if ("BYTES_PER_ELEMENT" in ctor) length /= ctor.BYTES_PER_ELEMENT;
return new ctor(buffer, byteOffset, length);
},
Error(value) {
assert(Array.isArray(value));
const [name, message, stack, cause] = value;
assert(typeof name === "string");
assert(typeof message === "string");
assert(stack === void 0 || typeof stack === "string");
const ctor = globalThis[name];
assert(ALLOWED_ERROR_CONSTRUCTORS.includes(ctor));
const error = new ctor(message, { cause });
error.stack = stack;
return error;
}
};
// src/shared/chunking-socket.ts
import assert2 from "node:assert";
import { Buffer as Buffer2 } from "node:buffer";
function createChunkingSocket(socket, maxChunkByteLength = 1048576) {
const listeners = [];
const decoder = new TextDecoder();
let chunks;
socket.on((message) => {
if (typeof message === "string") {
if (chunks !== void 0) {
assert2.strictEqual(message, "", "Expected end-of-chunks");
message = chunks + decoder.decode();
chunks = void 0;
}
for (const listener of listeners) {
listener(message);
}
} else {
chunks ??= "";
chunks += decoder.decode(message, { stream: true });
}
});
return {
post(value) {
if (Buffer2.byteLength(value) > maxChunkByteLength) {
const encoded = Buffer2.from(value);
for (let i = 0; i < encoded.byteLength; i += maxChunkByteLength) {
socket.post(encoded.subarray(i, i + maxChunkByteLength));
}
socket.post("");
} else {
socket.post(value);
}
},
on(listener) {
listeners.push(listener);
}
};
}
// src/worker/index.ts
export * from "__VITEST_POOL_WORKERS_USER_OBJECT";
function structuredSerializableStringify(value) {
return devalue.stringify(value, structuredSerializableReducers);
}
function structuredSerializableParse(value) {
return devalue.parse(value, structuredSerializableRevivers);
}
globalThis.Buffer = Buffer3;
globalThis.process = process;
process.argv = [];
var cwd;
process.cwd = () => {
assert3(cwd !== void 0, "Expected cwd to be set");
return cwd;
};
Object.setPrototypeOf(process, events.EventEmitter.prototype);
globalThis.__console = console;
function getCallerFileName(of) {
const originalStackTraceLimit = Error.stackTraceLimit;
const originalPrepareStackTrace = Error.prepareStackTrace;
try {
let fileName;
Error.stackTraceLimit = 1;
Error.prepareStackTrace = (_error, callSites) => {
fileName = callSites[0]?.getFileName();
return "";
};
const error = {};
Error.captureStackTrace(error, of);
void error.stack;
return fileName;
} finally {
Error.stackTraceLimit = originalStackTraceLimit;
Error.prepareStackTrace = originalPrepareStackTrace;
}
}
var originalSetTimeout = globalThis.setTimeout;
var originalClearTimeout = globalThis.clearTimeout;
var timeoutPromiseResolves = /* @__PURE__ */ new Map();
var monkeypatchedSetTimeout = (...args) => {
const [callback, delay, ...restArgs] = args;
const callbackName = args[0]?.name ?? "";
const callerFileName = getCallerFileName(monkeypatchedSetTimeout);
const fromVitest = /\/node_modules\/(\.store\/)?vitest/.test(
callerFileName ?? ""
);
if (!fromVitest || delay) {
return originalSetTimeout.apply(globalThis, args);
}
if (callbackName === "NOOP") {
return -0.5;
}
let promiseResolve;
const promise = new Promise((resolve) => {
promiseResolve = resolve;
});
assert3(promiseResolve !== void 0);
registerHandlerAndGlobalWaitUntil(promise);
const id = originalSetTimeout.call(globalThis, () => {
promiseResolve?.();
callback?.(...restArgs);
});
timeoutPromiseResolves.set(id, promiseResolve);
return id;
};
globalThis.setTimeout = monkeypatchedSetTimeout;
globalThis.clearTimeout = (...args) => {
const id = args[0];
if (id === -0.5) {
return;
}
const maybePromiseResolve = timeoutPromiseResolves.get(id);
timeoutPromiseResolves.delete(id);
maybePromiseResolve?.();
return originalClearTimeout.apply(globalThis, args);
};
function isDifferentIOContextError(e) {
return e instanceof Error && e.message.startsWith("Cannot perform I/O on behalf of a different");
}
var WebSocketMessagePort = class extends events.EventEmitter {
constructor(socket) {
super();
this.socket = socket;
this.#chunkingSocket = createChunkingSocket({
post(message) {
socket.send(message);
},
on(listener) {
socket.addEventListener("message", (event) => {
listener(event.data);
});
}
});
this.#chunkingSocket.on((message) => {
const parsed = structuredSerializableParse(message);
this.emit("message", parsed);
});
socket.accept();
}
#chunkingSocket;
postMessage(data) {
const stringified = structuredSerializableStringify(data);
try {
if (this.socket.readyState === WebSocket.READY_STATE_OPEN) {
this.#chunkingSocket.post(stringified);
}
} catch (error) {
if (isDifferentIOContextError(error)) {
const promise = runInRunnerObject(internalEnv, () => {
this.#chunkingSocket.post(stringified);
}).catch((e) => {
__console.error("Error sending to pool inside runner:", e, data);
});
registerHandlerAndGlobalWaitUntil(promise);
} else {
__console.error("Error sending to pool:", error, data);
}
}
}
};
function reduceError(e) {
return {
name: e?.name,
message: e?.message ?? String(e),
stack: e?.stack,
cause: e?.cause === void 0 ? void 0 : reduceError(e.cause)
};
}
var patchedFunction = false;
function ensurePatchedFunction(unsafeEval) {
if (patchedFunction) {
return;
}
patchedFunction = true;
globalThis.Function = new Proxy(globalThis.Function, {
construct(_target, args, _newTarget) {
const script = args.pop();
return unsafeEval.newFunction(script, "anonymous", ...args);
}
});
}
function applyDefines() {
for (const [key, value] of Object.entries(defines)) {
const segments = key.split(".");
let target = globalThis;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (i === segments.length - 1) {
target[segment] = value;
} else {
target = target[segment] ??= {};
}
}
}
}
var RunnerObject = class {
executor;
constructor(_state, env) {
vm._setUnsafeEval(env.__VITEST_POOL_WORKERS_UNSAFE_EVAL);
ensurePatchedFunction(env.__VITEST_POOL_WORKERS_UNSAFE_EVAL);
setEnv(env);
applyDefines();
}
async handleVitestRunRequest(request) {
assert3.strictEqual(request.headers.get("Upgrade"), "websocket");
const { 0: poolSocket, 1: poolResponseSocket } = new WebSocketPair();
const workerDataHeader = request.headers.get("MF-Vitest-Worker-Data");
assert3(workerDataHeader !== null);
const wd = structuredSerializableParse(workerDataHeader);
assert3(typeof wd === "object" && wd !== null);
assert3("filePath" in wd && typeof wd.filePath === "string");
assert3("name" in wd && typeof wd.name === "string");
assert3("data" in wd && typeof wd.data === "object" && wd.data !== null);
assert3("cwd" in wd && typeof wd.cwd === "string");
cwd = wd.cwd;
const port = new WebSocketMessagePort(poolSocket);
try {
const module = await import(wd.filePath);
const { VitestExecutor } = await import("vitest/execute");
const originalResolveUrl = VitestExecutor.prototype.resolveUrl;
const that = this;
VitestExecutor.prototype.resolveUrl = function(...args) {
that.executor = this;
return originalResolveUrl.apply(this, args);
};
wd.data.port = port;
module[wd.name](wd.data).then(() => {
poolSocket.close(1e3, "Done");
}).catch((e) => {
port.postMessage({ vitestPoolWorkersError: e });
const error = reduceError(e);
__console.error("Error running worker:", error.stack);
poolSocket.close(1011, "Internal Error");
});
} catch (e) {
const error = reduceError(e);
__console.error("Error initialising worker:", error.stack);
return Response.json(error, {
status: 500,
headers: { "MF-Experimental-Error-Stack": "true" }
});
}
return new Response(null, { status: 101, webSocket: poolResponseSocket });
}
async fetch(request) {
const response = await maybeHandleRunRequest(request, this);
if (response !== void 0) {
return response;
}
return this.handleVitestRunRequest(request);
}
};
var worker_default = createWorkerEntrypointWrapper("default");
export {
RunnerObject,
worker_default as default
};
//# sourceMappingURL=index.mjs.map