-
Notifications
You must be signed in to change notification settings - Fork 21
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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]" | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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", | ||
); |
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", | ||
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(); |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without this, workerd fail:
(process is used for |
||
|
||
// ---- 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 ---- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have all Node.js upstream tests for |
||
|
||
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/" }, | ||
); | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems not being picked by workerd.