Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: setup workerd isolate tests #311

Merged
merged 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- run: pnpm lint
- run: pnpm test:types
- run: pnpm test:node
- run: pnpm test:workerd
- run: pnpm build
- name: nightly release
if: |
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@
"test": "pnpm lint && pnpm test:types && pnpm test:node",
"test:cf": "pnpm jiti test/cloudflare.ts",
"test:deno": "NODE_NO_WARNINGS=1 pnpm jiti test/deno.ts",
"test:node-coverage": "node test/node-coverage.mjs",
"test:node": "node --test --import jiti/register ./test/node/test-*",
"test:node-coverage": "node test/node-coverage.mjs",
"test:node:watch": "node --test --watch --import jiti/register ./test/node/test-*",
"test:types": "tsc --noEmit",
"test:vc": "pnpm jiti test/vercel.ts"
"test:vc": "pnpm jiti test/vercel.ts",
"test:workerd": "node test/workerd/main.mjs"
},
"dependencies": {
"defu": "^6.1.4",
Expand All @@ -47,16 +48,19 @@
"ufo": "^1.5.4"
},
"devDependencies": {
"@parcel/watcher": "^2.4.1",
"@types/node": "^22.5.5",
"automd": "^0.3.8",
"changelogen": "^0.5.7",
"consola": "^3.2.3",
"esbuild": "^0.23.1",
"eslint": "^9.10.0",
"eslint-config-unjs": "^0.3.2",
"jiti": "2.0.0-rc.1",
"prettier": "^3.3.3",
"typescript": "^5.6.2",
"unbuild": "^2.0.0",
"workerd": "^1.20240909.0",
"wrangler": "^3.78.5"
},
"packageManager": "[email protected]"
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions test/workerd/config.capnp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Workerd = import "../../node_modules/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
(name = "tests", worker = .testsWorker),
],
);

const testsWorker :Workerd.Worker = (
modules = [
(name = "tests", esModule = embed "./tests.mjs")
],
compatibilityDate = "2024-09-01",
compatibilityFlags = ["nodejs_compat"],
moduleFallback = "localhost:8888",
);
195 changes: 195 additions & 0 deletions test/workerd/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { dirname, join } from "node:path";
import { existsSync } from "node:fs";
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
import workerd from "workerd";

// CLI args
const watchMode = process.argv.includes("--watch");

// Dirs
export const testsDir = fileURLToPath(new URL(".", import.meta.url));
export const rootDir = fileURLToPath(new URL("../..", import.meta.url));
export const srcDir = join(rootDir, "src");

/**
* Test runner main
*/
async function main() {
// Print info
console.log(
`Workerd: ${workerd.version} (compatibility date: ${workerd.compatibilityDate})`,
);

// Start module server
const server = await createModuleServer(8888);
server.unref();

// Run tests once
if (runTests() === false) {
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

// Start watcher
if (watchMode) {
const watcher = await import("@parcel/watcher").then((r) => r.default);
const watchDirs = [srcDir, testsDir];
console.log(
`Watching for changes:\n${watchDirs.map((d) => ` - ${d}`).join("\n")}`,
);
for (const dir of watchDirs) {
watcher.subscribe(
dir,
() => {
console.clear();
runTests();
},
{ ignore: [".tmp"] },
);
}
}
}

/**
* Spawn workerd to run tests
*/
function runTests() {
try {
if (runTests.proc) {
runTests.proc.kill();
runTests.proc = undefined;
}
console.log(`Running tests...`);
const workerdBin = workerd.default;
runTests.proc = spawn(
workerdBin,
["test", "--experimental", "config.capnp"],
{
cwd: testsDir,
stdio: "inherit",
env: {
...process.env,
LLVM_SYMBOLIZER: findLLVMsymbolizer(),
},
},
);
} catch (error) {
if (error) {
console.error(error.stdout || error);
}
return false;
}
}

/**
* Try to llvm-symbolizer binary in common locations
*/
function findLLVMsymbolizer() {
if (process.env.LLVM_SYMBOLIZER) {
return process.env.LLVM_SYMBOLIZER;
}
const paths = [
"/opt/homebrew/opt/llvm/bin/llvm-symbolizer",
"/usr/bin/llvm-symbolizer",
];
for (const path of paths) {
if (existsSync(path)) {
return path;
}
}
return "llvm-symbolizer";
}

/**
* Create fallback module server
*
* Reference:
* https://github.com/cloudflare/workerd/pull/1423
* https://github.com/cloudflare/workerd/tree/main/samples/module_fallback
*/
async function createModuleServer(port = 8888) {
// Unenv preset
const { createJiti } = await import("jiti");
const jiti = createJiti(import.meta.url);
const unenv = await jiti.import("../../src/index.ts");
const preset = unenv.env(unenv.nodeless, unenv.cloudflare);
const alias = Object.fromEntries(
Object.entries(preset.alias).map(([k, v]) => [
k,
v.replace("unenv/runtime", join(srcDir, "runtime")),
]),
);

// Use esbuild to bundle
const { build } = await import("esbuild");

const server = createServer(async (req, res) => {
try {
const resolveMethod = req.headers["x-resolve-method"];
const url = new URL(req.url, "http://localhost");
const referrer = url.searchParams.get("referrer");
const specifier = url.searchParams.get("specifier");
const rawSpecifier = url.searchParams.get("rawSpecifier");

console.log(
`[server] ${rawSpecifier} ${referrer ? `from ${referrer}` : ""}`,
);

// unenv/runtime/*
const unenvPath = /^unenv\/runtime\/(.*)$/.exec(rawSpecifier)?.[1];
if (!unenvPath) {
res.writeHead(404);
return res.end();
}

// Load node module
// prettier-ignore
const entryDir = join(srcDir, "runtime", unenvPath);
const entryFile = existsSync(join(entryDir, "$cloudflare.ts"))
? join(entryDir, "$cloudflare.ts")
: join(entryDir, "index.ts");

const transpiled = await build({
entryPoints: [entryFile],
banner: {
js: `/*\n * Raw specifier: ${rawSpecifier}\n * Source: ${entryFile}\n */\n`,
},
bundle: true,
write: false,
format: "esm",
target: "esnext",
platform: "node",
sourcemap: "inline",
pi0 marked this conversation as resolved.
Show resolved Hide resolved
alias,
});

const esModule = transpiled.outputFiles[0].text;

if (process.env.DUMP_MODULES) {
const dumpPath = join(testsDir, ".tmp", rawSpecifier + ".mjs");
await mkdir(dirname(dumpPath), { recursive: true });
await writeFile(dumpPath, esModule, "utf8");
}

res.end(JSON.stringify({ esModule }));
} catch (error) {
console.error("[server]", error);
res.writeHead(500);
res.end();
}
});

return new Promise((resolve) => {
server.listen({ port, host: "localhost" }, () => {
console.log(
`Module fallback server listening on http://localhost:${port}`,
);
resolve(server);
});
});
}

await main();
32 changes: 32 additions & 0 deletions test/workerd/tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "node:assert";
import process from "node:process";

globalThis.process = process;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, workerd fail:

workerd/io/worker.c++:1950: info: uncaught exception; source = Uncaught (in promise); stack = ReferenceError: process is not defined
    at unenv/runtime/node/crypto:387:21
workerd/io/io-context.c++:348: info: uncaught exception; exception = workerd/jsg/_virtual_includes/jsg/workerd/jsg/value.h:1367: failed: jsg.ReferenceError: process is not defined

(process is used for process.getBuiltinModule("node:crypto")...)

@jasnell @anonrig is this expected?


// ---- node:crypto ----

export const crypto_getRandomValues = {
async test() {
const crypto = await import("unenv/runtime/node/crypto");

const array = new Uint32Array(10);
crypto.getRandomValues(array);
assert.strictEqual(array.length, 10);
assert(array.every((v) => v >= 0 && v <= 0xff_ff_ff_ff));
},
};

// ---- node:url ----
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have all Node.js upstream tests for url, in test/node/* now. But since this entry is inlined in capnp config, I'm not sure how to dynamically bring them. If we can, we can reuse same impl.


export const url_parse = {
async test() {
const url = await import("unenv/runtime/node/url");

assert.throws(
() => {
url.parse("http://[127.0.0.1\u0000c8763]:8000/");
},
{ code: "ERR_INVALID_URL", input: "http://[127.0.0.1\u0000c8763]:8000/" },
);
},
};