From 491949fd4b59e09605ff0fdfb146290e0830f7f9 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 14 Dec 2017 12:49:19 -0800 Subject: [PATCH] Introduce ts_web_test rule which runs karma Design docs: ABC: https://goo.gl/QX4bc4 Google: https://docs.google.com/a/angular.io/document/d/1BELNK46L_kqMH_KjeOu1AjgKhHKvXumSxDdPACKDk68/edit?usp=drivesdk Closes #72 PiperOrigin-RevId: 179081847 --- defs.bzl | 4 +- examples/testing/BUILD.bazel | 20 +++++ examples/testing/decrement.spec.ts | 7 ++ examples/testing/decrement.ts | 3 + internal/karma/BUILD.bazel | 20 +++++ internal/karma/index.ts | 74 ++++++++++++++++++ internal/karma/karma.conf.js | 56 ++++++++++++++ internal/karma/package.json | 13 ++++ internal/karma/ts_web_test.bzl | 118 +++++++++++++++++++++++++++++ internal/ts_repositories.bzl | 5 ++ package.json | 1 + yarn.lock | 4 + 12 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 examples/testing/BUILD.bazel create mode 100644 examples/testing/decrement.spec.ts create mode 100644 examples/testing/decrement.ts create mode 100644 internal/karma/BUILD.bazel create mode 100644 internal/karma/index.ts create mode 100644 internal/karma/karma.conf.js create mode 100644 internal/karma/package.json create mode 100644 internal/karma/ts_web_test.bzl diff --git a/defs.bzl b/defs.bzl index 7a27cd2d..fbb30474 100644 --- a/defs.bzl +++ b/defs.bzl @@ -20,8 +20,10 @@ load("//internal:build_defs.bzl", _ts_library = "ts_library") load("//internal:ts_config.bzl", _ts_config = "ts_config") load("//internal/devserver:ts_devserver.bzl", _ts_devserver = "ts_devserver_macro") load("//internal:ts_repositories.bzl", _ts_repositories = "ts_repositories") +load("//internal/karma:ts_web_test.bzl", _ts_web_test = "ts_web_test_macro") ts_library = _ts_library ts_config = _ts_config -ts_repositories = _ts_repositories ts_devserver = _ts_devserver +# TODO(alexeagle): make ts_web_test work in google3 +ts_web_test = _ts_web_test diff --git a/examples/testing/BUILD.bazel b/examples/testing/BUILD.bazel new file mode 100644 index 00000000..cb79f03a --- /dev/null +++ b/examples/testing/BUILD.bazel @@ -0,0 +1,20 @@ +load("@build_bazel_rules_typescript//:defs.bzl", "ts_library", "ts_web_test") + +ts_library( + name = "lib", + srcs = ["decrement.ts"], +) + +ts_library( + name = "tests", + srcs = ["decrement.spec.ts"], + deps = [":lib"], + testonly = 1, +) + +ts_web_test( + name = "testing", + deps = [ + ":tests", + ], +) diff --git a/examples/testing/decrement.spec.ts b/examples/testing/decrement.spec.ts new file mode 100644 index 00000000..bf1bd91d --- /dev/null +++ b/examples/testing/decrement.spec.ts @@ -0,0 +1,7 @@ +import {decrement} from './decrement'; + +describe('decrementing', () => { + it('should do that', () => { + expect(decrement(1)).toBe(0); + }); +}); diff --git a/examples/testing/decrement.ts b/examples/testing/decrement.ts new file mode 100644 index 00000000..7173ecc4 --- /dev/null +++ b/examples/testing/decrement.ts @@ -0,0 +1,3 @@ +export function decrement(n: number) { + return n - 1; +} diff --git a/internal/karma/BUILD.bazel b/internal/karma/BUILD.bazel new file mode 100644 index 00000000..9a45e8e6 --- /dev/null +++ b/internal/karma/BUILD.bazel @@ -0,0 +1,20 @@ +package(default_visibility=["//visibility:public"]) + +exports_files(["test-main.js", "karma.conf.js"]) + +load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") + +ts_library( + name = "karma_concat_js", + srcs = glob(["*.ts"]), + module_name = "karma-concat-js", +) + +load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary") + +nodejs_binary( + name = "karma_bin", + entry_point = "karma/bin/karma", + data = [":karma_concat_js"], + node_modules = "@build_bazel_rules_typescript_karma_deps//:node_modules", +) diff --git a/internal/karma/index.ts b/internal/karma/index.ts new file mode 100644 index 00000000..8ae1809d --- /dev/null +++ b/internal/karma/index.ts @@ -0,0 +1,74 @@ +/* + * Concat all JS files before serving. + */ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; +import * as tmp from 'tmp'; +/// + +/** + * Return SHA1 of data buffer. + */ +function sha1(data) { + const hash = crypto.createHash('sha1'); + hash.update(data); + return hash.digest('hex'); +} + +/** + * Entry-point for the Karma plugin. + */ +function initConcatJs(logger, emitter, basePath) { + const log = logger.create('framework.concat_js'); + + // Create a tmp file for the concat bundle that is automatically cleaned up on + // exit. + const tmpFile = tmp.fileSync({keep: false, dir: process.env['TEST_TMPDIR']}); + + emitter.on('file_list_modified', files => { + const bundleFile = { + path: '/concatjs_bundle.js', + contentPath: tmpFile.name, + isUrl: false, + content: '' + } as any; + const included = []; + + files.included.forEach(file => { + if (path.extname(file.originalPath) !== '.js') { + // Preserve all non-JS that were there in the included list. + included.push(file); + } else { + const relativePath = path.relative(basePath, file.originalPath); + + // Remove 'use strict'. + let content = file.content.replace(/('use strict'|"use strict");?/, + ''); + content = JSON.stringify( + content + '\n//# sourceURL=http://concatjs/base/' + + relativePath + '\n'); + content = `//${relativePath}\neval(${content});\n`; + bundleFile.content += content; + } + }); + + bundleFile.sha = sha1(new Buffer(bundleFile.content)); + bundleFile.mtime = new Date(); + included.unshift(bundleFile); + + files.included = included; + files.served.push(bundleFile); + + log.debug('Writing concatjs bundle to tmp file %s', + bundleFile.contentPath); + fs.writeFileSync(bundleFile.contentPath, bundleFile.content); + }); +} + +(initConcatJs as any).$inject = ['logger', 'emitter', 'config.basePath']; + +module.exports = { + 'framework:concat_js': ['factory', initConcatJs] +}; diff --git a/internal/karma/karma.conf.js b/internal/karma/karma.conf.js new file mode 100644 index 00000000..e256f1e4 --- /dev/null +++ b/internal/karma/karma.conf.js @@ -0,0 +1,56 @@ +// Karma configuration +// GENERATED BY Bazel +module.exports = function(config) { + if (process.env['IBAZEL_NOTIFY_CHANGES'] === 'y') { + // Tell karma to only listen for ibazel messages on stdin rather than watch all the input files + // This is from fork alexeagle/karma in the ibazel branch: + // https://github.com/alexeagle/karma/blob/576d262af50b10e63485b86aee99c5358958c4dd/lib/server.js#L172 + config.set({watchMode: 'ibazel'}); + } + + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: 'TMPL_runfiles_path', + files: [ + 'build_bazel_rules_typescript_karma_deps/node_modules/karma/requirejs.config.tpl.js', +TMPL_files + ], + plugins: ['karma-*', 'karma-concat-js'], + frameworks: ['jasmine', 'concat_js', 'requirejs'], + + // 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, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['DISPLAY' in Object.keys(process.env) ? 'Chrome': 'ChromeHeadless'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + // note: run_karma.sh may override this as a command-line option. + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/internal/karma/package.json b/internal/karma/package.json new file mode 100644 index 00000000..86e6f47b --- /dev/null +++ b/internal/karma/package.json @@ -0,0 +1,13 @@ +{ + "description": "runtime dependences for ts_web_test rules", + "devDependencies": { + "@types/tmp": "0.0.33", + "jasmine-core": "2.8.0", + "karma": "alexeagle/karma#fa1a84ac881485b5657cb669e9b4e5da77b79f0a", + "karma-chrome-launcher": "2.2.0", + "karma-jasmine": "1.1.1", + "karma-requirejs": "1.1.0", + "requirejs": "2.3.5", + "tmp": "0.0.33" + } +} diff --git a/internal/karma/ts_web_test.bzl b/internal/karma/ts_web_test.bzl new file mode 100644 index 00000000..ce1b4abe --- /dev/null +++ b/internal/karma/ts_web_test.bzl @@ -0,0 +1,118 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Implementation of the ts_web_test rule.""" + +load("@build_bazel_rules_nodejs//internal:node.bzl", + "sources_aspect", + "expand_path_into_runfiles", +) + +_CONF_TMPL = "//internal/karma:karma.conf.js" +_LOADER = "@build_bazel_rules_typescript_karma_deps//:node_modules/karma/requirejs.config.tpl.js" + +def _ts_web_test_impl(ctx): + conf = ctx.actions.declare_file( + "%s.conf.js" % ctx.label.name, + sibling=ctx.outputs.executable) + + files = depset(ctx.files.srcs) + for d in ctx.attr.deps: + if hasattr(d, "node_sources"): + files += d.node_sources + elif hasattr(d, "files"): + files += d.files + + files_entries = [ + " '%s'," % expand_path_into_runfiles(ctx, f.short_path) + for f in files + ] + + # root-relative (runfiles) path to the directory containing karma.conf + config_segments = len(conf.short_path.split("/")) + + ctx.actions.expand_template( + output = conf, + template = ctx.file._conf_tmpl, + substitutions = { + "TMPL_runfiles_path": "/".join([".."] * config_segments), + "TMPL_files": "\n".join(files_entries), + "TMPL_workspace_name": ctx.workspace_name, + }) + + ctx.actions.write( + output = ctx.outputs.executable, + is_executable = True, + content = """#!/usr/bin/env bash +readonly KARMA={TMPL_karma} +readonly CONF={TMPL_conf} +export HOME=$(mktemp -d) +ARGV=( "start" $CONF ) + +# Detect that we are running as a test, by using a well-known environment +# variable. See go/test-encyclopedia +if [ ! -z "$TEST_TMPDIR" ]; then + ARGV+=( "--single-run" ) +fi + +$KARMA ${{ARGV[@]}} +""".format(TMPL_karma = ctx.executable._karma.short_path, + TMPL_conf = conf.short_path)) + return [DefaultInfo( + runfiles = ctx.runfiles( + files = ctx.files.srcs + ctx.files.deps + [ + conf, + ctx.file._loader, + ], + transitive_files = files, + # Propagate karma_bin and its runfiles + collect_data = True, + collect_default = True, + ), + )] + +ts_web_test = rule( + implementation = _ts_web_test_impl, + test = True, + attrs = { + "srcs": attr.label_list(allow_files = ["js"]), + "deps": attr.label_list( + allow_files = True, + aspects = [sources_aspect], + ), + "data": attr.label_list(cfg = "data"), + "_karma": attr.label( + default = Label("//internal/karma:karma_bin"), + executable = True, + cfg = "data", + single_file = False, + allow_files = True), + "_conf_tmpl": attr.label( + default = Label(_CONF_TMPL), + allow_files = True, single_file = True), + "_loader": attr.label( + default = Label(_LOADER), + allow_files = True, single_file = True), + }, +) + +# This macro exists only to modify the users rule definition a bit. +# DO NOT add composition of additional rules here. +def ts_web_test_macro(tags = [], data = [], **kwargs): + ts_web_test( + # Users don't need to know that this tag is required to run under ibazel + tags = tags + ["ibazel_notify_changes"], + # Our binary dependency must be in data[] for collect_data to pick it up + # FIXME: maybe we can just ask the attr._karma for its runfiles attr + data = data + ["@build_bazel_rules_typescript//internal/karma:karma_bin"], + **kwargs) diff --git a/internal/ts_repositories.bzl b/internal/ts_repositories.bzl index 28ee1453..fa69d9fd 100644 --- a/internal/ts_repositories.bzl +++ b/internal/ts_repositories.bzl @@ -59,3 +59,8 @@ def ts_repositories(default_tsconfig = None): name = "build_bazel_rules_typescript_devserver_deps", package_json = "@build_bazel_rules_typescript//internal/devserver:package.json", ) + + npm_install( + name = "build_bazel_rules_typescript_karma_deps", + package_json = "@build_bazel_rules_typescript//internal/karma:package.json", + ) diff --git a/package.json b/package.json index 865ac471..e8969c6a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "typescript": ">=2.4.2" }, "devDependencies": { + "@bazel/ibazel": "^0.2.0", "@types/jasmine": "^2.8.2", "@types/node": "7.0.18", "@types/source-map": "^0.5.1", diff --git a/yarn.lock b/yarn.lock index 707e88aa..a7894c89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@bazel/ibazel@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.2.0.tgz#c119aef4344a789cef5e792caaee52264123e71c" + "@types/jasmine@^2.8.2": version "2.8.2" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.2.tgz#6ae4d8740c0da5d5a627df725b4eed71b8e36668"