Skip to content

Commit a81daea

Browse files
authored
Auto-discover new WPT as they are added (#3329)
1 parent c2818d8 commit a81daea

11 files changed

+552
-360
lines changed

build/wd_test.bzl

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ def wd_test(
55
data = [],
66
name = None,
77
args = [],
8+
ts_deps = [],
89
**kwargs):
910
"""Rule to define tests that run `workerd test` with a particular config.
1011
@@ -38,7 +39,7 @@ def wd_test(
3839
source_map = True,
3940
composite = True,
4041
declaration = True,
41-
deps = ["//src/node:node@tsproject"],
42+
deps = ["//src/node:node@tsproject"] + ts_deps,
4243
)
4344
data += [js_src.removesuffix(".ts") + ".js" for js_src in ts_srcs]
4445

build/wpt_test.bzl

+118-21
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,121 @@
22
# Licensed under the Apache 2.0 license found in the LICENSE file or at:
33
# https://opensource.org/licenses/Apache-2.0
44

5-
# The public entry point is a macro named wpt_test. It first invokes a private
6-
# rule named _wpt_test_gen to access the files in the wpt filegroup and
7-
# generate a corresponding wd-test file. It then invokes the wd_test macro
8-
# to set up the test.
9-
105
load("//:build/wd_test.bzl", "wd_test")
116

12-
def wpt_test(name, wpt_directory, test_js):
13-
test_gen_rule = "{}@_wpt_test_gen".format(name)
14-
_wpt_test_gen(
15-
name = test_gen_rule,
7+
def wpt_test(name, wpt_directory, test_config):
8+
"""
9+
Main entry point.
10+
1. Generates a workerd test suite in JS. This contains the logic to run
11+
each WPT test file, applying the relevant test config.
12+
2. Generates a wd-test file for this test suite. This contains all of the
13+
paths to modules needed to run the test: generated test suite, test config
14+
file, WPT test scripts, associated JSON resources.
15+
"""
16+
17+
js_test_gen_rule = "{}@_wpt_js_test_gen".format(name)
18+
test_config_as_js = test_config.removesuffix(".ts") + ".js"
19+
20+
_wpt_js_test_gen(
21+
name = js_test_gen_rule,
1622
test_name = name,
1723
wpt_directory = wpt_directory,
18-
test_js = test_js,
24+
test_config = test_config_as_js,
25+
)
26+
27+
wd_test_gen_rule = "{}@_wpt_wd_test_gen".format(name)
28+
_wpt_wd_test_gen(
29+
name = wd_test_gen_rule,
30+
test_name = name,
31+
wpt_directory = wpt_directory,
32+
test_config = test_config_as_js,
33+
test_js_generated = js_test_gen_rule,
1934
)
2035

2136
wd_test(
2237
name = "{}".format(name),
23-
src = test_gen_rule,
38+
src = wd_test_gen_rule,
2439
args = ["--experimental"],
40+
ts_deps = ["//src/wpt:wpt-test-harness"],
2541
data = [
2642
"//src/wpt:wpt-test-harness",
27-
test_js,
43+
test_config,
44+
js_test_gen_rule,
2845
wpt_directory,
2946
"//src/workerd/io:trimmed-supported-compatibility-date.txt",
3047
],
3148
)
3249

33-
def _wpt_test_gen_impl(ctx):
50+
def _wpt_js_test_gen_impl(ctx):
51+
"""
52+
Generates a workerd test suite in JS. This contains the logic to run
53+
each WPT test file, applying the relevant test config.
54+
"""
55+
56+
src = ctx.actions.declare_file("{}-test.generated.js".format(ctx.attr.test_name))
57+
ctx.actions.write(
58+
output = src,
59+
content = WPT_JS_TEST_TEMPLATE.format(
60+
test_config = ctx.file.test_config.basename,
61+
cases = generate_external_cases(ctx.attr.wpt_directory.files),
62+
),
63+
)
64+
65+
return DefaultInfo(
66+
files = depset([src]),
67+
)
68+
69+
def generate_external_cases(files):
70+
"""
71+
Generate a workerd test case that runs each test file in the WPT module.
72+
"""
73+
74+
result = []
75+
76+
for file in files.to_list():
77+
if file.extension == "js":
78+
entry = """export const {} = run(config, '{}');""".format(test_case_name(file.basename), file.basename)
79+
result.append(entry)
80+
81+
return "\n".join(result)
82+
83+
def test_case_name(filename):
84+
"""
85+
Converts a JS filename to a valid JS identifier for use as a test case name.
86+
WPT files are named with the convention some-words-with-hyphens.some-suffix.js.
87+
We would turn this into someWordsWithHyphensSomeSuffix.
88+
"""
89+
90+
words = (filename
91+
.removesuffix(".js")
92+
.removesuffix(".any")
93+
.replace(".", "-")
94+
.split("-"))
95+
96+
return words[0] + "".join([word.capitalize() for word in words[1:]])
97+
98+
WPT_JS_TEST_TEMPLATE = """// This file is autogenerated by wpt_test.bzl
99+
// DO NOT EDIT.
100+
import {{ run }} from 'wpt:harness';
101+
import config from '{test_config}';
102+
103+
{cases}
104+
"""
105+
106+
def _wpt_wd_test_gen_impl(ctx):
107+
"""
108+
Generates a wd-test file for this test suite. This contains all of the
109+
paths to modules needed to run the test: generated test suite, test config
110+
file, WPT test scripts, associated JSON resources.
111+
"""
112+
34113
src = ctx.actions.declare_file("{}.wd-test".format(ctx.attr.test_name))
35114
ctx.actions.write(
36115
output = src,
37-
content = WPT_TEST_TEMPLATE.format(
116+
content = WPT_WD_TEST_TEMPLATE.format(
38117
test_name = ctx.attr.test_name,
39-
test_js = wd_relative_path(ctx.file.test_js),
118+
test_config = ctx.file.test_config.basename,
119+
test_js_generated = wd_relative_path(ctx.file.test_js_generated),
40120
modules = generate_external_modules(ctx.attr.wpt_directory.files),
41121
),
42122
)
@@ -45,14 +125,15 @@ def _wpt_test_gen_impl(ctx):
45125
files = depset([src]),
46126
)
47127

48-
WPT_TEST_TEMPLATE = """
128+
WPT_WD_TEST_TEMPLATE = """
49129
using Workerd = import "/workerd/workerd.capnp";
50130
const unitTests :Workerd.Config = (
51131
services = [
52132
( name = "{test_name}",
53133
worker = (
54134
modules = [
55-
(name = "worker", esModule = embed "{test_js}"),
135+
(name = "worker", esModule = embed "{test_js_generated}"),
136+
(name = "{test_config}", esModule = embed "{test_config}"),
56137
(name = "wpt:harness", esModule = embed "../../../../../workerd/src/wpt/harness.js"),
57138
{modules}
58139
],
@@ -75,6 +156,7 @@ def wd_relative_path(file):
75156
Returns a relative path which can be referenced in the .wd-test file.
76157
This is four directories up from the bazel short_path
77158
"""
159+
78160
return "../" * 4 + file.short_path
79161

80162
def generate_external_modules(files):
@@ -85,6 +167,7 @@ def generate_external_modules(files):
85167
Example for a JSON file:
86168
(name = "resources/urltestdata.json", json = embed "../../../../../wpt/url/resources/urltestdata.json"),
87169
"""
170+
88171
result = []
89172

90173
for file in files.to_list():
@@ -102,14 +185,28 @@ def generate_external_modules(files):
102185

103186
return ",\n".join(result)
104187

105-
_wpt_test_gen = rule(
106-
implementation = _wpt_test_gen_impl,
188+
_wpt_wd_test_gen = rule(
189+
implementation = _wpt_wd_test_gen_impl,
190+
attrs = {
191+
# A string to use as the test name. Used in the wd-test filename and the worker's name
192+
"test_name": attr.string(),
193+
# A file group representing a directory of wpt tests. All files in the group will be embedded.
194+
"wpt_directory": attr.label(),
195+
# A JS file containing the test configuration.
196+
"test_config": attr.label(allow_single_file = True),
197+
# An auto-generated JS file containing the test logic.
198+
"test_js_generated": attr.label(allow_single_file = True),
199+
},
200+
)
201+
202+
_wpt_js_test_gen = rule(
203+
implementation = _wpt_js_test_gen_impl,
107204
attrs = {
108205
# A string to use as the test name. Used in the wd-test filename and the worker's name
109206
"test_name": attr.string(),
110207
# A file group representing a directory of wpt tests. All files in the group will be embedded.
111208
"wpt_directory": attr.label(),
112-
# A JS file containing the actual test logic.
113-
"test_js": attr.label(allow_single_file = True),
209+
# A JS file containing the test configuration.
210+
"test_config": attr.label(allow_single_file = True),
114211
},
115212
)

src/workerd/api/wpt/BUILD.bazel

+28-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,34 @@
22
# Licensed under the Apache 2.0 license found in the LICENSE file or at:
33
# https://opensource.org/licenses/Apache-2.0
44

5+
load("@npm//:eslint/package_json.bzl", eslint_bin = "bin")
56
load("//:build/wpt_test.bzl", "wpt_test")
67

8+
srcs = glob(["*-test.ts"])
9+
710
[wpt_test(
8-
name = file.replace("-test.js", ""),
9-
test_js = file,
10-
wpt_directory = "@wpt//:{}".format(file.replace("-test.js", "")),
11-
) for file in glob(["*-test.js"])]
11+
name = file.replace("-test.ts", ""),
12+
test_config = file,
13+
wpt_directory = "@wpt//:{}".format(file.replace("-test.ts", "")),
14+
) for file in srcs]
15+
16+
eslint_bin.eslint_test(
17+
name = "all@eslint",
18+
size = "large",
19+
args = [
20+
"--config $(location {})".format("eslint.config.mjs"),
21+
"--parser-options project:$(location {})".format("tsconfig.json"),
22+
"-f stylish",
23+
"--report-unused-disable-directives",
24+
] + ["$(location " + src + ")" for src in srcs],
25+
data = srcs + [
26+
"eslint.config.mjs",
27+
"tsconfig.json",
28+
"//tools:base-eslint",
29+
],
30+
tags = ["lint"],
31+
target_compatible_with = select({
32+
"@platforms//os:windows": ["@platforms//:incompatible"],
33+
"//conditions:default": [],
34+
}),
35+
)

src/workerd/api/wpt/eslint.config.mjs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { baseConfig } from '../../../../tools/base.eslint.config.mjs';
2+
3+
export default [
4+
...baseConfig({ tsconfigRootDir: import.meta.dirname }),
5+
{
6+
files: ['src/workerd/api/wpt/*-test.js'],
7+
rules: {
8+
'sort-keys': 'error',
9+
},
10+
},
11+
];

src/workerd/api/wpt/tsconfig.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"lib": ["ESNext", "dom"],
6+
"alwaysStrict": true,
7+
"strict": true,
8+
"allowJs": true,
9+
"allowUnreachableCode": false,
10+
"allowUnusedLabels": false,
11+
"exactOptionalPropertyTypes": true,
12+
"noFallthroughCasesInSwitch": true,
13+
"noImplicitOverride": true,
14+
"noImplicitReturns": true,
15+
"noPropertyAccessFromIndexSignature": false,
16+
"noUncheckedIndexedAccess": true,
17+
"noUnusedLocals": true,
18+
"noUnusedParameters": true,
19+
"strictNullChecks": true,
20+
"esModuleInterop": true,
21+
"moduleResolution": "node",
22+
"declaration": true,
23+
"composite": true,
24+
"sourceMap": true,
25+
"paths": {
26+
"wpt:*": ["../../../wpt/*"]
27+
}
28+
},
29+
"include": ["**/*.ts"],
30+
"exclude": []
31+
}

0 commit comments

Comments
 (0)