Skip to content

Commit

Permalink
WPT: Run tests with unsafeEval so they can use IOContext
Browse files Browse the repository at this point in the history
  • Loading branch information
npaun committed Jan 17, 2025
1 parent a50d62a commit e15ea7c
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 38 deletions.
17 changes: 8 additions & 9 deletions build/wpt_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
)

Expand All @@ -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"],
Expand All @@ -159,21 +160,19 @@ 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 = []

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)
Expand Down
5 changes: 3 additions & 2 deletions src/workerd/api/wpt/url-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
70 changes: 43 additions & 27 deletions src/wpt/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ export type TestRunnerConfig = {
[key: string]: TestRunnerOptions;
};

type Env = {
unsafe: { eval: (code: string) => void };
[key: string]: unknown;
};

type TestCase = {
test(): Promise<void>;
test(_: unknown, env: Env): Promise<void>;
};

type TestRunnerFn = (callback: TestFn | PromiseTestFn, message: string) => void;
Expand All @@ -67,13 +72,13 @@ type PromiseTestFn = () => Promise<void>;
type ThrowingFn = () => unknown;

declare global {
// eslint-disable-next-line no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis
/* eslint-disable no-var -- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#type-checking-for-globalthis */
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 };
var env: Env;
var promises: { [name: string]: Promise<void> };
/* eslint-enable no-var */

function test(func: TestFn, name: string): void;
function done(): undefined;
Expand All @@ -82,12 +87,12 @@ declare global {
testType: TestRunnerFn,
testCallback: TestFn | PromiseTestFn,
testMessage: string
): void | Promise<void>;
): void;
function promise_test(
func: PromiseTestFn,
name: string,
properties?: unknown
): Promise<void>;
): 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;
Expand Down Expand Up @@ -178,22 +183,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<Response> => {
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<unknown> => data;
response.json = async (): Promise<unknown> => exports;
return response;
};

Expand All @@ -213,7 +210,7 @@ globalThis.subsetTestByKey = (
testType,
testCallback,
testMessage
): void | Promise<void> => {
): 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.
Expand All @@ -222,13 +219,13 @@ globalThis.subsetTestByKey = (
return testType(testCallback, testMessage);
};

globalThis.promise_test = async (func, name, _properties): Promise<void> => {
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));
}
Expand Down Expand Up @@ -438,12 +435,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<void> {
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;
Expand All @@ -452,6 +462,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);
}
}

Expand All @@ -471,15 +484,18 @@ export function run(config: TestRunnerConfig, file: string): TestCase {
const options = config[file] ?? {};

return {
async test(): Promise<void> {
async test(_: unknown, env: Env): Promise<void> {
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);
},
};
}

0 comments on commit e15ea7c

Please sign in to comment.