deno/tests/wpt/runner/runner.ts
snek a15cafeb3b
feat(ext/web): transferable {Readable,Writable,Transform}Stream (#31126)
https://streams.spec.whatwg.org/#rs-transfer
https://streams.spec.whatwg.org/#ws-transfer
https://streams.spec.whatwg.org/#ts-transfer

Remaining test failures are due to our `DOMException` not correctly
being serializable and can be solved in a followup.


```js
// example

const INDEX_HTML = Deno.readTextFileSync("./index.html");
const worker = new Worker("./the_algorithm.js", { type: "module" });
Deno.serve(async (req) => {
  if (req.method === "POST" && req.path === "/the-algorithm") {
    const { port1, port2 } = new MessageChannel();
    worker.postMessage({ stream: req.body, port: port1 }, { transfer: [req.body, port1] });
    const res = await new Promise((resolve) => {
      port1.onmessage = (e) => resolve(e.data);
    });
    return new Response(res);
  }
  if (req.path === "/") {
    return new Response(INDEX_HTML, { "content-type": "text/html" });
  }
  return new Response(null, { status: 404 });
});
```

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
2025-12-08 12:31:43 +00:00

273 lines
6.9 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
import {
delay,
join,
ROOT_PATH,
TextLineStream,
toFileUrl,
} from "../../../tools/util.js";
import { assert, denoBinary, ManifestTestOptions, runPy } from "./utils.ts";
import { DOMParser } from "https://deno.land/x/deno_dom@v0.1.3-alpha2/deno-dom-wasm.ts";
export async function runWithTestUtil<T>(
verbose: boolean,
f: () => Promise<T>,
): Promise<T> {
const proc = runPy([
"wpt",
"serve",
"--config",
"../../../tests/wpt/runner/config.json",
], {
stdout: verbose ? "inherit" : "piped",
stderr: verbose ? "inherit" : "piped",
});
const start = performance.now();
while (true) {
await delay(1000);
try {
const req = await fetch("http://localhost:8000/");
await req.body?.cancel();
if (req.status == 200) {
break;
}
} catch (_err) {
// do nothing if this fails
}
const passedTime = performance.now() - start;
if (passedTime > 15000) {
try {
proc.kill("SIGINT");
} catch {
// Might have already died
}
await proc.status;
throw new Error("Timed out while trying to start wpt test util.");
}
}
if (verbose) console.log(`Started wpt test util.`);
try {
return await f();
} finally {
if (verbose) console.log("Killing wpt test util.");
try {
proc.kill("SIGINT");
} catch {
// Might have already died
}
await proc.status;
}
}
export interface TestResult {
cases: TestCaseResult[];
harnessStatus: TestHarnessStatus | null;
duration: number;
status: number;
stderr: string;
}
export interface TestHarnessStatus {
status: number;
message: string | null;
stack: string | null;
}
export interface TestCaseResult {
name: string;
passed: boolean;
status: number;
message: string | null;
stack: string | null;
}
export async function runSingleTest(
url: URL,
_options: ManifestTestOptions,
reporter: (result: TestCaseResult) => void,
inspectBrk: boolean,
timeouts: { long: number; default: number },
): Promise<TestResult> {
const timeout = _options.timeout === "long"
? timeouts.long
: timeouts.default;
const filename = url.pathname.substring(
url.pathname.lastIndexOf("/") + 1,
url.pathname.indexOf("."),
);
const { title } = Object.fromEntries(_options.script_metadata || []);
const bundle = await generateBundle(url);
const tempFile = await Deno.makeTempFile({
prefix: "wpt-bundle-",
suffix: ".js",
});
let interval;
try {
await Deno.writeTextFile(tempFile, bundle);
const startTime = new Date().getTime();
const args = [
"run",
"-A",
"--unstable-webgpu",
"--unstable-net",
"--v8-flags=--expose-gc",
];
if (inspectBrk) {
args.push("--inspect-brk");
}
args.push(
"--enable-testing-features-do-not-use",
"--location",
url.toString(),
"--cert",
join(ROOT_PATH, `./tests/wpt/runner/certs/cacert.pem`),
tempFile,
"[]",
);
const start = performance.now();
const proc = new Deno.Command(denoBinary(), {
args,
env: {
NO_COLOR: "1",
},
stdout: "null",
stderr: "piped",
}).spawn();
const cases = [];
let stderr = "";
let harnessStatus = null;
const lines = proc.stderr.pipeThrough(new TextDecoderStream()).pipeThrough(
new TextLineStream(),
);
interval = setInterval(() => {
const passedTime = performance.now() - start;
if (passedTime > timeout) {
proc.kill("SIGINT");
}
}, 1000);
for await (const line of lines) {
if (line.startsWith("{")) {
const data = JSON.parse(line);
const result = { ...data, passed: data.status == 0 };
if (/^Untitled( \d+)?$/.test(result.name)) {
result.name = `${title || filename}${result.name.slice(8)}`;
}
cases.push(result);
reporter(result);
} else if (line.startsWith("#$#$#{")) {
harnessStatus = JSON.parse(line.slice(5));
} else {
stderr += line + "\n";
}
}
const duration = new Date().getTime() - startTime;
const { code } = await proc.status;
return {
status: code,
harnessStatus,
duration,
cases,
stderr,
};
} finally {
clearInterval(interval);
await Deno.remove(tempFile);
}
}
function getShim(test: string): string {
const shim = [];
shim.push("globalThis.window = globalThis;");
if (test.includes("streams/transferable")) {
shim.push(`
{
const { port1, port2 } = new MessageChannel();
port2.addEventListener('message', (e) => {
queueMicrotask(() => {
globalThis.dispatchEvent(e);
});
});
port2.start();
globalThis.postMessage = (message, targetOriginOrOptions, transfer) => {
let options = targetOriginOrOptions;
if (transfer || typeof targetOriginOrOptions === 'string') {
options = { transfer };
}
return port1.postMessage(message, options);
};
}
`);
}
return shim.join("\n");
}
async function generateBundle(location: URL): Promise<string> {
const res = await fetch(location);
const body = await res.text();
const doc = new DOMParser().parseFromString(body, "text/html");
assert(doc, "document should have been parsed");
const scripts = doc.getElementsByTagName("script");
const title = doc.getElementsByTagName("title")[0]?.childNodes[0]?.nodeValue;
const scriptContents = [];
let inlineScriptCount = 0;
if (title) {
const url = new URL(`#${inlineScriptCount}`, location);
inlineScriptCount++;
scriptContents.push([
url.href,
`globalThis.META_TITLE=${JSON.stringify(title)}`,
]);
}
const shim = getShim(location.pathname);
for (const script of scripts) {
const src = script.getAttribute("src");
if (src === "/resources/testharnessreport.js") {
const url = toFileUrl(
join(ROOT_PATH, "./tests/wpt/runner/testharnessreport.js"),
);
const contents = await Deno.readTextFile(url);
scriptContents.push([url.href, shim]);
scriptContents.push([url.href, contents]);
} else if (src) {
const url = new URL(src, location);
const res = await fetch(url);
if (res.ok) {
const contents = await res.text();
scriptContents.push([url.href, shim]);
scriptContents.push([url.href, contents]);
}
} else {
const url = new URL(`#${inlineScriptCount}`, location);
inlineScriptCount++;
scriptContents.push([url.href, shim]);
scriptContents.push([url.href, script.textContent]);
}
}
return scriptContents.map(([url, contents]) => `
(function() {
const [_,err] = Deno[Deno.internal].core.evalContext(${
JSON.stringify(contents)
}, ${JSON.stringify(url)});
if (err !== null) {
throw err?.thrown;
}
})();`).join("\n");
}