acab911351
In order to extract the PNG files produced by our CanvasKit gms, we need our JS tests to POST them to a server which can write to disk. The easiest way to do this is to use the test_on_env rule defined in the Skia Infra repo for exactly this purpose. This required https://skia-review.googlesource.com/c/buildbot/+/510717 to be able to configure the binary correctly and https://skia-review.googlesource.com/c/buildbot/+/511862, for nicer debugging so the skia-infra dep was updated via the following commands: $ go get go.skia.org/infra@d8a552a29e $ go mod download $ make -C infra/bots train $ make -C bazel gazelle_update_repo This caused many automated changes to infra/bots/tasks.json The flow is: 1. User types bazelisk test :hello_world_test_with_env 2. The test_on_env rule starts gold_test_env and waits for the file defined in $ENV_READY_FILE to be created. 3. gold_test_env starts a web server on a random port. It writes this port number to $ENV_DIR/port. Then, it creates $ENV_READY_FILE to signal ready. 4. test_on_env sees the ready file and then starts the karma_test rule. (Reminder: this is a bash script which starts karma using the Bazel-bundled chromium). 5. The karma_test rule runs the karma.bazel.js file (which has been injected with some JS code to fill in Bazel paths and settings) using Bazel-bundled node. This reads in the port file and sets up a Karma proxy to redirect /gold_rpc/report to http://localhost:PORT/report 6. The JS tests run via Karma (and do assertions via Jasmine). Some tests, the gms, make POST requests to the proxy. 7. gold_test_env gets these POST requests writes the images to a special Bazel folder on disk as defined by $TEST_UNDECLARED_OUTPUTS_DIR. 8. test_on_env identifies that the tests finish (because the karma_test script returns 0). It sends SIGINT to gold_test_env. 9. gold_test_env stops the webserver. The special Bazel folder will zip up anything inside it and make it available for future rules (e.g. a rule that will upload to Gold via goldctl). Suggested Review Order: - bazel/karma_test.bzl to see the test_on_env rule bundled into the karma_test macro. I chose to put it there because it might be confusing to have to define both a karma_test and test_on_env rule in the same package but not be able to call one because it will fail to talk to the server. - gold_test_env.go to see how the appropriate files are written to signal the environment is ready and the handlers are set up. - karma.bazel.js to see how we make our own proxy given the port from the env binary. The fact that we could not create our own proxy with the existing karma_test rule was why the chain ending in https://skia-review.googlesource.com/c/skia/+/508797 had to be abandoned. - tests/*.js to see how the environment is probed via /healthz and then used to make POST requests with data. - Everything else. Change-Id: I32a90def41796ca94cf187d640cfff8e262f85f6 BUG: skia:12541 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/510737 Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
342 lines
14 KiB
Python
342 lines
14 KiB
Python
"""This module defines rules for running JS tests in a browser."""
|
|
|
|
load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "node_modules_aspect")
|
|
|
|
# https://github.com/bazelbuild/rules_webtesting/blob/master/web/web.bzl
|
|
load("@io_bazel_rules_webtesting//web:web.bzl", "web_test")
|
|
|
|
# https://github.com/google/skia-buildbot/blob/main/bazel/test_on_env/test_on_env.bzl
|
|
load("@org_skia_go_infra//bazel/test_on_env:test_on_env.bzl", "test_on_env")
|
|
|
|
def karma_test(name, config_file, srcs, static_files = None, env = None, **kwargs):
|
|
"""Tests the given JS files using Karma and a browser provided by Bazel (Chromium)
|
|
|
|
This rule injects some JS code into the karma config file and produces both that modified
|
|
configuration file and a bash script which invokes Karma. That script is then invoked
|
|
in an environment that has the Bazel-downloaded browser available and the tests run using it.
|
|
|
|
When invoked via `bazel test`, the test runs in headless mode. When invoked via `bazel run`,
|
|
a visible web browser appears for the user to inspect and debug.
|
|
|
|
This draws inspiration from the karma_web_test implementation in concatjs
|
|
https://github.com/bazelbuild/rules_nodejs/blob/700b7a3c5f97f2877320e6e699892ee706f85269/packages/concatjs/web_test/karma_web_test.bzl
|
|
but we were unable to use it because they prevented us from defining some proxies ourselves,
|
|
which we need in order to communicate our test gms (PNG files) to a server that runs alongside
|
|
the test. This implementation is simpler than concatjs's and does not try to work for all
|
|
situations nor bundle everything together.
|
|
|
|
Args:
|
|
name: The name of the rule which actually runs the tests. generated dependent rules will use
|
|
this name plus an applicable suffix.
|
|
config_file: A karma config file. The user is to expect a function called BAZEL_APPLY_SETTINGS
|
|
is defined and should call it with the configuration object before passing it to config.set.
|
|
srcs: A list of JavaScript test files or helpers.
|
|
static_files: Arbitrary files which are available to be loaded.
|
|
Files are served at:
|
|
- `/static/<WORKSPACE_NAME>/<path-to-file>` or
|
|
- `/static/<WORKSPACE_NAME>/<path-to-rule>/<file>`
|
|
Examples:
|
|
- `/static/skia/modules/canvaskit/tests/assets/color_wheel.gif`
|
|
- `/static/skia/modules/canvaskit/canvaskit_wasm/canvaskit.wasm`
|
|
env: An optional label to a binary. If set, the test will be wrapped in a test_on_env rule,
|
|
and this binary will be used as the "env" part of test_on_env. It will be started before
|
|
the tests run and be running in parallel to them. See the test_on_env.bzl in the
|
|
Skia Infra repo for more.
|
|
**kwargs: Additional arguments are passed to @io_bazel_rules_webtesting/web_test.
|
|
"""
|
|
if len(srcs) == 0:
|
|
fail("Must pass at least one file into srcs or there will be no tests to run")
|
|
if not static_files:
|
|
static_files = []
|
|
|
|
karma_test_name = name + "_karma_test"
|
|
_karma_test(
|
|
name = karma_test_name,
|
|
srcs = srcs,
|
|
deps = [
|
|
"@npm//karma-chrome-launcher",
|
|
"@npm//karma-firefox-launcher",
|
|
"@npm//karma-jasmine",
|
|
"@npm//jasmine-core",
|
|
],
|
|
config_file = config_file,
|
|
static_files = static_files,
|
|
visibility = ["//visibility:private"],
|
|
tags = ["manual"],
|
|
)
|
|
|
|
# See the following link for the options.
|
|
# https://github.com/bazelbuild/rules_webtesting/blob/e9cf17123068b1123c68219edf9b274bf057b9cc/web/internal/web_test.bzl#L164
|
|
# TODO(kjlubick) consider using web_test_suite to test on Firefox as well.
|
|
if not env:
|
|
web_test(
|
|
name = name,
|
|
launcher = ":" + karma_test_name,
|
|
browser = "@io_bazel_rules_webtesting//browsers:chromium-local",
|
|
test = karma_test_name,
|
|
**kwargs
|
|
)
|
|
else:
|
|
web_test_name = name + "_web_test"
|
|
web_test(
|
|
name = web_test_name,
|
|
launcher = ":" + karma_test_name,
|
|
browser = "@io_bazel_rules_webtesting//browsers:chromium-local",
|
|
test = karma_test_name,
|
|
visibility = ["//visibility:private"],
|
|
**kwargs
|
|
)
|
|
test_on_env(
|
|
name = name,
|
|
env = env,
|
|
test = ":" + web_test_name,
|
|
test_on_env_binary = "@org_skia_go_infra//bazel/test_on_env:test_on_env",
|
|
)
|
|
|
|
# This JS code is injected into the the provided karma configuration file. It contains
|
|
# Bazel-specific logic that could be re-used across different configuration files.
|
|
# Concretely, it sets up the browser configuration and whether we want to just run the tests
|
|
# and exit (e.g. the user ran `bazel test foo`) or if we want to have an interactive session
|
|
# (e.g. the user ran `bazel run foo`).
|
|
_apply_bazel_settings_js_code = """
|
|
(function(cfg) {
|
|
// This is is a JS function provided via environment variables to let us resolve files
|
|
// https://bazelbuild.github.io/rules_nodejs/Built-ins.html#nodejs_binary-templated_args
|
|
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']);
|
|
|
|
// Apply the paths to any files that are coming from other Bazel rules (e.g. compiled JS).
|
|
function addFilePaths(cfg) {
|
|
if (!cfg.files) {
|
|
cfg.files = [];
|
|
}
|
|
cfg.files = cfg.files.concat([_BAZEL_SRCS]);
|
|
cfg.basePath = "_BAZEL_BASE_PATH";
|
|
|
|
if (!cfg.proxies) {
|
|
cfg.proxies = {};
|
|
}
|
|
// The following is based off of the concatjs version
|
|
// https://github.com/bazelbuild/rules_nodejs/blob/700b7a3c5f97f2877320e6e699892ee706f85269/packages/concatjs/web_test/karma.conf.js#L276
|
|
const staticFiles = [_BAZEL_STATIC_FILES];
|
|
for (const file of staticFiles) {
|
|
// We need to find the actual path (symlinks can apparently cause issues on Windows).
|
|
const resolvedFile = runfiles.resolve(file);
|
|
cfg.files.push({pattern: resolvedFile, included: false});
|
|
// We want the file to be available on a path according to its location in the workspace
|
|
// (and not the path on disk), so we use a proxy to redirect.
|
|
// Prefixing the proxy path with '/absolute' allows karma to load files that are not
|
|
// underneath the basePath. This doesn't see to be an official API.
|
|
// https://github.com/karma-runner/karma/issues/2703
|
|
cfg.proxies['/static/' + file] = '/absolute' + resolvedFile;
|
|
}
|
|
}
|
|
|
|
// Returns true if invoked with bazel run, i.e. the user wants to see the results on a real
|
|
// browser.
|
|
function isBazelRun() {
|
|
// This env var seems to be a good indicator on Linux, at least.
|
|
return !!process.env['DISPLAY'];
|
|
}
|
|
|
|
// Configures the settings to run chrome.
|
|
function applyChromiumSettings(cfg, chromiumPath) {
|
|
if (isBazelRun()) {
|
|
cfg.browsers = ['Chrome'];
|
|
cfg.singleRun = false;
|
|
} else {
|
|
// Invoked via bazel test, so run the tests once in a headless browser and be done
|
|
cfg.browsers = ['ChromeHeadless'];
|
|
cfg.singleRun = true;
|
|
}
|
|
|
|
try {
|
|
// Setting the CHROME_BIN environment variable tells Karma which chrome to use.
|
|
// We want it to use the Chrome brought via Bazel.
|
|
process.env.CHROME_BIN = runfiles.resolve(chromiumPath);
|
|
} catch {
|
|
throw new Error(`Failed to resolve Chromium binary '${chromiumPath}' in runfiles`);
|
|
}
|
|
}
|
|
|
|
function applyBazelSettings(cfg) {
|
|
addFilePaths(cfg)
|
|
|
|
// This is a JSON file that contains this metadata, mixed in with some other data, e.g.
|
|
// the link to the correct executable for the given platform.
|
|
// https://github.com/bazelbuild/rules_webtesting/blob/e9cf17123068b1123c68219edf9b274bf057b9cc/browsers/chromium-local.json
|
|
const webTestMetadata = require(runfiles.resolve(process.env['WEB_TEST_METADATA']));
|
|
|
|
const webTestFiles = webTestMetadata['webTestFiles'][0];
|
|
const path = webTestFiles['namedFiles']['CHROMIUM'];
|
|
if (path) {
|
|
applyChromiumSettings(cfg, path);
|
|
} else {
|
|
throw new Error("not supported yet");
|
|
}
|
|
}
|
|
|
|
applyBazelSettings(cfg)
|
|
|
|
// The user is expected to treat the BAZEL_APPLY_SETTINGS as a function name and pass in
|
|
// the configuration as a parameter. Thus, we need to end such that our IIFE will be followed
|
|
// by the parameter in parentheses and get passed in as cfg.
|
|
})"""
|
|
|
|
def _expand_templates_in_karma_config(ctx):
|
|
# Wrap the absolute paths of our files in quotes and make them comma seperated so they
|
|
# can go in the Karma files list.
|
|
srcs = ['"{}"'.format(_absolute_path(ctx, f)) for f in ctx.files.srcs]
|
|
src_list = ", ".join(srcs)
|
|
|
|
# Set our base path to that which contains the karma configuration file.
|
|
# This requires going up a few directory segments. This allows our absolute paths to
|
|
# all be compatible with each other.
|
|
config_segments = len(ctx.outputs.configuration.short_path.split("/"))
|
|
base_path = "/".join([".."] * config_segments)
|
|
|
|
static_files = ['"{}"'.format(_absolute_path(ctx, f)) for f in ctx.files.static_files]
|
|
static_list = ", ".join(static_files)
|
|
|
|
# Replace the placeholders in the embedded JS with those files. We cannot use .format() because
|
|
# the curly braces from the JS code throw it off.
|
|
apply_bazel_settings = _apply_bazel_settings_js_code.replace("_BAZEL_SRCS", src_list)
|
|
apply_bazel_settings = apply_bazel_settings.replace("_BAZEL_BASE_PATH", base_path)
|
|
apply_bazel_settings = apply_bazel_settings.replace("_BAZEL_STATIC_FILES", static_list)
|
|
|
|
# Add in the JS fragment that applies the Bazel-specific settings to the provided config.
|
|
# https://docs.bazel.build/versions/main/skylark/lib/actions.html#expand_template
|
|
ctx.actions.expand_template(
|
|
output = ctx.outputs.configuration,
|
|
template = ctx.file.config_file,
|
|
substitutions = {
|
|
"BAZEL_APPLY_SETTINGS": apply_bazel_settings,
|
|
},
|
|
)
|
|
|
|
def _absolute_path(ctx, file):
|
|
# Referencing things in @npm yields a short_path that starts with ../
|
|
# For those cases, we can just remove the ../
|
|
if file.short_path.startswith("../"):
|
|
return file.short_path[3:]
|
|
|
|
# Otherwise, we have a local file, so we need to include the workspace path to make it
|
|
# an absolute path
|
|
return ctx.workspace_name + "/" + file.short_path
|
|
|
|
_invoke_karma_bash_script = """#!/usr/bin/env bash
|
|
# --- begin runfiles.bash initialization v2 ---
|
|
# Copy-pasted from the Bazel Bash runfiles library v2.
|
|
# https://github.com/bazelbuild/bazel/blob/master/tools/bash/runfiles/runfiles.bash
|
|
set -uo pipefail; f=build_bazel_rules_nodejs/third_party/github.com/bazelbuild/bazel/tools/bash/runfiles/runfiles.bash
|
|
source "${{RUNFILES_DIR:-/dev/null}}/$f" 2>/dev/null || \
|
|
source "$(grep -sm1 "^$f " "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" | cut -f2- -d' ')" 2>/dev/null || \
|
|
source "$0.runfiles/$f" 2>/dev/null || \
|
|
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
|
|
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
|
|
{{ echo>&2 "ERROR: cannot find $f"; exit 1; }}; f=; set -e
|
|
# --- end runfiles.bash initialization v2 ---
|
|
|
|
readonly KARMA=$(rlocation "{_KARMA_EXECUTABLE_SCRIPT}")
|
|
readonly CONF=$(rlocation "{_KARMA_CONFIGURATION_FILE}")
|
|
|
|
# set a temporary directory as the home directory, because otherwise Chrome fails to
|
|
# start up, complaining about a read-only file system. This does not get cleaned up automatically
|
|
# by Bazel, so we do so after Karma finishes.
|
|
export HOME=$(mktemp -d)
|
|
|
|
readonly COMMAND="${{KARMA}} "start" ${{CONF}}"
|
|
${{COMMAND}}
|
|
KARMA_EXIT_CODE=$?
|
|
echo "Karma returned ${{KARMA_EXIT_CODE}}"
|
|
# Attempt to clean up the temporary home directory. If this fails, that's not a big deal because
|
|
# the contents are small and will be cleaned up by the OS on reboot.
|
|
rm -rf $HOME || true
|
|
exit $KARMA_EXIT_CODE
|
|
"""
|
|
|
|
def _create_bash_script_to_invoke_karma(ctx):
|
|
ctx.actions.write(
|
|
output = ctx.outputs.executable,
|
|
is_executable = True,
|
|
content = _invoke_karma_bash_script.format(
|
|
_KARMA_EXECUTABLE_SCRIPT = _absolute_path(ctx, ctx.executable.karma),
|
|
_KARMA_CONFIGURATION_FILE = _absolute_path(ctx, ctx.outputs.configuration),
|
|
),
|
|
)
|
|
|
|
def _karma_test_impl(ctx):
|
|
_expand_templates_in_karma_config(ctx)
|
|
_create_bash_script_to_invoke_karma(ctx)
|
|
|
|
# The files that need to be included when we run the bash script that invokes Karma are:
|
|
# - The templated configuration file
|
|
# - Any JS test files the user provided
|
|
# - Any static files the user specified
|
|
# - The other dependencies from npm (e.g. jasmine-core)
|
|
runfiles = [
|
|
ctx.outputs.configuration,
|
|
]
|
|
runfiles += ctx.files.srcs
|
|
runfiles += ctx.files.static_files
|
|
runfiles += ctx.files.deps
|
|
|
|
# We need to add the sources for our Karma dependencies as transitive dependencies, otherwise
|
|
# things like the karma-chrome-launcher will not be available for Karma to load.
|
|
# https://docs.bazel.build/versions/main/skylark/lib/depset.html
|
|
node_modules_depsets = []
|
|
for dep in ctx.attr.deps:
|
|
if ExternalNpmPackageInfo in dep:
|
|
node_modules_depsets.append(dep[ExternalNpmPackageInfo].sources)
|
|
else:
|
|
fail("Not an external npm file: " + dep)
|
|
node_modules = depset(transitive = node_modules_depsets)
|
|
|
|
# https://docs.bazel.build/versions/main/skylark/lib/DefaultInfo.html
|
|
return [DefaultInfo(
|
|
runfiles = ctx.runfiles(
|
|
files = runfiles,
|
|
transitive_files = node_modules,
|
|
).merge(ctx.attr.karma[DefaultInfo].data_runfiles),
|
|
executable = ctx.outputs.executable,
|
|
)]
|
|
|
|
_karma_test = rule(
|
|
implementation = _karma_test_impl,
|
|
test = True,
|
|
executable = True,
|
|
attrs = {
|
|
"config_file": attr.label(
|
|
doc = "The karma config file",
|
|
mandatory = True,
|
|
allow_single_file = [".js"],
|
|
),
|
|
"srcs": attr.label_list(
|
|
doc = "A list of JavaScript test files",
|
|
allow_files = [".js"],
|
|
mandatory = True,
|
|
),
|
|
"deps": attr.label_list(
|
|
doc = """Any karma plugins (aka peer deps) required. These are generally listed
|
|
in the provided config_file""",
|
|
allow_files = True,
|
|
aspects = [node_modules_aspect],
|
|
mandatory = True,
|
|
),
|
|
"karma": attr.label(
|
|
doc = "karma binary label",
|
|
# By default, we use the karma pulled in via Bazel running npm install
|
|
default = "@npm//karma/bin:karma",
|
|
executable = True,
|
|
cfg = "exec",
|
|
allow_files = True,
|
|
),
|
|
"static_files": attr.label_list(
|
|
doc = "Additional files which are available to be loaded",
|
|
allow_files = True,
|
|
),
|
|
},
|
|
outputs = {
|
|
"configuration": "%{name}.conf.js",
|
|
},
|
|
)
|