From 0e9d65636437d662140803ffec24c4d627906b32 Mon Sep 17 00:00:00 2001 From: Nicholas Paun Date: Fri, 17 Jan 2025 12:23:56 -0800 Subject: [PATCH] WPT: Run tests with unsafeEval so they can use IOContext --- build/wpt_test.bzl | 17 ++++----- src/workerd/api/wpt/url-test.ts | 5 ++- src/wpt/harness.ts | 67 +++++++++++++++++++++------------ 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/build/wpt_test.bzl b/build/wpt_test.bzl index 365bd9ce064..d2d5d5abd58 100644 --- a/build/wpt_test.bzl +++ b/build/wpt_test.bzl @@ -117,7 +117,7 @@ def _wpt_wd_test_gen_impl(ctx): test_name = ctx.attr.test_name, test_config = ctx.file.test_config.basename, test_js_generated = wd_relative_path(ctx.file.test_js_generated), - modules = generate_external_modules(ctx.attr.wpt_directory.files), + bindings = generate_external_bindings(ctx.attr.wpt_directory.files), ), ) @@ -135,10 +135,11 @@ const unitTests :Workerd.Config = ( (name = "worker", esModule = embed "{test_js_generated}"), (name = "{test_config}", esModule = embed "{test_config}"), (name = "wpt:harness", esModule = embed "../../../../../workerd/src/wpt/harness.js"), - {modules} ], bindings = [ (name = "wpt", service = "wpt"), + (name = "unsafe", unsafeEval = void), + {bindings} ], compatibilityDate = embed "../../../../../workerd/src/workerd/io/trimmed-supported-compatibility-date.txt", compatibilityFlags = ["nodejs_compat", "experimental"], @@ -159,13 +160,11 @@ def wd_relative_path(file): return "../" * 4 + file.short_path -def generate_external_modules(files): +def generate_external_bindings(files): """ - Generates a string for all files in the given directory in the specified format. - Example for a JS file: - (name = "url-origin.any.js", esModule = embed "../../../../../wpt/url/url-origin.any.js"), - Example for a JSON file: - (name = "resources/urltestdata.json", json = embed "../../../../../wpt/url/resources/urltestdata.json"), + Generates appropriate bindings for each file in the WPT module: + - JS files: text binding to allow code to be evaluated + - JSON files: JSON binding to allow test code to fetch resources """ result = [] @@ -173,7 +172,7 @@ def generate_external_modules(files): for file in files.to_list(): file_path = wd_relative_path(file) if file.extension == "js": - entry = """(name = "{}", esModule = embed "{}")""".format(file.basename, file_path) + entry = """(name = "{}", text = embed "{}")""".format(file.basename, file_path) elif file.extension == "json": # TODO(soon): It's difficult to manipulate paths in Bazel, so we assume that all JSONs are in a resources/ directory for now entry = """(name = "resources/{}", json = embed "{}")""".format(file.basename, file_path) diff --git a/src/workerd/api/wpt/url-test.ts b/src/workerd/api/wpt/url-test.ts index d37602899dc..f4f525e21ae 100644 --- a/src/workerd/api/wpt/url-test.ts +++ b/src/workerd/api/wpt/url-test.ts @@ -50,11 +50,12 @@ export default { ], }, 'url-setters-a-area.window.js': { - comment: 'Implement promise_test', + comment: 'Implement globalThis.document', skipAllTests: true, }, 'urlencoded-parser.any.js': { - comment: 'Implement unsafeRequire', + comment: + 'Requests fail due to HTTP method "LADIDA", responses fail due to shift_jis encoding', expectedFailures: [ 'request.formData() with input: test', 'response.formData() with input: test', diff --git a/src/wpt/harness.ts b/src/wpt/harness.ts index e10a5cfa790..68d20091692 100644 --- a/src/wpt/harness.ts +++ b/src/wpt/harness.ts @@ -57,8 +57,13 @@ export type TestRunnerConfig = { [key: string]: TestRunnerOptions; }; +type Env = { + unsafe: { eval: (code: string) => void }; + [key: string]: unknown; +}; + type TestCase = { - test(): Promise; + test(_: unknown, env: Env): Promise; }; type TestRunnerFn = (callback: TestFn | PromiseTestFn, message: string) => void; @@ -71,9 +76,12 @@ declare global { var errors: Error[]; // eslint-disable-next-line no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis var testOptions: TestRunnerOptions; - // eslint-disable-next-line no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis var GLOBAL: { isWindow(): boolean }; + // eslint-disable-next-line no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis + var env: Env; + // eslint-disable-next-line no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis + var promises: { [name: string]: Promise }; function test(func: TestFn, name: string): void; function done(): undefined; @@ -82,12 +90,12 @@ declare global { testType: TestRunnerFn, testCallback: TestFn | PromiseTestFn, testMessage: string - ): void | Promise; + ): void; function promise_test( func: PromiseTestFn, name: string, properties?: unknown - ): Promise; + ): void; function assert_equals(a: unknown, b: unknown, message?: string): void; function assert_not_equals(a: unknown, b: unknown, message?: string): void; function assert_true(val: unknown, message?: string): void; @@ -178,22 +186,14 @@ globalThis.Window = Object.getPrototypeOf(globalThis).constructor; globalThis.fetch = async ( input: RequestInfo | URL, _init?: RequestInit + // eslint-disable-next-line @typescript-eslint/require-await -- We are emulating an existing interface that returns a promise ): Promise => { const url = input instanceof Request ? input.url.toString() : input.toString(); - const exports: unknown = await import(url); - - if ( - !(typeof exports == 'object' && exports !== null && 'default' in exports) - ) { - throw new Error(`Cannot fetch ${url}`); - } - - const data: unknown = exports.default; - + const exports: unknown = env[url]; const response = new Response(); // eslint-disable-next-line @typescript-eslint/require-await -- We are emulating an existing interface that returns a promise - response.json = async (): Promise => data; + response.json = async (): Promise => exports; return response; }; @@ -213,7 +213,7 @@ globalThis.subsetTestByKey = ( testType, testCallback, testMessage -): void | Promise => { +): void => { // This function is designed to allow selecting only certain tests when // running in a browser, by changing the query string. We'll always run // all the tests. @@ -222,13 +222,13 @@ globalThis.subsetTestByKey = ( return testType(testCallback, testMessage); }; -globalThis.promise_test = async (func, name, _properties): Promise => { +globalThis.promise_test = (func, name, _properties): void => { if (!shouldRunTest(name)) { return; } try { - await func.call(this); + globalThis.promises[name] = func.call(this); } catch (err) { globalThis.errors.push(new AggregateError([err], name)); } @@ -438,12 +438,25 @@ function shouldRunTest(message: string): boolean { return true; } -function prepare(options: TestRunnerOptions): void { +function prepare(env: Env, options: TestRunnerOptions): void { globalThis.errors = []; globalThis.testOptions = options; + globalThis.env = env; + globalThis.promises = {}; } -function validate(testFileName: string, options: TestRunnerOptions): void { +async function validate( + testFileName: string, + options: TestRunnerOptions +): Promise { + for (const [name, promise] of Object.entries(globalThis.promises)) { + try { + await promise; + } catch (err) { + globalThis.errors.push(new AggregateError([err], name)); + } + } + const expectedFailures = new Set(options.expectedFailures ?? []); let failing = false; @@ -452,6 +465,9 @@ function validate(testFileName: string, options: TestRunnerOptions): void { err.message = sanitize_unpaired_surrogates(err.message); console.error(err); failing = true; + } else if (options.verbose) { + err.message = sanitize_unpaired_surrogates(err.message); + console.warn('Expected failure: ', err); } } @@ -471,15 +487,18 @@ export function run(config: TestRunnerConfig, file: string): TestCase { const options = config[file] ?? {}; return { - async test(): Promise { + async test(_: unknown, env: Env): Promise { if (options.skipAllTests) { console.warn(`All tests in ${file} have been skipped.`); return; } - prepare(options); - await import(file); - validate(file, options); + prepare(env, options); + if (typeof env[file] !== 'string') { + throw new Error(`Unable to run ${file}. Code is not a string`); + } + env.unsafe.eval(env[file]); + await validate(file, options); }, }; }