File
Blob: build/wd_test.bzl
| 1 | load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project") |
| 2 | load("@workerd//:build/lint_test.bzl", "lint_test") |
| 3 | |
| 4 | def 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 | |
| 156 | WINDOWS_TEMPLATE = """ |
| 157 | @echo off |
| 158 | setlocal EnableDelayedExpansion |
| 159 | |
| 160 | REM Run supervisor to start sidecar if specified |
| 161 | if 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 | |
| 171 | set TEST_EXIT=!ERRORLEVEL! |
| 172 | exit /b !TEST_EXIT! |
| 173 | """ |
| 174 | |
| 175 | SH_TEMPLATE = """#!/bin/bash |
| 176 | set -euo pipefail |
| 177 | |
| 178 | # Set up coverage for workerd subprocess |
| 179 | if [ -n "${{COVERAGE_DIR:-}}" ]; then |
| 180 | export LLVM_PROFILE_FILE="${{COVERAGE_DIR}}/%p-%m.profraw" |
| 181 | export KJ_CLEAN_SHUTDOWN=1 |
| 182 | fi |
| 183 | |
| 184 | # Run supervisor to start sidecar if specified |
| 185 | if [ ! -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} |
| 188 | else |
| 189 | {runtest} |
| 190 | fi |
| 191 | """ |
| 192 | |
| 193 | WINDOWS_RUNTEST_NORMAL = """powershell -Command \"%*\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR""" |
| 194 | |
| 195 | SH_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 | |
| 208 | WINDOWS_RUNTEST_SNAPSHOT = """ |
| 209 | $PYTHON_SAVE_SNAPSHOT_OPTIONS = $ENV:PYTHON_SAVE_SNAPSHOT_ARGS -split ' ' |
| 210 | powershell -Command \"%* --python-save-snapshot @PYTHON_SAVE_SNAPSHOT_OPTIONS\" `-dTEST_TMPDIR=$ENV:TEST_TMPDIR |
| 211 | set TEST_EXIT=!ERRORLEVEL! |
| 212 | if !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 | |
| 218 | SH_RUNTEST_SNAPSHOT = """ |
| 219 | echo Creating Python Snapshot |
| 220 | "$@" -dTEST_TMPDIR=$TEST_TMPDIR --python-save-snapshot $PYTHON_SAVE_SNAPSHOT_ARGS |
| 221 | echo "" |
| 222 | echo Using Python Snapshot |
| 223 | "$@" -dTEST_TMPDIR=$TEST_TMPDIR --python-load-snapshot snapshot.bin |
| 224 | """ |
| 225 | |
| 226 | def _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 | ) |