Skip to content
File

Blob: build/wd_test.bzl

16.1 KB
1load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
2load("@workerd//:build/lint_test.bzl", "lint_test")
3 
4def wd_test(
5 src,
6 data = [],
7 name = None,
8 args = [],
9 ts_deps = [],
10 lint = True,
11 python_snapshot_test = False,
12 generate_default_variant = True,
13 generate_all_autogates_variant = True,
14 generate_all_compat_flags_variant = True,
15 generate_gc_stress_variant = True,
16 predictable = True,
17 compat_date = "",
18 **kwargs):
19 """Rule to define tests that run `workerd test` with a particular config.
20 
21 Args:
22 src: A .capnp config file defining the test. (`name` will be derived from this if not
23 specified.) The extension `.wd-test` is also permitted instead of `.capnp`, in order to
24 avoid confusing other build systems that may assume a `.capnp` file should be compiled.
25 data: Additional files which the .capnp config file may embed. All TypeScript files will be compiled,
26 their resulting files will be passed to the test as well. Usually TypeScript or Javascript source files.
27 args: Additional arguments to pass to `workerd`. Typically used to pass `--experimental`.
28 generate_default_variant: If True (default), generate the default variant with oldest compat date.
29 generate_all_autogates_variant: If True (default), generate @all-autogates variants.
30 generate_all_compat_flags_variant: If True (default), generate @all-compat-flags variants.
31 generate_gc_stress_variant: If True (default), generate @gc-stress variant.
32 predictable: If True (default), pass `--predictable` to workerd.
33 compat_date: If specified, use this compat date for the default variant instead of 2000-01-01.
34 Does not affect the @all-compat-flags variant which always uses 2999-12-31.
35 
36 The following test variants are generated based on the flags:
37 - name@ (if generate_default_variant): oldest compat date (2000-01-01)
38 - name@all-compat-flags (if generate_all_compat_flags_variant): newest compat date (2999-12-31)
39 - name@all-autogates (if generate_all_autogates_variant): all autogates + oldest compat date
40 - name@gc-stress (if generate_gc_stress_variant): forced GC at every continuation
41 """
42 
43 # Add workerd binary to "data" dependencies.
44 data = data + [src, "//src/workerd/server:workerd_cross"]
45 
46 ts_srcs = [src for src in data if src.endswith(".ts")]
47 
48 # Default name based on src.
49 if name == None:
50 name = src.removesuffix(".capnp").removesuffix(".wd-test").removesuffix(".ts-wd-test")
51 
52 if len(ts_srcs) != 0:
53 # NOTE: We intentionally do not use isolated_typecheck here. While isolated_typecheck can
54 # improve build parallelism by separating transpilation from type-checking, it requires
55 # isolatedDeclarations in tsconfig (which mandates explicit return type annotations on all
56 # exports) and uses --noResolve during transpilation. The --noResolve flag prevents
57 # TypeScript from finding @types/node, breaking IDE support for Node.js imports like
58 # 'node:assert'. Since wd_test TypeScript files are typically standalone test files (leaf
59 # nodes in the dependency graph), the parallelization benefits would be minimal anyway.
60 ts_config(
61 name = name + "@ts_config",
62 src = "tsconfig.json",
63 deps = ["@workerd//tools:base-tsconfig"],
64 )
65 ts_project(
66 name = name + "@ts_project",
67 srcs = ts_srcs,
68 tsconfig = ":" + name + "@ts_config",
69 deps = ["//src/node:node@tsproject"] + ts_deps,
70 )
71 data += [js_src.removesuffix(".ts") + ".js" for js_src in ts_srcs]
72 
73 if lint:
74 lint_srcs = [s for s in data if (s.endswith(".ts") or s.endswith(".mts") or s.endswith(".js") or s.endswith(".mjs")) and not s.startswith("../") and not s.startswith("//")]
75 if lint_srcs:
76 lint_test(
77 name = name,
78 eslintrc_json = "@workerd//tools:base.eslint.config.mjs",
79 tsconfig_json = "@workerd//tools:base.tsconfig.json",
80 srcs = lint_srcs,
81 data = ["tsconfig.json"] if len(ts_srcs) > 0 else [],
82 no_copy_to_bin = [
83 "@workerd//tools:base.eslint.config.mjs",
84 "@workerd//tools:base.tsconfig.json",
85 ],
86 )
87 
88 # Add initial arguments for `workerd test` command.
89 base_args = [
90 "$(location //src/workerd/server:workerd_cross)",
91 "test",
92 ] + (["--predictable"] if predictable else []) + [
93 "$(location {})".format(src),
94 ] + args
95 
96 # Define the compat-date args for each variant
97 # Note: dates must be in range [2000-01-01, 2999-12-31] due to parsing constraints
98 default_compat_args = ["--compat-date=2000-01-01"]
99 newest_compat_args = ["--compat-date=2999-12-31"]
100 
101 if compat_date:
102 default_compat_args = ["--compat-date={}".format(compat_date)]
103 
104 # Generate variants based on the flags
105 # Default variant: oldest compat date
106 if generate_default_variant:
107 _wd_test(
108 src = src,
109 name = name + "@",
110 data = data,
111 args = base_args + default_compat_args,
112 python_snapshot_test = python_snapshot_test,
113 **kwargs
114 )
115 
116 # All compat flags variant: newest compat date
117 if generate_all_compat_flags_variant:
118 _wd_test(
119 src = src,
120 name = name + "@all-compat-flags",
121 data = data,
122 args = base_args + newest_compat_args,
123 python_snapshot_test = python_snapshot_test,
124 **kwargs
125 )
126 
127 # All autogates variant: all autogates + oldest compat date
128 if generate_all_autogates_variant:
129 _wd_test(
130 src = src,
131 name = name + "@all-autogates",
132 data = data,
133 args = base_args + default_compat_args + ["--all-autogates"],
134 python_snapshot_test = python_snapshot_test,
135 **kwargs
136 )
137 
138 # GC stress variant: forced full GC at every awaitIo continuation.
139 # Uses newest compat date unless the test opted out of @all-compat-flags (meaning
140 # it is compat-date-sensitive), in which case it uses the default compat date.
141 # Tagged off-by-default so it only runs when explicitly requested.
142 if generate_gc_stress_variant:
143 gc_stress_compat = newest_compat_args if generate_all_compat_flags_variant else default_compat_args
144 gc_stress_kwargs = dict(kwargs)
145 gc_stress_tags = gc_stress_kwargs.pop("tags", []) + ["gc-stress", "no-coverage", "off-by-default"]
146 _wd_test(
147 src = src,
148 name = name + "@gc-stress",
149 data = data,
150 args = base_args + gc_stress_compat + ["--gc-stress"],
151 python_snapshot_test = python_snapshot_test,
152 tags = gc_stress_tags,
153 **gc_stress_kwargs
154 )
155 
156WINDOWS_TEMPLATE = """
157@echo off
158setlocal EnableDelayedExpansion
159 
160REM Run supervisor to start sidecar if specified
161if not "{sidecar}" == "" (
162 REM These environment variables are processed by the supervisor executable
163 set PORTS_TO_ASSIGN={port_bindings}
164 set RANDOMIZE_IP={randomize_ip}
165 set SIDECAR_COMMAND="{sidecar}"
166 powershell -Command \"{supervisor} {runtest}\"
167) else (
168 {runtest}
169)
170 
171set TEST_EXIT=!ERRORLEVEL!
172exit /b !TEST_EXIT!
173"""
174 
175SH_TEMPLATE = """#!/bin/bash
176set -euo pipefail
177 
178# Set up coverage for workerd subprocess
179if [ -n "${{COVERAGE_DIR:-}}" ]; then
180 export LLVM_PROFILE_FILE="${{COVERAGE_DIR}}/%p-%m.profraw"
181 export KJ_CLEAN_SHUTDOWN=1
182fi
183 
184# Run supervisor to start sidecar if specified
185if [ ! -z "{sidecar}" ]; then
186 # These environment variables are processed by the supervisor executable
187 PORTS_TO_ASSIGN={port_bindings} RANDOMIZE_IP={randomize_ip} SIDECAR_COMMAND="{sidecar}" {supervisor} {runtest}
188else
189 {runtest}
190fi
191"""
192 
193WINDOWS_RUNTEST_NORMAL = """powershell -Command \"%*\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR"""
194 
195SH_RUNTEST_NORMAL = """"$@" -dTEST_TMPDIR=$TEST_TMPDIR"""
196 
197# We need variants of the RUN_TEST command for Python memory snapshot tests. We have to invoke
198# workerd twice, once with --python-save-snapshot to produce the snapshot and once with
199# --python-load-snapshot to use it.
200#
201# We would like to implement this in py_wd_test and not have to complicate wd_test for it, but
202# unfortunately bazel provides no way for a test to create a file that is used by another test. So
203# we cannot do this with two separate `wd_test` rules. We _could_ use a build step to create the
204# snapshot, but then a failure at this stage would be reported as a build failure when really it
205# should count as a test failure. So the only option left is to make this modification to wd_test to
206# invoke workerd twice for snapshot tests.
207 
208WINDOWS_RUNTEST_SNAPSHOT = """
209$PYTHON_SAVE_SNAPSHOT_OPTIONS = $ENV:PYTHON_SAVE_SNAPSHOT_ARGS -split ' '
210powershell -Command \"%* --python-save-snapshot @PYTHON_SAVE_SNAPSHOT_OPTIONS\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR
211set TEST_EXIT=!ERRORLEVEL!
212if !TEST_EXIT! EQU 0 (
213 powershell -Command \"%* --python-load-snapshot snapshot.bin\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR
214 set TEST_EXIT=!ERRORLEVEL!
215)
216"""
217 
218SH_RUNTEST_SNAPSHOT = """
219echo Creating Python Snapshot
220"$@" -dTEST_TMPDIR=$TEST_TMPDIR --python-save-snapshot $PYTHON_SAVE_SNAPSHOT_ARGS
221echo ""
222echo Using Python Snapshot
223"$@" -dTEST_TMPDIR=$TEST_TMPDIR --python-load-snapshot snapshot.bin
224"""
225 
226def _wd_test_impl(ctx):
227 is_windows = ctx.target_platform_has_constraint(ctx.attr._platforms_os_windows[platform_common.ConstraintValueInfo])
228 
229 if ctx.file.sidecar and ctx.attr.python_snapshot_test:
230 # TODO(later): Implement support for generating a combined script with these two options
231 # if we have a test that requires it. Not doing it for now due to complexity.
232 return print("sidecar and python_snapshot_test currently cannot be used together")
233 
234 # Bazel insists that the rule must actually create the executable that it intends to run; it
235 # can't just specify some other executable with some args. OK, fine, we'll use a script that
236 # just execs its args.
237 if is_windows:
238 # Batch script executables must end with ".bat"
239 executable = ctx.actions.declare_file("%s_wd_test.bat" % ctx.label.name)
240 template = WINDOWS_TEMPLATE
241 runtest = WINDOWS_RUNTEST_SNAPSHOT if ctx.attr.python_snapshot_test else WINDOWS_RUNTEST_NORMAL
242 else:
243 executable = ctx.outputs.executable
244 template = SH_TEMPLATE
245 runtest = SH_RUNTEST_SNAPSHOT if ctx.attr.python_snapshot_test else SH_RUNTEST_NORMAL
246 
247 content = template.format(
248 sidecar = ctx.file.sidecar.short_path if ctx.file.sidecar else "",
249 runtest = runtest,
250 supervisor = ctx.file.sidecar_supervisor.short_path if ctx.file.sidecar_supervisor else "",
251 port_bindings = ",".join(ctx.attr.sidecar_port_bindings),
252 randomize_ip = "true" if ctx.attr.sidecar_randomize_ip else "false",
253 )
254 
255 ctx.actions.write(
256 output = executable,
257 content = content,
258 is_executable = True,
259 )
260 
261 runfiles = ctx.runfiles(files = ctx.files.data)
262 if ctx.file.sidecar:
263 runfiles = runfiles.merge(ctx.runfiles(files = [ctx.file.sidecar]))
264 
265 # Also merge the sidecar's own runfiles if it has any
266 default_runfiles = ctx.attr.sidecar[DefaultInfo].default_runfiles
267 if default_runfiles:
268 runfiles = runfiles.merge(default_runfiles)
269 
270 runfiles = runfiles.merge(ctx.runfiles(files = [ctx.file.sidecar_supervisor]))
271 
272 # Also merge the supervisor's own runfiles if it has any
273 default_runfiles = ctx.attr.sidecar_supervisor[DefaultInfo].default_runfiles
274 if default_runfiles:
275 runfiles = runfiles.merge(default_runfiles)
276 
277 # IMPORTANT: The workerd binary must be listed in dependency_attributes
278 # to ensure its transitive dependencies (all the C++ source files) are
279 # included in the coverage instrumentation. Without this, coverage data
280 # won't be collected for the actual workerd implementation code.
281 instrumented_files_info = coverage_common.instrumented_files_info(
282 ctx,
283 source_attributes = ["src", "data"],
284 dependency_attributes = ["workerd", "sidecar", "sidecar_supervisor"],
285 )
286 environment = dict(ctx.attr.env)
287 if ctx.attr.python_snapshot_test:
288 environment["PYTHON_SAVE_SNAPSHOT_ARGS"] = ""
289 if ctx.attr.load_snapshot:
290 if ctx.attr.python_snapshot_test:
291 environment["PYTHON_SAVE_SNAPSHOT_ARGS"] = "--python-load-snapshot load_snapshot.bin"
292 f = ctx.attr.load_snapshot.files.to_list()[0]
293 runfiles = runfiles.merge(ctx.runfiles(symlinks = {"load_snapshot.bin": f}))
294 
295 return [
296 DefaultInfo(
297 executable = executable,
298 runfiles = runfiles,
299 ),
300 RunEnvironmentInfo(
301 environment = environment,
302 ),
303 instrumented_files_info,
304 ]
305 
306_wd_test = rule(
307 implementation = _wd_test_impl,
308 test = True,
309 attrs = {
310 # Implicit dependencies used by Bazel to generate coverage reports.
311 "_lcov_merger": attr.label(
312 default = configuration_field(fragment = "coverage", name = "output_generator"),
313 executable = True,
314 cfg = config.exec(exec_group = "test"),
315 ),
316 "_collect_cc_coverage": attr.label(
317 default = "@bazel_tools//tools/test:collect_cc_coverage",
318 executable = True,
319 cfg = config.exec(exec_group = "test"),
320 ),
321 # Source file
322 "src": attr.label(allow_single_file = True),
323 # The workerd executable is used to run all tests.
324 # Using cfg = "target" instead of "exec" ensures workerd is built with coverage
325 # instrumentation when running `bazel coverage`. With cfg = "exec", the binary
326 # would be built in exec configuration which doesn't include coverage flags.
327 "workerd": attr.label(
328 allow_single_file = True,
329 executable = True,
330 cfg = "target",
331 default = "//src/workerd/server:workerd_cross",
332 ),
333 # A list of files that this test requires to be present in order to run.
334 "data": attr.label_list(allow_files = True),
335 # If set, an executable that is run in parallel with the test, and provides some functionality
336 # needed for the test. This is usually a backend server, with workerd serving as the client.
337 # The sidecar will be killed once the test completes.
338 "sidecar": attr.label(
339 allow_single_file = True,
340 executable = True,
341 cfg = "exec",
342 ),
343 # Sidecars
344 # ---------
345 # For detailed documentation, see src/workerd/api/node/tests/sidecar-supervisor.mjs
346 
347 # A list of binding names which will be filled in with random port numbers that the sidecar
348 # and test can use for communication. The test will only begin once the sidecar is
349 # listening to these ports.
350 #
351 # In the sidecar, access these bindings as environment variables. In the wd-test file, add
352 # fromEnvironment bindings to expose them to the test.
353 #
354 # Reminder: you'll also need to add a network = ( allow = ["private"] ) service as well.
355 "sidecar_port_bindings": attr.string_list(),
356 # If true, a random IP address will be assigned to the sidecar process, and provided in the
357 # environment variable SIDECAR_HOSTNAME,
358 "sidecar_randomize_ip": attr.bool(default = True),
359 # An executable that is used to manage port assignments and child process creation when a
360 # sidecar is specified.
361 "sidecar_supervisor": attr.label(
362 allow_single_file = True,
363 executable = True,
364 cfg = "exec",
365 default = "//src/workerd/api/node/tests:sidecar-supervisor",
366 ),
367 "python_snapshot_test": attr.bool(),
368 "load_snapshot": attr.label(allow_single_file = True),
369 # Environment variables to set when running the test
370 "env": attr.string_dict(),
371 # A reference to the Windows platform label, needed for the implementation of wd_test
372 "_platforms_os_windows": attr.label(default = "@platforms//os:windows"),
373 },
374)