[bazel] Make custom karma_test rule
Run the tests in headless mode and output the logs bazel test :hello_world --test_output=all Start up a visible web browser with the karma test driver (need to go to Debug tab to actually run tests) bazel run :hello_world Suggested review order - package.json to see the karma dependencies to run jasmine tests on chrome and firefox. - WORKSPACE.bazel to see how the packages listed in package.json and package-lock.json are downloaded into the Bazel sandbox/cache via the npm_install rule. As mentioned in the package.json comment, the version of build_bazel_rules_nodejs which emscripten uses [1] is 4.4.1 and if we tried to install it ourselves, that installation will be ignored. We also bring in hermetic browsers via io_bazel_rules_webtesting. - bazel/karma_test.bzl which defines a new rule _karma_test and a macro karma_test which joins the new rule with an existing web_test rule to run it on a hermetic browser which Bazel downloads. This rule takes heavy inspiration from @bazel/concatjs [2], but is much simpler and lets us configure more things (e.g. proxies, so we can work with test_on_env). - karma.bazel.js, which is a pretty ordinary looking karma configuration file [2] with effectively a JS macro BAZEL_APPLY_SETTINGS. JS doesn't have a preprocessor or actual macros, but this string will be replaced by the JS code in karma_test.bzl which will set correct filepaths for Bazel content. - All other files. [1]c33c7be17f/bazel/deps.bzl (L10)
[2]700b7a3c5f/packages/concatjs/web_test/karma_web_test.bzl (L318)
[3] http://karma-runner.github.io/6.3/config/configuration-file.html Change-Id: Id64c0a86d6be37d627762cef0beaaf23ad390ac1 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/509717 Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
This commit is contained in:
parent
4ae863ba8a
commit
a2d3958e1f
@ -16,6 +16,8 @@ http_archive(
|
||||
|
||||
load("@emsdk//:deps.bzl", emsdk_deps = "deps")
|
||||
|
||||
# One of the deps here is build_bazel_rules_nodejs, currently version 4.4.1
|
||||
# If we try to install it ourselves after this, it won't work.
|
||||
emsdk_deps()
|
||||
|
||||
load("@emsdk//:emscripten_deps.bzl", emsdk_emscripten_deps = "emscripten_deps")
|
||||
@ -82,3 +84,38 @@ go_rules_dependencies()
|
||||
go_register_toolchains(version = "1.17.2")
|
||||
|
||||
gazelle_dependencies(go_repository_default_config = "//:WORKSPACE.bazel")
|
||||
|
||||
###################################################
|
||||
# JavaScript / TypeScript rules and dependencies. #
|
||||
###################################################
|
||||
|
||||
# The npm_install rule runs anytime the package.json or package-lock.json file changes. It also
|
||||
# extracts any Bazel rules distributed in an npm package.
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
|
||||
|
||||
# Manages the node_modules directory.
|
||||
npm_install(
|
||||
name = "npm",
|
||||
package_json = "//:package.json",
|
||||
package_lock_json = "//:package-lock.json",
|
||||
)
|
||||
|
||||
# io_bazel_rules_webtesting allows us to download browsers in a hermetic, repeatable way. This
|
||||
# currently includes Chromium and Firefox. Note that the version on this does not necessarily
|
||||
# match the version below of the browsers-X.Y.Z below that is available.
|
||||
http_archive(
|
||||
name = "io_bazel_rules_webtesting",
|
||||
sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
|
||||
urls = [
|
||||
"https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz",
|
||||
"https://storage.googleapis.com/skia-world-readable/bazel/e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a.tar.gz",
|
||||
],
|
||||
)
|
||||
|
||||
# https://github.com/bazelbuild/rules_webtesting/blob/e9cf17123068b1123c68219edf9b274bf057b9cc/web/versioned/browsers-0.3.3.bzl
|
||||
load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl", "browser_repositories")
|
||||
|
||||
browser_repositories(
|
||||
chromium = True,
|
||||
firefox = True,
|
||||
)
|
||||
|
@ -66,6 +66,7 @@ func main() {
|
||||
if err := processOne(workDir, *url, *sha256Hash); err != nil {
|
||||
fatalf("Error while processing entry: %s", err)
|
||||
}
|
||||
fmt.Printf("https://storage.googleapis.com/skia-world-readable/bazel/%s.tar.gz\n", *sha256Hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
270
bazel/karma_test.bzl
Normal file
270
bazel/karma_test.bzl
Normal file
@ -0,0 +1,270 @@
|
||||
# https://github.com/bazelbuild/rules_webtesting/blob/master/web/web.bzl
|
||||
load("@io_bazel_rules_webtesting//web:web.bzl", "web_test")
|
||||
load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "node_modules_aspect")
|
||||
|
||||
def karma_test(name, srcs, config_file, **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:
|
||||
srcs: A list of JavaScript test files or helpers.
|
||||
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.
|
||||
"""
|
||||
if len(srcs) == 0:
|
||||
fail("Must pass at least one file into srcs or there will be no tests to run")
|
||||
|
||||
wrapped_test_name = name + "_karma_test"
|
||||
_karma_test(
|
||||
name = wrapped_test_name,
|
||||
srcs = srcs,
|
||||
deps = [
|
||||
"@npm//karma-chrome-launcher",
|
||||
"@npm//karma-firefox-launcher",
|
||||
"@npm//karma-jasmine",
|
||||
"@npm//jasmine-core",
|
||||
],
|
||||
config_file = config_file,
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
# 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.
|
||||
web_test(
|
||||
name = name,
|
||||
launcher = ":" + wrapped_test_name,
|
||||
browser = "@io_bazel_rules_webtesting//browsers:chromium-local",
|
||||
test = wrapped_test_name,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# 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) {
|
||||
// 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";
|
||||
}
|
||||
|
||||
// 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, runfiles, 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 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']);
|
||||
|
||||
// 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, runfiles, 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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.
|
||||
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
|
||||
# - The other dependencies from npm (e.g. jasmine-core)
|
||||
runfiles = [
|
||||
ctx.outputs.configuration,
|
||||
]
|
||||
runfiles += ctx.files.srcs
|
||||
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:
|
||||
print("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,
|
||||
),
|
||||
},
|
||||
outputs = {
|
||||
"configuration": "%{name}.conf.js",
|
||||
},
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
load("@emsdk//emscripten_toolchain:wasm_rules.bzl", "wasm_cc_binary")
|
||||
load("//bazel/common_config_settings:defs.bzl", "bool_flag")
|
||||
load("//bazel:cc_binary_with_flags.bzl", "cc_binary_with_flags")
|
||||
load("//bazel:karma_test.bzl", "karma_test")
|
||||
|
||||
package(default_visibility = ["//:__subpackages__"])
|
||||
|
||||
@ -429,3 +430,11 @@ bool_flag(
|
||||
default = True,
|
||||
flag_name = "include_matrix_js",
|
||||
)
|
||||
|
||||
karma_test(
|
||||
name = "hello_world",
|
||||
srcs = [
|
||||
"tests/hello_world.js",
|
||||
],
|
||||
config_file = "karma.bazel.js",
|
||||
)
|
||||
|
29
modules/canvaskit/karma.bazel.js
Normal file
29
modules/canvaskit/karma.bazel.js
Normal file
@ -0,0 +1,29 @@
|
||||
module.exports = function(config) {
|
||||
// http://karma-runner.github.io/6.3/config/configuration-file.html
|
||||
let cfg = {
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['progress'],
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
browserDisconnectTimeout: 20000,
|
||||
browserNoActivityTimeout: 20000,
|
||||
|
||||
// How many browsers should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
};
|
||||
|
||||
// Bazel will inject some code here to add/change the following items:
|
||||
// - files
|
||||
// - proxies
|
||||
// - browsers
|
||||
// - basePath
|
||||
// - singleRun
|
||||
BAZEL_APPLY_SETTINGS(cfg)
|
||||
|
||||
config.set(cfg);
|
||||
};
|
8
modules/canvaskit/tests/hello_world.js
Normal file
8
modules/canvaskit/tests/hello_world.js
Normal file
@ -0,0 +1,8 @@
|
||||
describe('The test harness', () => {
|
||||
it('runs the first test', () => {
|
||||
expect(2+3).toBe(5);
|
||||
});
|
||||
it('runs the second test', () => {
|
||||
expect(null).toBeFalsy();
|
||||
});
|
||||
})
|
3603
package-lock.json
generated
Normal file
3603
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
package.json
Normal file
13
package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"//": [
|
||||
"These versions of these were the latest version avaiable at the time of writing",
|
||||
"with the exception of jasmine, which was version 3 because our tests currently use v3."
|
||||
],
|
||||
"devDependencies": {
|
||||
"jasmine-core": "3.10.1",
|
||||
"karma": "6.3.15",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-jasmine": "4.0.1"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user