Skip to content

Instantly share code, notes, and snippets.

@mattem
Created October 16, 2020 18:30
Show Gist options
  • Save mattem/f6e85437b0dbcca661013a19247889a9 to your computer and use it in GitHub Desktop.
Save mattem/f6e85437b0dbcca661013a19247889a9 to your computer and use it in GitHub Desktop.
karma web tests with Chrome and ChromeHeadless with bazel
// THIS FILE IS AUTO GENERATED BY TEMPL_TARGET
// DO NOT EDIT
const path = require('path');
const child_process = require('child_process');
const runfiles = require(process.env.BAZEL_NODE_RUNFILES_HELPER);
/**
* Helper function to find a particular namedFile
* within the webTestMetadata webTestFiles
*/
function findNamedFile(webTestMetadata, key) {
let result;
webTestMetadata.webTestFiles.forEach(entry => {
const webTestNamedFiles = entry.namedFiles;
if (webTestNamedFiles && webTestNamedFiles[key]) {
result = webTestNamedFiles[key];
}
});
return result;
}
/**
* Helper function to extract a browser archive
* and return the path to extract executable
*/
function extractWebArchive(extractExe, archiveFile, executablePath) {
try {
// Paths are relative to the root runfiles folder
extractExe = extractExe ? path.join('..', extractExe) : extractExe;
archiveFile = path.join('..', archiveFile);
const extractedExecutablePath = path.join(process.cwd(), executablePath);
if (!extractExe) {
throw new Error('No EXTRACT_EXE found');
}
child_process.execFileSync(
extractExe, [archiveFile, '.'], {stdio: [process.stdin, process.stdout, process.stderr]});
return extractedExecutablePath;
} catch (e) {
console.error(`Failed to extract ${archiveFile}`);
throw e;
}
}
/**
* Check if Chrome sandboxing is supported on the current platform.
*/
function supportChromeSandboxing() {
if (process.platform === 'darwin') {
// Chrome 73+ fails to initialize the sandbox on OSX when running under Bazel.
// ```
// ERROR [launcher]: Cannot start ChromeHeadless
// ERROR:crash_report_database_mac.mm(96)] mkdir
// /private/var/tmp/_bazel_greg/62ef096b0da251c6d093468a1efbfbd3/execroot/angular/bazel-out/darwin-fastbuild/bin/external/io_bazel_rules_webtesting/third_party/chromium/chromium.out/chrome-mac/Chromium.app/Contents/Versions/73.0.3683.0/Chromium
// Framework.framework/Versions/A/new: Permission denied (13) ERROR:file_io.cc(89)]
// ReadExactly: expected 8, observed 0 ERROR:crash_report_database_mac.mm(96)] mkdir
// /private/var/tmp/_bazel_greg/62ef096b0da251c6d093468a1efbfbd3/execroot/angular/bazel-out/darwin-fastbuild/bin/external/io_bazel_rules_webtesting/third_party/chromium/chromium.out/chrome-mac/Chromium.app/Contents/Versions/73.0.3683.0/Chromium
// Framework.framework/Versions/A/new: Permission denied (13) Chromium Helper[94642] <Error>:
// SeatbeltExecServer: Failed to initialize sandbox: -1 Operation not permitted Failed to
// initialize sandbox. [0213/201206.137114:FATAL:content_main_delegate.cc(54)] Check failed:
// false. 0 Chromium Framework 0x000000010c078bc9 ChromeMain + 43788137 1
// Chromium Framework 0x000000010bfc0f43 ChromeMain + 43035363
// ...
// ```
return false;
}
if (process.platform === 'linux') {
// Chrome on Linux uses sandboxing, which needs user namespaces to be enabled.
// This is not available on all kernels and it might be turned off even if it is available.
// Notable examples where user namespaces are not available include:
// - In Debian it is compiled-in but disabled by default.
// - The Docker daemon for Windows or OSX does not support user namespaces.
// We can detect if user namespaces are supported via
// /proc/sys/kernel/unprivileged_userns_clone. For more information see:
// https://github.com/Googlechrome/puppeteer/issues/290
// https://superuser.com/questions/1094597/enable-user-namespaces-in-debian-kernel#1122977
// https://github.com/karma-runner/karma-chrome-launcher/issues/158
// https://github.com/angular/angular/pull/24906
try {
const res = child_process.execSync('cat /proc/sys/kernel/unprivileged_userns_clone')
.toString()
.trim();
return res === '1';
} catch (error) {
}
return false;
}
return true;
}
function setChromeBrowserForEnv(conf) {
// when using bazel run, BUILD_WORKSPACE_DIRECTORY is set to the the absolute path of the bazel workspace
// however we also want to run in headless when running remotely and the user is SSH'd to the remote machine
// when using bazel test, this env var is not set
// this allows us to use chrome for debugging with the run verb
const browser = process.env.BUILD_WORKSPACE_DIRECTORY && !process.env.SSH_CONNECTION ? 'Chrome' : 'ChromeHeadless';
const webTestMetadata = require(runfiles.resolve(process.env['WEB_TEST_METADATA']));
if (webTestMetadata.environment === 'local') {
// When a local chrome (or firefox) browser is chosen such as
// "@io_bazel_rules_webtesting//browsers:chromium-local" or
// "@io_bazel_rules_webtesting//browsers:firefox-local"
// then the 'environment' will equal 'local' and
// 'webTestFiles' will contain the path to the binary to use
const extractExe = findNamedFile(webTestMetadata, 'EXTRACT_EXE');
webTestMetadata.webTestFiles.forEach(webTestFiles => {
const webTestNamedFiles = webTestFiles.namedFiles;
const archiveFile = webTestFiles.archiveFile;
if (webTestNamedFiles.CHROMIUM) {
// When karma is configured to use Chrome it will look for a CHROME_BIN
// environment variable.
if (archiveFile) {
process.env.CHROME_BIN = extractWebArchive(extractExe, archiveFile, webTestNamedFiles.CHROMIUM);
} else {
try {
process.env.CHROME_BIN = runfiles.resolve(webTestNamedFiles.CHROMIUM);
} catch {
// Fail as this file is expected to be in runfiles
throw new Error(`Failed to resolve rules_webtesting Chromium binary '${webTestNamedFiles.CHROMIUM}' in runfiles`);
}
}
}
});
}
if (!supportChromeSandboxing()) {
const launcher = 'CustomChromeWithoutSandbox';
conf.customLaunchers = {[launcher]: {base: browser, flags: ['--no-sandbox']}};
conf.browsers.push(launcher);
} else {
conf.browsers.push(browser);
}
}
function setIsSingleRun(conf) {
// for bazel run, we don't want a single run, and instead we want to keep the browser open for debugging
// for test, run the test and close right away
conf.singleRun = !process.env.BUILD_WORKSPACE_DIRECTORY;
}
function setIsAutoWatch(conf) {
conf.autoWatch = process.env.IBAZEL_NOTIFY_CHANGES === 'y';
}
module.exports = function(config) {
const conf = {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '.',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
{ pattern: runfiles.resolve('npm/node_modules/zone.js/dist/zone-testing-bundle.js') },
{ pattern: runfiles.resolve('npm/node_modules/reflect-metadata/Reflect.js') },
{ pattern: runfiles.resolve(path.posix.join(process.env.BAZEL_WORKSPACE, 'TEMPL_TEST_BUNDLE')), type: 'module' },
{ pattern: runfiles.resolve(path.posix.join(process.env.BAZEL_WORKSPACE, 'TEMPL_MAP_TEST_BUNDLE')), included: false, watched: false }
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'**/*.js': ['sourcemap']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
// this is set later by setChromeBrowserForEnv
browsers: [],
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
};
setChromeBrowserForEnv(conf);
setIsSingleRun(conf);
setIsAutoWatch(conf);
config.set(conf);
}
load("//ev/tooling/bazel/rules_typescript:ts_library.bzl", _ts_library = "ts_library")
load("//ev/tooling/bazel/rules_ev_web:esbuild.bzl", _ev_es_build = "ev_es_build")
load("//ev/tooling/bazel/ev_lib:expand_template.bzl", _ev_expand_template = "ev_expand_template")
load("@io_bazel_rules_webtesting//web:web.bzl", _web_test_suite = "web_test_suite")
load("@bazel_skylib//rules:write_file.bzl", _write_file = "write_file")
load("@npm//karma:index.bzl", _karma = "karma")
def generate_test_entry_point(name, specs, test_library):
entry_point = "%s.bootstrap.ts" % name
_write_file(
name = "%s_bootstrap_ts_file" % name,
content = [
"// THIS FILE IS GENERATED BY %s DO NOT EDIT" % name,
"declare const __karma__: any;",
"__karma__.loaded = () => {};",
] + [
"import './%s';" % spec[:-3]
for spec in specs if spec.endswith('spec.ts')
] + [
"__karma__.start();",
],
out = entry_point,
)
entry_point_library = "%s_bootstrap" % name
_ts_library(
name = entry_point_library,
testonly = 1,
srcs = [
":%s_bootstrap_ts_file" % name,
],
linting = False,
deps = [
":%s" % test_library,
],
)
return entry_point, entry_point_library
def generate_testing_bundle(name, entry_point, deps):
_ev_es_build(
name = "%s_bundle" % name,
testonly = 1,
entry_point = entry_point,
deps = deps,
format = "esm",
)
def generate_karma_runner(name):
_ev_expand_template(
name = "%s_karma.conf" % name,
testonly = 1,
template = "//ev/tooling/bazel/rules_ev_web:_karma.conf.js",
substitutions = {
"TEMPL_TARGET": name,
"TEMPL_TEST_BUNDLE": "%s/%s_bundle.js" % (native.package_name(), name),
"TEMPL_MAP_TEST_BUNDLE": "%s/%s_bundle.js.map" % (native.package_name(), name),
},
data = [
":%s_bundle.js" % name,
":%s_bundle.js.map" % name,
],
out = "%s.karma.conf.js" % name,
)
wrapped_test_name = "%s_karma" % name
_karma(
name = wrapped_test_name,
testonly = 1,
data = [
"%s_karma.conf" % name,
":%s_bundle.js" % name,
":%s_bundle.js.map" % name,
"@npm//karma-jasmine",
"@npm//karma-chrome-launcher",
"@npm//karma-sourcemap-loader",
"@npm//:node_modules/zone.js/dist/zone-testing-bundle.js",
"@npm//:node_modules/reflect-metadata/Reflect.js",
],
templated_args = [
"start",
"$(rootpath %s_karma.conf)" % name,
],
tags = ["native"],
)
return wrapped_test_name
def ev_ts_web_test(name, srcs = [], deps = [], size = "large", test_suite_tags = [], linting = True, **kwargs):
test_library = "%s_lib" % name
_ts_library(
name = test_library,
testonly = 1,
srcs = srcs,
deps = [
"@npm//@types/jasmine",
"@npm//rxjs",
] + deps,
linting = linting,
)
entry_point, entry_point_library = generate_test_entry_point(name, srcs, test_library)
generate_testing_bundle(name, entry_point, [entry_point_library])
wrapped_test_name = generate_karma_runner(name)
tags = kwargs.pop("tags", []) + [
# Users don't need to know that this tag is required to run under ibazel
"ibazel_notify_changes",
]
# rules_webesting requires the "native" tag for browsers
if not "native" in tags:
tags = tags + ["native"]
_web_test_suite(
name = name,
args = kwargs.pop("args", None),
flaky = kwargs.pop("flaky", None),
local = kwargs.pop("local", None),
shard_count = kwargs.pop("shard_count", None),
size = kwargs.pop("size", "large"),
timeout = kwargs.pop("timeout", None),
launcher = ":" + wrapped_test_name,
browsers = ["@io_bazel_rules_webtesting//browsers:chromium-local"],
browser_overrides = kwargs.pop("browser_overrides", None),
config = kwargs.pop("config", None),
data = kwargs.pop("web_test_data", []),
tags = tags,
test = ":" + wrapped_test_name,
test_suite_tags = kwargs.pop("test_suite_tags", None),
visibility = kwargs.pop("visibility", None),
)
@mattem
Copy link
Author

mattem commented Oct 16, 2020

ev_ts_web_test(
    name = "test",
    srcs = [
        "foo.spec.ts",
    ],
    deps = [
        ...
    ],
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment