Skip to content

Commit 52bd452

Browse files
npaunshrima-cf
authored andcommitted
Provide dynamic port assignments for wd_test sidecars (#3862)
* Provide dynamic port assignments for wd_test sidecars * Update node:net and node:tls tests for dynamic ports * Update WPT for dynamic ports
1 parent 26016fe commit 52bd452

File tree

13 files changed

+296
-146
lines changed

13 files changed

+296
-146
lines changed

build/BUILD.wpt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
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("@bazel_skylib//rules:write_file.bzl", "write_file")
65
load("@workerd//:build/wpt_get_directories.bzl", "wpt_get_directories")
76
load("@workerd//:build/wpt_test.bzl", "wpt_module", "wpt_server_entrypoint")
87

@@ -24,20 +23,9 @@ load("@workerd//:build/wpt_test.bzl", "wpt_module", "wpt_server_entrypoint")
2423
name = dir,
2524
) for dir in wpt_get_directories(root = "fetch")]
2625

27-
write_file(
28-
name = "gen_config_json",
29-
out = "config.json",
30-
content = [json.encode({
31-
"server_host": "localhost",
32-
"check_subdomains": False,
33-
})],
34-
visibility = ["//visibility:public"],
35-
)
36-
3726
wpt_server_entrypoint(
3827
name = "entrypoint",
3928
srcs = ["wpt"] + glob(["**/*.py"]),
40-
config_json = "config.json",
4129
python = "@python3_13_host//:python",
4230
visibility = ["//visibility:public"],
4331
)

build/wd_test.bzl

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -65,53 +65,35 @@ WINDOWS_TEMPLATE = """
6565
@echo off
6666
setlocal EnableDelayedExpansion
6767
68-
REM Start sidecar if specified
69-
if not "%SIDECAR%" == "" (
70-
start /b "" "%SIDECAR%" > nul 2>&1
71-
set SIDECAR_PID=!ERRORLEVEL!
72-
timeout /t 1 > nul
73-
)
74-
75-
REM Run the actual test
76-
$(RUNTEST)
77-
78-
REM Cleanup sidecar if it was started
79-
if defined SIDECAR_PID (
80-
taskkill /F /PID !SIDECAR_PID! > nul 2>&1
68+
REM Run supervisor to start sidecar if specified
69+
if not "{sidecar}" == "" (
70+
REM These environment variables are processed by the supervisor executable
71+
set PORTS_TO_ASSIGN={port_bindings}
72+
set SIDECAR_COMMAND="{sidecar}"
73+
powershell -Command \"{supervisor} {runtest}\"
74+
) else (
75+
{runtest}
8176
)
8277
78+
set TEST_EXIT=!ERRORLEVEL!
8379
exit /b !TEST_EXIT!
8480
"""
8581

8682
SH_TEMPLATE = """#!/bin/sh
8783
set -e
8884
89-
cleanup() {
90-
if [ ! -z "$SIDECAR_PID" ]; then
91-
kill $SIDECAR_PID 2>/dev/null || true
92-
fi
93-
}
94-
95-
trap cleanup EXIT
96-
97-
# Start sidecar if specified
98-
if [ ! -z "$(SIDECAR)" ]; then
99-
"$(SIDECAR)" & SIDECAR_PID=$!
100-
# Wait until the process is ready
101-
sleep 3
85+
# Run supervisor to start sidecar if specified
86+
if [ ! -z "{sidecar}" ]; then
87+
# These environment variables are processed by the supervisor executable
88+
PORTS_TO_ASSIGN={port_bindings} SIDECAR_COMMAND="{sidecar}" {supervisor} {runtest}
89+
else
90+
{runtest}
10291
fi
103-
104-
$(RUNTEST)
10592
"""
10693

107-
WINDOWS_RUNTEST_NORMAL = """
108-
powershell -Command \"%*\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR
109-
set TEST_EXIT=!ERRORLEVEL!
110-
"""
94+
WINDOWS_RUNTEST_NORMAL = """powershell -Command \"%*\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR"""
11195

112-
SH_RUNTEST_NORMAL = """
113-
"$@" -dTEST_TMPDIR=$TEST_TMPDIR
114-
"""
96+
SH_RUNTEST_NORMAL = """"$@" -dTEST_TMPDIR=$TEST_TMPDIR"""
11597

11698
# We need variants of the RUN_TEST command for Python memory snapshot tests. We have to invoke
11799
# workerd twice, once with --python-save-snapshot to produce the snapshot and once with
@@ -144,20 +126,30 @@ echo Using Python Snapshot
144126
def _wd_test_impl(ctx):
145127
is_windows = ctx.target_platform_has_constraint(ctx.attr._platforms_os_windows[platform_common.ConstraintValueInfo])
146128

129+
if ctx.file.sidecar and ctx.attr.python_snapshot_test:
130+
# TODO(later): Implement support for generating a combined script with these two options
131+
# if we have a test that requires it. Not doing it for now due to complexity.
132+
return print("sidecar and python_snapshot_test currently cannot be used together")
133+
147134
# Bazel insists that the rule must actually create the executable that it intends to run; it
148135
# can't just specify some other executable with some args. OK, fine, we'll use a script that
149136
# just execs its args.
150137
if is_windows:
151138
# Batch script executables must end with ".bat"
152139
executable = ctx.actions.declare_file("%s_wd_test.bat" % ctx.label.name)
153-
content = WINDOWS_TEMPLATE.replace("$(SIDECAR)", ctx.file.sidecar.path if ctx.file.sidecar else "")
140+
template = WINDOWS_TEMPLATE
154141
runtest = WINDOWS_RUNTEST_SNAPSHOT if ctx.attr.python_snapshot_test else WINDOWS_RUNTEST_NORMAL
155-
content = content.replace("$(RUNTEST)", runtest)
156142
else:
157143
executable = ctx.outputs.executable
158-
content = SH_TEMPLATE.replace("$(SIDECAR)", ctx.file.sidecar.short_path if ctx.file.sidecar else "")
144+
template = SH_TEMPLATE
159145
runtest = SH_RUNTEST_SNAPSHOT if ctx.attr.python_snapshot_test else SH_RUNTEST_NORMAL
160-
content = content.replace("$(RUNTEST)", runtest)
146+
147+
content = template.format(
148+
sidecar = ctx.file.sidecar.short_path if ctx.file.sidecar else "",
149+
runtest = runtest,
150+
supervisor = ctx.file.sidecar_supervisor.short_path if ctx.file.sidecar_supervisor else "",
151+
port_bindings = ",".join(ctx.attr.sidecar_port_bindings),
152+
)
161153

162154
ctx.actions.write(
163155
output = executable,
@@ -174,6 +166,13 @@ def _wd_test_impl(ctx):
174166
if default_runfiles:
175167
runfiles = runfiles.merge(default_runfiles)
176168

169+
runfiles = runfiles.merge(ctx.runfiles(files = [ctx.file.sidecar_supervisor]))
170+
171+
# Also merge the supervisor's own runfiles if it has any
172+
default_runfiles = ctx.attr.sidecar_supervisor[DefaultInfo].default_runfiles
173+
if default_runfiles:
174+
runfiles = runfiles.merge(default_runfiles)
175+
177176
return [
178177
DefaultInfo(
179178
executable = executable,
@@ -185,20 +184,42 @@ _wd_test = rule(
185184
implementation = _wd_test_impl,
186185
test = True,
187186
attrs = {
187+
# The workerd executable is used to run all tests
188188
"workerd": attr.label(
189189
allow_single_file = True,
190190
executable = True,
191191
cfg = "exec",
192192
default = "//src/workerd/server:workerd",
193193
),
194-
"flags": attr.string_list(),
194+
# A list of files that this test requires to be present in order to run.
195195
"data": attr.label_list(allow_files = True),
196+
# If set, an executable that is run in parallel with the test, and provides some functionality
197+
# needed for the test. This is usually a backend server, with workerd serving as the client.
198+
# The sidecar will be killed once the test completes.
196199
"sidecar": attr.label(
197200
allow_single_file = True,
198201
executable = True,
199202
cfg = "exec",
200203
),
204+
# A list of binding names which will be filled in with random port numbers that the sidecar
205+
# and test can use for communication. The test will only begin once the sidecar is
206+
# listening to these ports.
207+
#
208+
# In the sidecar, access these bindings as environment variables. In the wd-test file, add
209+
# fromEnvironment bindings to expose them to the test.
210+
#
211+
# Reminder: you'll also need to add a network = ( allow = ["private"] ) service as well.
212+
"sidecar_port_bindings": attr.string_list(),
213+
# An executable that is used to manage port assignments and child process creation when a
214+
# sidecar is specified.
215+
"sidecar_supervisor": attr.label(
216+
allow_single_file = True,
217+
executable = True,
218+
cfg = "exec",
219+
default = "//src/workerd/api/node:sidecar-supervisor",
220+
),
201221
"python_snapshot_test": attr.bool(),
222+
# A reference to the Windows platform label, needed for the implementation of wd_test
202223
"_platforms_os_windows": attr.label(default = "@platforms//os:windows"),
203224
},
204225
)

build/wpt_test.bzl

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def wpt_test(name, wpt_directory, config, autogates = [], start_server = False,
5252
name = "{}".format(name),
5353
src = wd_test_gen_rule,
5454
args = ["--experimental"],
55+
sidecar_port_bindings = ["HTTP_PORT", "HTTPS_PORT"] if start_server else [],
5556
sidecar = "@wpt//:entrypoint" if start_server else None,
5657
data = [
5758
test_config_as_js, # e.g. "url-test.js"
@@ -246,6 +247,8 @@ const unitTests :Workerd.Config = (
246247
bindings = [
247248
(name = "wpt", service = "wpt"),
248249
(name = "unsafe", unsafeEval = void),
250+
(name = "HTTP_PORT", fromEnvironment = "HTTP_PORT"),
251+
(name = "HTTPS_PORT", fromEnvironment = "HTTPS_PORT"),
249252
{bindings}
250253
],
251254
compatibilityDate = embed "{compat_date}",
@@ -309,11 +312,20 @@ def generate_external_bindings(wd_test_file, base, files):
309312
### (Create a single no-args script that starts the WPT server)
310313

311314
WPT_ENTRYPOINT_SCRIPT_TEMPLATE = """
312-
# Make /usr/sbin/sysctl visibile (Python needs to call it on macOS)
315+
# Make /usr/sbin/sysctl visible (Python needs to call it on macOS)
313316
export PATH="$PATH:/usr/sbin"
314317
315318
cd $(dirname $0)
316-
{python} wpt.py serve --config {config_json}
319+
{python} wpt.py serve --no-h2 --config /dev/stdin <<EOF
320+
{{
321+
"server_host": "localhost",
322+
"check_subdomains": false,
323+
"ports": {{
324+
"http": [$HTTP_PORT, "auto"],
325+
"https": [$HTTPS_PORT, "auto"]
326+
}}
327+
}}
328+
EOF
317329
"""
318330

319331
def _wpt_server_entrypoint_impl(ctx):
@@ -330,14 +342,13 @@ def _wpt_server_entrypoint_impl(ctx):
330342
is_executable = True,
331343
content = WPT_ENTRYPOINT_SCRIPT_TEMPLATE.format(
332344
python = ctx.file.python.short_path,
333-
config_json = ctx.file.config_json.basename,
334345
),
335346
)
336347

337348
return DefaultInfo(
338349
runfiles = ctx.runfiles(
339350
files = [start_src],
340-
transitive_files = depset(ctx.files.srcs + [ctx.file.python, ctx.file.config_json]),
351+
transitive_files = depset(ctx.files.srcs + [ctx.file.python]),
341352
),
342353
executable = start_src,
343354
)
@@ -349,8 +360,6 @@ wpt_server_entrypoint = rule(
349360
"python": attr.label(allow_single_file = True),
350361
# All the Python code that should be visible
351362
"srcs": attr.label_list(allow_files = True),
352-
# Config file to pass to wpt serve
353-
"config_json": attr.label(allow_single_file = True),
354363
},
355364
)
356365

src/workerd/api/node/BUILD.bazel

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,11 @@ wd_test(
318318
args = ["--experimental"],
319319
data = ["tests/net-nodejs-test.js"],
320320
sidecar = "net-nodejs-tcp-server",
321-
target_compatible_with = select({
322-
# TODO(soon): Investigate why this test timeout on Windows.
323-
"@platforms//os:windows": ["@platforms//:incompatible"],
324-
"//conditions:default": [],
325-
}),
321+
sidecar_port_bindings = [
322+
"SERVER_PORT",
323+
"ECHO_SERVER_PORT",
324+
"TIMEOUT_SERVER_PORT",
325+
],
326326
)
327327

328328
wd_test(
@@ -377,9 +377,14 @@ wd_test(
377377
"tests/tls-nodejs-test.js",
378378
],
379379
sidecar = "tls-nodejs-tcp-server",
380-
target_compatible_with = select({
381-
# TODO(soon): Investigate why this test timeout on Windows.
382-
"@platforms//os:windows": ["@platforms//:incompatible"],
383-
"//conditions:default": [],
384-
}),
380+
sidecar_port_bindings = [
381+
"ECHO_SERVER_PORT",
382+
"HELLO_SERVER_PORT",
383+
],
384+
)
385+
386+
js_binary(
387+
name = "sidecar-supervisor",
388+
entry_point = "tests/sidecar-supervisor.mjs",
389+
visibility = ["//visibility:public"],
385390
)

src/workerd/api/node/tests/net-nodejs-tcp-server.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@
77
// We execute this command using Node.js, which makes net.createServer available.
88
const net = require('node:net');
99

10+
function reportPort(server) {
11+
console.info(`Listening on port ${server.address().port}`);
12+
}
13+
1014
const server = net.createServer((s) => {
1115
s.on('error', () => {
1216
// Do nothing
1317
});
1418
s.end();
1519
});
16-
server.listen(9999, () => console.info('Listening on port 9999'));
20+
server.listen(process.env.SERVER_PORT, () => reportPort(server));
1721

1822
const echoServer = net.createServer((s) => {
1923
s.setTimeout(100);
@@ -22,7 +26,7 @@ const echoServer = net.createServer((s) => {
2226
});
2327
s.pipe(s);
2428
});
25-
echoServer.listen(9998, () => console.info('Listening on port 9998'));
29+
echoServer.listen(process.env.ECHO_SERVER_PORT, () => reportPort(echoServer));
2630

2731
const timeoutServer = net.createServer((s) => {
2832
s.setTimeout(100);
@@ -39,4 +43,6 @@ const timeoutServer = net.createServer((s) => {
3943
// Do nothing
4044
});
4145
});
42-
timeoutServer.listen(9997, () => console.info('Listening on port 9997'));
46+
timeoutServer.listen(process.env.TIMEOUT_SERVER_PORT, () =>
47+
reportPort(timeoutServer)
48+
);

0 commit comments

Comments
 (0)