UNPKG

@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
// 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