test: run legacy-cli e2e tests via bazel

This commit is contained in:
Jason Bedard 2022-09-07 17:56:13 -07:00 committed by angular-robot[bot]
parent b8e68eb28a
commit 794e33ae72
26 changed files with 652 additions and 119 deletions

View File

@ -90,6 +90,10 @@ build:snapshot --workspace_status_command="yarn -s ng-dev release build-env-stam
build:snapshot --stamp
build:snapshot --//:enable_snapshot_repo_deps
build:e2e --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release"
build:e2e --stamp
test:e2e --test_timeout=3600
build:local --//:enable_package_json_tar_deps
###############################

View File

@ -48,7 +48,9 @@ var_6: &only_pull_requests
only:
- /pull\/\d+/
# All e2e test suites
var_7: &all_e2e_subsets ['npm', 'esbuild', 'yarn']
var_8: &all_e2e_build_types ['e2e', 'snapshot']
# Executor Definitions
# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors
@ -63,10 +65,20 @@ executors:
working_directory: ~/ng
resource_class: small
bazel-executor:
parameters:
nodeversion:
type: string
default: *default_nodeversion
docker:
- image: cimg/node:<< parameters.nodeversion >>-browsers
working_directory: ~/ng
resource_class: xlarge
windows-executor:
# Same as https://circleci.com/orbs/registry/orb/circleci/windows, but named.
working_directory: ~/ng
resource_class: windows.medium
resource_class: windows.large
shell: powershell.exe -ExecutionPolicy Bypass
machine:
# Contents of this image:
@ -116,7 +128,7 @@ commands:
- initialize_env
- run: nvm install 16.13
- run: nvm use 16.13
- run: npm install -g yarn@1.22.10
- run: npm install -g yarn@1.22.10 @bazel/bazelisk@${BAZELISK_VERSION}
- run: node --version
- run: yarn --version
@ -126,6 +138,7 @@ commands:
type: env_var_name
default: CIRCLE_PROJECT_REPONAME
steps:
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- devinfra/setup-bazel-remote-exec:
bazelrc: ./.bazelrc.user
@ -269,23 +282,24 @@ jobs:
paths:
- dist/_*.tgz
build-bazel-e2e:
executor: action-executor
resource_class: medium
bazel-build:
executor: bazel-executor
steps:
- custom_attach_workspace
- run: yarn bazel build //tests/legacy-cli/...
- setup_bazel_rbe
- run:
name: Bazel Build Packages
command: yarn bazel build //...
- fail_fast
unit-test:
executor: action-executor
resource_class: xlarge
bazel-test:
executor: bazel-executor
parameters:
nodeversion:
type: string
default: *default_nodeversion_major
steps:
- custom_attach_workspace
- browser-tools/install-chrome
- setup_bazel_rbe
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- when:
@ -311,6 +325,59 @@ jobs:
no_output_timeout: 40m
- fail_fast
bazel-e2e-tests:
executor: bazel-executor
parallelism: 8
parameters:
build_type:
type: enum
enum: *all_e2e_build_types
default: 'e2e'
subset:
type: enum
enum: *all_e2e_subsets
default: 'npm'
steps:
- custom_attach_workspace
- initialize_env
- setup_bazel_rbe
- run: mkdir /mnt/ramdisk/e2e
- run:
name: Test << parameters.build_type >> << parameters.subset >>
command: yarn bazel test --define=E2E_TEMP=/mnt/ramdisk/e2e --define=E2E_SHARD_TOTAL=${CIRCLE_NODE_TOTAL} --define=E2E_SHARD_INDEX=${CIRCLE_NODE_INDEX} --config=<< parameters.build_type >> //tests/legacy-cli:e2e.<< parameters.subset >>
no_output_timeout: 40m
- store_artifacts:
path: dist/testlogs/tests/legacy-cli/e2e.<< parameters.subset >>
- store_test_results:
path: dist/testlogs/tests/legacy-cli/e2e.<< parameters.subset >>
- fail_fast
bazel-test-browsers:
executor: bazel-executor
steps:
- custom_attach_workspace
- initialize_env
- setup_bazel_rbe
- run:
name: Initialize Saucelabs
command: setSecretVar SAUCE_ACCESS_KEY $(echo $SAUCE_ACCESS_KEY | rev)
- run:
name: Start Saucelabs Tunnel
command: ./scripts/saucelabs/start-tunnel.sh
background: true
# Waits for the Saucelabs tunnel to be ready. This ensures that we don't run tests
# too early without Saucelabs not being ready.
- run: ./scripts/saucelabs/wait-for-tunnel.sh
- run:
name: E2E Saucelabs Tests
command: yarn bazel test --config=saucelabs //tests/legacy-cli:e2e.saucelabs
- run: ./scripts/saucelabs/stop-tunnel.sh
- store_artifacts:
path: dist/testlogs/tests/legacy-cli/e2e.saucelabs
- store_test_results:
path: dist/testlogs/tests/legacy-cli/e2e.saucelabs
- fail_fast
snapshot_publish:
executor: action-executor
resource_class: medium
@ -382,6 +449,48 @@ jobs:
node tests\legacy-cli\run_e2e.js --nb-shards=$env:CIRCLE_NODE_TOTAL --shard=$env:CIRCLE_NODE_INDEX --tmpdir=X:/ramdisk/e2e-main --ignore="tests/misc/browsers.ts"
- fail_fast
bazel-e2e-cli-win:
executor: windows-executor
parallelism: 12
steps:
- checkout
- rebase_pr_win
- setup_windows
- restore_cache:
keys:
- *cache_key_win
- run:
# We use Arsenal Image Mounter (AIM) instead of ImDisk because of: https://github.com/nodejs/node/issues/6861
# Useful resources for AIM: http://reboot.pro/index.php?showtopic=22068
name: 'Arsenal Image Mounter (RAM Disk)'
command: |
pwsh ./.circleci/win-ram-disk.ps1
- run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
key: *cache_key_win
paths:
- ~/.cache/yarn
# Path where Arsenal Image Mounter files are downloaded.
# Must match path in .circleci/win-ram-disk.ps1
- ./aim
- run:
name: Execute E2E Tests
environment:
# Required by `yarn ng-dev`
# See https://github.com/angular/angular/issues/46858
PWD: .
command: |
mkdir X:/ramdisk/e2e
bazel test --define=E2E_TEMP=X:/ramdisk/e2e --define=E2E_SHARD_TOTAL=$env:CIRCLE_NODE_TOTAL --define=E2E_SHARD_INDEX=$env:CIRCLE_NODE_INDEX --config=e2e //tests/legacy-cli:e2e.npm
# This timeout provides time for the actual tests to timeout and report status
# instead of CircleCI stopping the job without test failure information.
no_output_timeout: 40m
- fail_fast
- store_artifacts:
path: dist/testlogs/tests/legacy-cli/e2e.npm
- store_test_results:
path: dist/testlogs/tests/legacy-cli/e2e.npm
workflows:
version: 2
default_workflow:
@ -457,23 +566,67 @@ workflows:
# These jobs only really depend on Setup, but the build job is very quick to run (~35s) and
# will catch any build errors before proceeding to the more lengthy and resource intensive
# Bazel jobs.
- unit-test:
- bazel-test:
name: test-node<< matrix.nodeversion >>
matrix:
parameters:
nodeversion: *all_nodeversion_major
requires:
- build
# Compile the e2e tests with bazel to ensure the non-runtime typescript
# compilation completes succesfully.
- build-bazel-e2e:
requires:
- build
- bazel-build
# Windows jobs
- e2e-cli-win
- bazel-e2e-cli-win
# Bazel jobs
- bazel-build:
requires:
- setup
- bazel-e2e-tests:
name: bazel-e2e-cli-<< matrix.subset >>
matrix:
parameters:
subset: *all_e2e_subsets
build_type: 'e2e'
filters:
branches:
ignore:
- main
- /\d+\.\d+\.x/
requires:
- bazel-build
- bazel-e2e-tests:
name: bazel-e2e-snapshots-<< matrix.subset >>
matrix:
parameters:
subset: *all_e2e_subsets
build_type: 'snapshot'
pre-steps:
- when:
condition:
and:
- not:
equal: [main, << pipeline.git.branch >>]
- not: << pipeline.parameters.snapshot_changed >>
steps:
# Don't run snapshot E2E's unless it's on the main branch or the snapshots file has been updated.
- run: circleci-agent step halt
requires:
- bazel-build
filters:
branches:
only:
- main
# This is needed to run this steps on Renovate PRs that amend the snapshots package.json
- /^pull\/.*/
- bazel-test-browsers:
requires:
- bazel-build
# Publish jobs
- snapshot_publish:
<<: *only_release_branches

View File

@ -36,3 +36,8 @@ source $BASH_ENV;
# Disable husky.
setPublicVar HUSKY 0
# Expose the Bazelisk version. We need to run Bazelisk globally since Windows has problems launching
# Bazel from a node modules directoy that might be modified by the Bazel Yarn install then.
setPublicVar BAZELISK_VERSION \
"$(cd ${PROJECT_ROOT}; node -p 'require("./package.json").devDependencies["@bazel/bazelisk"]')"

View File

@ -26,5 +26,6 @@ if (-not (Test-Path -Path $aimContents)) {
./aim/cli/x64/aim_ll.exe --install ./aim/drivers
# Setup RAM disk mount. Same parameters as ImDisk
# Ensure size is large enough to support the bazel 'shard_count's such as for e2e tests.
# See: https://support.circleci.com/hc/en-us/articles/4411520952091-Create-a-windows-RAM-disk
./aim/cli/x64/aim_ll.exe -a -s 5G -m X: -p "/fs:ntfs /q /y"
./aim/cli/x64/aim_ll.exe -a -s 12G -m X: -p "/fs:ntfs /q /y"

View File

@ -3,6 +3,7 @@
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
package(default_visibility = ["//visibility:public"])
@ -16,6 +17,14 @@ exports_files([
"package.json",
])
# Files required by e2e tests
copy_to_bin(
name = "config-files",
srcs = [
"package.json",
],
)
# Detect if the build is running under --stamp
config_setting(
name = "stamp",

View File

@ -98,3 +98,14 @@ nodejs_register_toolchains(
name = "node16",
node_version = "16.13.1",
)
register_toolchains(
"@npm//@angular/build-tooling/bazel/git-toolchain:git_linux_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_x86_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_arm64_toolchain",
"@npm//@angular/build-tooling/bazel/git-toolchain:git_windows_toolchain",
)
load("@npm//@angular/build-tooling/bazel/browsers:browser_repositories.bzl", "browser_repositories")
browser_repositories()

View File

@ -85,7 +85,7 @@ You can find more info about debugging [tests with Bazel in the docs.](https://g
- Compile the packages being tested: `yarn build`
- Run all tests: `node tests/legacy-cli/run_e2e.js`
- Run a subset of the tests: `node tests/legacy-cli/run_e2e.js tests/legacy-cli/e2e/tests/i18n/ivy-localize-*`
- Run on a custom set of npm packages (tar files): `node tests/legacy-cli/run_e2e.js --package _angular_cli.tgz _angular_create.tgz dist/*.tgz ...`
- Run on a custom set of npm packages (tar files): `node tests/legacy-cli/run_e2e.js --package _angular_cli.tgz _angular_create.tgz dist/*.tgz tests/legacy-cli/e2e/tests/i18n/ivy-localize-*`
When running the debug commands, Node will stop and wait for a debugger to attach.
You can attach your IDE to the debugger to stop on breakpoints and step through the code. Also, see [IDE Specific Usage](#ide-specific-usage) for a

View File

@ -23,7 +23,7 @@ class Version {
}
}
// TODO: Convert this to use build-time version stamping after flipping the build script to use bazel
// TODO(bazel): Convert this to use build-time version stamping after flipping the build script to use bazel
// export const VERSION = new Version('0.0.0-PLACEHOLDER');
export const VERSION = new Version(
(

View File

@ -1,4 +1,5 @@
load("//tools:defaults.bzl", "ts_library")
load(":e2e.bzl", "e2e_suites")
ts_library(
name = "runner",
@ -11,16 +12,25 @@ ts_library(
deps = [
"//packages/angular_devkit/core",
"//packages/angular_devkit/core/node",
"//tests/legacy-cli/e2e/assets",
"//tests/legacy-cli/e2e/utils",
"@npm//@types/glob",
"@npm//@types/yargs-parser",
"@npm//ansi-colors",
"@npm//yargs-parser",
],
)
e2e_suites(
name = "e2e",
data = [
":runner",
# Tests + setup
# Loaded dynamically at runtime, not compiletime deps
"//tests/legacy-cli/e2e/assets",
"//tests/legacy-cli/e2e/setup",
"//tests/legacy-cli/e2e/initialize",
"//tests/legacy-cli/e2e/tests",
],
runner = ":e2e_runner.ts",
)

158
tests/legacy-cli/e2e.bzl Normal file
View File

@ -0,0 +1,158 @@
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_test")
# bazel query --output=label "kind('pkg_tar', //packages/...)"
TESTED_PACKAGES = [
"//packages/angular/cli:npm_package_archive.tgz",
"//packages/angular/create:npm_package_archive.tgz",
"//packages/angular/pwa:npm_package_archive.tgz",
"//packages/angular_devkit/architect:npm_package_archive.tgz",
"//packages/angular_devkit/architect_cli:npm_package_archive.tgz",
# this is private so don't use here
# "//packages/angular_devkit/benchmark:npm_package_archive.tgz",
"//packages/angular_devkit/build_angular:npm_package_archive.tgz",
"//packages/angular_devkit/build_webpack:npm_package_archive.tgz",
"//packages/angular_devkit/core:npm_package_archive.tgz",
"//packages/angular_devkit/schematics:npm_package_archive.tgz",
"//packages/angular_devkit/schematics_cli:npm_package_archive.tgz",
"//packages/ngtools/webpack:npm_package_archive.tgz",
"//packages/schematics/angular:npm_package_archive.tgz",
]
# Number of bazel shards per test target
TEST_SHARD_COUNT = 4
# NB: does not run on rbe because webdriver manager uses an absolute path to chromedriver
# Requires network to fetch npm packages.
TEST_TAGS = ["no-remote-exec", "requires-network"]
# Subset of tests for yarn/esbuild
BROWSER_TESTS = ["tests/misc/browsers.js"]
YARN_TESTS = ["tests/basic/**", "tests/update/**", "tests/commands/add/**"]
ESBUILD_TESTS = ["tests/basic/**", "tests/build/prod-build.js"]
# Tests excluded for esbuild
ESBUILD_IGNORE_TESTS = [
"tests/basic/environment.js",
"tests/basic/rebuild.js",
"tests/basic/serve.js",
"tests/basic/scripts-array.js",
]
def _to_glob(patterns):
if len(patterns) == 1:
return patterns[0]
return "\"{%s}\"" % ",".join(patterns)
def e2e_suites(name, runner, data):
"""
Construct all e2e test suite targets
Args:
name: the prefix to all rules
runner: the e2e test runner entry point
data: runtime deps such as tests and test data
"""
# Default target meant to be run manually for debugging, customizing test cli via bazel
_e2e_tests(name, runner = runner, data = data, tags = ["manual"])
# Pre-configured test suites
# TODO: add node 14 + 16
_e2e_suite(name, runner, "npm", data)
_e2e_suite(name, runner, "yarn", data)
_e2e_suite(name, runner, "esbuild", data)
_e2e_suite(name, runner, "saucelabs", data)
def _e2e_tests(name, runner, **kwargs):
# Always specify all the npm packages
args = kwargs.pop("templated_args", []) + ["--package"] + [
"$(rootpath %s)" % p
for p in TESTED_PACKAGES
]
# Always add all the npm packages as data
data = kwargs.pop("data", []) + TESTED_PACKAGES
# Tags that must always be applied
tags = kwargs.pop("tags", []) + TEST_TAGS
# Passthru E2E variables in case it is customized by CI etc
configuration_env_vars = kwargs.pop("configuration_env_vars", []) + ["E2E_TEMP", "E2E_SHARD_INDEX", "E2E_SHARD_TOTAL"]
env = kwargs.pop("env", {})
toolchains = kwargs.pop("toolchains", [])
# The git toolchain + env
env.update({"GIT_BIN": "$(GIT_BIN_PATH)"})
toolchains = toolchains + ["@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain"]
# Chromium browser toolchain
env.update({
"CHROME_BIN": "$(CHROMIUM)",
"CHROMEDRIVER_BIN": "$(CHROMEDRIVER)",
})
toolchains = toolchains + ["@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias"]
data = data + ["@npm//@angular/build-tooling/bazel/browsers/chromium"]
nodejs_test(
name = name,
templated_args = args,
data = data,
entry_point = runner,
env = env,
configuration_env_vars = configuration_env_vars,
tags = tags,
toolchains = toolchains,
**kwargs
)
def _e2e_suite(name, runner, type, data):
"""
Setup a predefined test suite (yarn|esbuild|saucelabs|npm).
"""
args = []
tests = None
ignore = None
if type == "yarn":
args.append("--yarn")
tests = YARN_TESTS
ignore = BROWSER_TESTS
elif type == "esbuild":
args.append("--esbuild")
tests = ESBUILD_TESTS
ignore = BROWSER_TESTS + ESBUILD_IGNORE_TESTS
elif type == "saucelabs":
tests = BROWSER_TESTS
ignore = None
elif type == "npm":
tests = None
ignore = BROWSER_TESTS
# Standard e2e tests
_e2e_tests(
name = "%s.%s" % (name, type),
runner = runner,
size = "enormous",
data = data,
shard_count = TEST_SHARD_COUNT,
templated_args = [
"--glob=%s" % _to_glob(tests) if tests else "",
"--ignore=%s" % _to_glob(ignore) if ignore else "",
],
)
# e2e tests of snapshot builds
_e2e_tests(
name = "%s.snapshot.%s" % (name, type),
runner = runner,
size = "enormous",
data = data,
shard_count = TEST_SHARD_COUNT,
templated_args = [
"--ng-snapshots",
"--glob=%s" % _to_glob(tests) if tests else "",
"--ignore=%s" % _to_glob(ignore) if ignore else "",
],
)

View File

@ -1,11 +1,7 @@
load("//tools:defaults.bzl", "js_library")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
js_library(
copy_to_bin(
name = "assets",
srcs = glob(["**"]),
visibility = ["//visibility:public"],
deps = [
"@npm//jasmine-spec-reporter",
"@npm//ts-node",
],
)

View File

@ -1,5 +1,6 @@
import { join } from 'path';
import yargsParser from 'yargs-parser';
import { IS_BAZEL } from '../utils/bazel';
import { getGlobalVariable } from '../utils/env';
import { expectFileToExist } from '../utils/fs';
import { gitClean } from '../utils/git';
@ -23,11 +24,13 @@ export default async function () {
// Install puppeteer in the parent directory for use by the CLI within any test project.
// Align the version with the primary project package.json.
const puppeteerVersion = require('../../../../package.json').devDependencies.puppeteer.replace(
/^[\^~]/,
'',
);
await installPackage(`puppeteer@${puppeteerVersion}`);
// Bazel has own browser toolchains
// TODO(bazel): remove non-bazel
if (!IS_BAZEL) {
const puppeteerVersion =
require('../../../../package.json').devDependencies.puppeteer.replace(/^[\^~]/, '');
await installPackage(`puppeteer@${puppeteerVersion}`);
}
await ng('new', 'test-project', '--skip-install');
await expectFileToExist(join(process.cwd(), 'test-project'));

View File

@ -5,7 +5,7 @@ ts_library(
testonly = True,
srcs = glob(["**/*.ts"]),
data = [
"//:package.json",
"//:config-files",
],
visibility = ["//visibility:public"],
deps = [

View File

@ -1,8 +1,7 @@
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
ts_library(
copy_to_bin(
name = "ng-snapshot",
srcs = [],
data = ["package.json"],
srcs = ["package.json"],
visibility = ["//visibility:public"],
)

View File

@ -12,7 +12,7 @@ export default async function () {
} else if (argv.tmpdir) {
tempRoot = argv.tmpdir;
} else {
tempRoot = await mktempd('angular-cli-e2e-');
tempRoot = await mktempd('angular-cli-e2e-', process.env.E2E_TEMP);
}
console.log(` Using "${tempRoot}" as temporary directory for a new project.`);
setGlobalVariable('tmp-root', tempRoot);

View File

@ -24,6 +24,10 @@ export default async function () {
process.env.NPM_CONFIG_PREFIX = npmModulesPrefix;
process.env.YARN_CONFIG_PREFIX = yarnModulesPrefix;
// Put the npm+yarn caches in the temp dir
process.env.NPM_CONFIG_CACHE = join(tempRoot, 'npm-cache');
process.env.YARN_CACHE_FOLDER = join(tempRoot, 'yarn-cache');
// Snapshot builds may contain versions that are not yet released (e.g., RC phase main branch).
// In this case peer dependency ranges may not resolve causing npm 7+ to fail during tests.
// To support this case, legacy peer dependency mode is enabled for snapshot builds.

View File

@ -4,9 +4,11 @@ ts_library(
name = "tests",
testonly = True,
srcs = glob(["**/*.ts"]),
data = [
"//tests/legacy-cli/e2e/ng-snapshot",
],
visibility = ["//visibility:public"],
deps = [
"//tests/legacy-cli/e2e/ng-snapshot",
"//tests/legacy-cli/e2e/utils",
"@npm//@types/express",
"@npm//@types/glob",

View File

@ -29,7 +29,18 @@ export default async function () {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadless'],
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
],
}
},
singleRun: false,
restartOnFileChange: true
});

View File

@ -1,7 +1,13 @@
import { join } from 'path';
import { IS_BAZEL } from '../../utils/bazel';
import { execWithEnv } from '../../utils/process';
export default async function () {
// TODO(bazel): fails with bazel on windows
if (IS_BAZEL && process.platform.startsWith('win')) {
return;
}
// Set the esbuild native binary path to a non-existent file to simulate a spawn error.
// The build should still succeed by falling back to the WASM variant of esbuild.
await execWithEnv('ng', ['build'], {

View File

@ -4,9 +4,11 @@ ts_library(
name = "utils",
testonly = True,
srcs = glob(["**/*.ts"]),
data = [
"//tests/legacy-cli/e2e/ng-snapshot",
],
visibility = ["//visibility:public"],
deps = [
"//tests/legacy-cli/e2e/ng-snapshot",
"@npm//@types/glob",
"@npm//@types/node-fetch",
"@npm//@types/semver",
@ -16,7 +18,6 @@ ts_library(
"@npm//glob",
"@npm//npm",
"@npm//protractor",
"@npm//puppeteer",
"@npm//rxjs",
"@npm//semver",
"@npm//tar",

View File

@ -0,0 +1,3 @@
// TODO(bazel): remove this along with any non-bazel specific logic using it.
export const IS_BAZEL = !!process.env.BAZEL_TARGET;

View File

@ -7,6 +7,7 @@ import { getGlobalVariable, getGlobalVariablesEnv } from './env';
import { catchError } from 'rxjs/operators';
import treeKill from 'tree-kill';
import { delimiter, join, resolve } from 'path';
import { IS_BAZEL } from './bazel';
interface ExecOptions {
silent?: boolean;
@ -167,7 +168,23 @@ export function extractNpmEnv() {
function extractCIEnv(): NodeJS.ProcessEnv {
return Object.keys(process.env)
.filter((v) => v.startsWith('SAUCE_') || v === 'CI' || v === 'CIRCLECI' || v === 'CHROME_BIN')
.filter(
(v) =>
v.startsWith('SAUCE_') ||
v === 'CI' ||
v === 'CIRCLECI' ||
v === 'CHROME_BIN' ||
v === 'CHROMEDRIVER_BIN',
)
.reduce<NodeJS.ProcessEnv>((vars, n) => {
vars[n] = process.env[n];
return vars;
}, {});
}
function extractNgEnv() {
return Object.keys(process.env)
.filter((v) => v.startsWith('NG_'))
.reduce<NodeJS.ProcessEnv>((vars, n) => {
vars[n] = process.env[n];
return vars;
@ -357,11 +374,11 @@ export function node(...args: string[]) {
}
export function git(...args: string[]) {
return _exec({}, 'git', args);
return _exec({}, process.env.GIT_BIN || 'git', args);
}
export function silentGit(...args: string[]) {
return _exec({ silent: true }, 'git', args);
return _exec({ silent: true }, process.env.GIT_BIN || 'git', args);
}
/**
@ -372,24 +389,42 @@ export function silentGit(...args: string[]) {
* registry (not the test runner or standard global node_modules).
*/
export async function launchTestProcess(entry: string, ...args: any[]): Promise<void> {
// NOTE: do NOT use the bazel TEST_TMPDIR. When sandboxing is not enabled the
// TEST_TMPDIR is not sandboxed and has symlinks into the src dir in a
// parent directory. Symlinks into the src dir will include package.json,
// .git and other files/folders that may effect e2e tests.
const tempRoot: string = getGlobalVariable('tmp-root');
const TEMP = process.env.TEMP ?? process.env.TMPDIR ?? tempRoot;
// Extract explicit environment variables for the test process.
const env: NodeJS.ProcessEnv = {
TEMP,
TMPDIR: TEMP,
HOME: TEMP,
// Use BAZEL_TARGET as a metadata variable to show it is a
// process managed by bazel
BAZEL_TARGET: process.env.BAZEL_TARGET,
...extractNpmEnv(),
...extractCIEnv(),
...extractNgEnv(),
...getGlobalVariablesEnv(),
};
// Modify the PATH environment variable...
env.PATH = (env.PATH || process.env.PATH)
?.split(delimiter)
// Only include paths within the sandboxed test environment or external
// non angular-cli paths such as /usr/bin for generic commands.
.filter((p) => p.startsWith(tempRoot) || !p.includes('angular-cli'))
// Only include paths within the sandboxed test environment or external
// non angular-cli paths such as /usr/bin for generic commands.
env.PATH = process.env
.PATH!.split(delimiter)
.filter((p) => p.startsWith(tempRoot) || p.startsWith(TEMP) || !p.includes('angular-cli'))
.join(delimiter);
const testProcessArgs = [resolve(__dirname, 'run_test_process'), entry, ...args];
const testProcessArgs = [
resolve(__dirname, IS_BAZEL ? 'test_process' : 'run_test_process'),
entry,
...args,
];
return new Promise<void>((resolve, reject) => {
spawn(process.execPath, testProcessArgs, {

View File

@ -2,8 +2,9 @@ import * as fs from 'fs';
import * as path from 'path';
import { prerelease, SemVer } from 'semver';
import yargsParser from 'yargs-parser';
import { IS_BAZEL } from './bazel';
import { getGlobalVariable } from './env';
import { prependToFile, readFile, replaceInFile, writeFile } from './fs';
import { readFile, replaceInFile, writeFile } from './fs';
import { gitCommit } from './git';
import { findFreePort } from './network';
import { installWorkspacePackages, PkgInfo } from './packages';
@ -50,37 +51,41 @@ export async function prepareProjectForE2e(name: string) {
await installWorkspacePackages();
await ng('generate', 'e2e', '--related-app-name', name);
const protractorPath = require.resolve('protractor');
const webdriverUpdatePath = require.resolve('webdriver-manager/selenium/update-config.json', {
paths: [protractorPath],
});
const webdriverUpdate = JSON.parse(await readFile(webdriverUpdatePath)) as {
chrome: { last: string };
};
// bazel will use its own sandboxed browser + webdriver
// TODO(bazel): remove non-bazel
if (!IS_BAZEL) {
const protractorPath = require.resolve('protractor');
const webdriverUpdatePath = require.resolve('webdriver-manager/selenium/update-config.json', {
paths: [protractorPath],
});
const webdriverUpdate = JSON.parse(await readFile(webdriverUpdatePath)) as {
chrome: { last: string };
};
const chromeDriverVersion = webdriverUpdate.chrome.last.match(/chromedriver_([\d|\.]+)/)?.[1];
if (!chromeDriverVersion) {
throw new Error('Could not extract chrome webdriver version.');
}
const chromeDriverVersion = webdriverUpdate.chrome.last.match(/chromedriver_([\d|\.]+)/)?.[1];
if (!chromeDriverVersion) {
throw new Error('Could not extract chrome webdriver version.');
}
// Initialize selenium webdriver.
// Often fails the first time so attempt twice if necessary.
const runWebdriverUpdate = () =>
exec(
process.execPath,
'node_modules/protractor/bin/webdriver-manager',
'update',
'--standalone',
'false',
'--gecko',
'false',
'--versions.chrome',
chromeDriverVersion,
);
try {
await runWebdriverUpdate();
} catch {
await runWebdriverUpdate();
// Initialize selenium webdriver.
// Often fails the first time so attempt twice if necessary.
const runWebdriverUpdate = () =>
exec(
process.execPath,
'node_modules/protractor/bin/webdriver-manager',
'update',
'--standalone',
'false',
'--gecko',
'false',
'--versions.chrome',
chromeDriverVersion,
);
try {
await runWebdriverUpdate();
} catch {
await runWebdriverUpdate();
}
}
await useCIChrome(name, 'e2e');
@ -182,42 +187,96 @@ export function useCIDefaults(projectName = 'test-project'): Promise<void> {
});
}
const KARMA_CONF_DEFAULT = `
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/$PROJECT_NAME$'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
`;
export async function useCIChrome(projectName: string, projectDir = ''): Promise<void> {
const protractorConf = path.join(projectDir, 'protractor.conf.js');
const chromePath = require('puppeteer').executablePath();
// Use Puppeteer in protractor if a config is found on the project.
if (fs.existsSync(protractorConf)) {
const protractorPath = require.resolve('protractor');
const webdriverUpdatePath = require.resolve('webdriver-manager/selenium/update-config.json', {
paths: [protractorPath],
});
const webdriverUpdate = JSON.parse(await readFile(webdriverUpdatePath)) as {
chrome: { last: string };
};
const chromeDriverPath = webdriverUpdate.chrome.last;
await replaceInFile(
protractorConf,
`browserName: 'chrome'`,
`browserName: 'chrome',
chromeOptions: {
args: ['--headless'],
binary: String.raw\`${chromePath}\`,
args: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
binary: String.raw\`${process.env.CHROME_BIN}\`,
}`,
);
await replaceInFile(
protractorConf,
'directConnect: true,',
`directConnect: true, chromeDriver: String.raw\`${chromeDriverPath}\`,`,
`directConnect: true, chromeDriver: String.raw\`${process.env.CHROMEDRIVER_BIN}\`,`,
);
}
// Use ChromeHeadless.
const karmaConf = path.join(projectDir, 'karma.conf.js');
// Create one with default config if it doesn't exist
if (!fs.existsSync(karmaConf)) {
await writeFile(karmaConf, KARMA_CONF_DEFAULT.replace('$PROJECT_NAME$', projectName));
}
// Update to use the headless sandboxed chrome
await replaceInFile(
karmaConf,
/browsers:.*\]\s*,/,
`
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
],
}
},
`,
);
return updateJsonFile('angular.json', (workspaceJson) => {
const project = workspaceJson.projects[projectName];
const appTargets = project.targets || project.architect;
appTargets.test.options.browsers = 'ChromeHeadless';
appTargets.test.options.browsers = 'ChromeHeadlessNoSandbox';
appTargets.test.options.karmaConfig = karmaConf;
});
}

View File

@ -24,8 +24,8 @@ export function wait(msecs: number): Promise<void> {
});
}
export async function mktempd(prefix: string): Promise<string> {
return realpath(await mkdtemp(path.join(tmpdir(), prefix)));
export async function mktempd(prefix: string, tempRoot?: string): Promise<string> {
return realpath(await mkdtemp(path.join(tempRoot ?? tmpdir(), prefix)));
}
export async function mockHome(cb: (home: string) => Promise<void>): Promise<void> {

View File

@ -8,10 +8,11 @@ import { getGlobalVariable, setGlobalVariable } from './e2e/utils/env';
import { gitClean } from './e2e/utils/git';
import { createNpmRegistry } from './e2e/utils/registry';
import { launchTestProcess } from './e2e/utils/process';
import { join } from 'path';
import { delimiter, dirname, join } from 'path';
import { IS_BAZEL } from './e2e/utils/bazel';
import { findFreePort } from './e2e/utils/network';
import { extractFile } from './e2e/utils/tar';
import { realpathSync } from 'fs';
import { readFileSync, realpathSync } from 'fs';
import { PkgInfo } from './e2e/utils/packages';
Error.stackTraceLimit = Infinity;
@ -61,6 +62,16 @@ const argv = yargsParser(process.argv.slice(2), {
},
default: {
'package': ['./dist/_*.tgz'],
'debug': !!process.env.BUILD_WORKSPACE_DIRECTORY,
'glob': process.env.TESTBRIDGE_TEST_ONLY,
'nb-shards':
Number(process.env.E2E_SHARD_TOTAL ?? 1) * Number(process.env.TEST_TOTAL_SHARDS ?? 1) || 1,
'shard':
process.env.E2E_SHARD_INDEX === undefined && process.env.TEST_SHARD_INDEX === undefined
? undefined
: Number(process.env.E2E_SHARD_INDEX ?? 0) * Number(process.env.TEST_TOTAL_SHARDS ?? 1) +
Number(process.env.TEST_SHARD_INDEX ?? 0),
},
});
@ -80,6 +91,20 @@ process.exitCode = 255;
*/
process.env.LEGACY_CLI_RUNNER = '1';
/**
* Add external git toolchain onto PATH
*/
if (process.env.GIT_BIN) {
process.env.PATH = process.env.PATH! + delimiter + dirname(process.env.GIT_BIN!);
}
/**
* Add external browser toolchains onto PATH
*/
if (process.env.CHROME_BIN) {
process.env.PATH = process.env.PATH! + delimiter + dirname(process.env.CHROME_BIN!);
}
const logger = createConsoleLogger(argv.verbose, process.stdout, process.stderr, {
info: (s) => s,
debug: (s) => s,
@ -93,17 +118,27 @@ function lastLogger() {
return logStack[logStack.length - 1];
}
const testGlob = argv.glob || 'tests/**/*.ts';
// Under bazel the compiled file (.js) and types (.d.ts) are available.
// Outside bazel the source .ts files are available.
const SRC_FILE_EXT = IS_BAZEL ? 'js' : 'ts';
const SRC_FILE_EXT_RE = new RegExp(`\.${SRC_FILE_EXT}$`);
const testGlob = argv.glob || `tests/**/*.${SRC_FILE_EXT}`;
const e2eRoot = path.join(__dirname, 'e2e');
const allSetups = glob.sync('setup/**/*.ts', { nodir: true, cwd: e2eRoot }).sort();
const allInitializers = glob.sync('initialize/**/*.ts', { nodir: true, cwd: e2eRoot }).sort();
const allSetups = glob.sync(`setup/**/*.${SRC_FILE_EXT}`, { nodir: true, cwd: e2eRoot }).sort();
const allInitializers = glob
.sync(`initialize/**/*.${SRC_FILE_EXT}`, { nodir: true, cwd: e2eRoot })
.sort();
const allTests = glob
.sync(testGlob, { nodir: true, cwd: e2eRoot, ignore: argv.ignore })
// Replace windows slashes.
.map((name) => name.replace(/\\/g, '/'))
.filter((name) => {
if (name.endsWith('/setup.ts')) {
if (name.endsWith(`/setup.${SRC_FILE_EXT}`)) {
return false;
}
if (!SRC_FILE_EXT_RE.test(name)) {
return false;
}
@ -122,8 +157,8 @@ const allTests = glob
})
.sort();
const shardId = 'shard' in argv ? argv['shard'] : null;
const nbShards = (shardId === null ? 1 : argv['nb-shards']) || 2;
const shardId = argv['shard'] !== undefined ? Number(argv['shard']) : null;
const nbShards = shardId === null ? 1 : Number(argv['nb-shards']);
const tests = allTests.filter((name) => {
// Check for naming tests on command line.
if (argv._.length == 0) {
@ -134,7 +169,7 @@ const tests = allTests.filter((name) => {
return (
path.join(process.cwd(), argName + '') == path.join(__dirname, 'e2e', name) ||
argName == name ||
argName == name.replace(/\.ts$/, '')
argName == name.replace(SRC_FILE_EXT_RE, '')
);
});
});
@ -143,7 +178,7 @@ const tests = allTests.filter((name) => {
const testsToRun = tests.filter((name, i) => shardId === null || i % nbShards == shardId);
if (testsToRun.length === 0) {
if (shardId !== null && tests.length >= shardId ? 1 : 0) {
if (shardId !== null && tests.length <= shardId) {
console.log(`No tests to run on shard ${shardId}, exiting.`);
process.exit(0);
} else {
@ -170,9 +205,28 @@ console.log(['Tests:', ...testsToRun].join('\n '));
setGlobalVariable('argv', argv);
setGlobalVariable('package-manager', argv.yarn ? 'yarn' : 'npm');
// This is needed by karma-chrome-launcher
// Use the chrome supplied by bazel or the puppeteer chrome and webdriver-manager driver outside.
// This is needed by karma-chrome-launcher, protractor etc.
// https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer
process.env['CHROME_BIN'] = require('puppeteer').executablePath();
//
// Resolve from relative paths to absolute paths within the bazel runfiles tree
// so subprocesses spawned in a different working directory can still find them.
process.env.CHROME_BIN = IS_BAZEL
? path.resolve(process.env.CHROME_BIN!)
: require('puppeteer').executablePath();
process.env.CHROMEDRIVER_BIN = IS_BAZEL
? path.resolve(process.env.CHROMEDRIVER_BIN!)
: (function () {
const protractorPath = require.resolve('protractor');
const webdriverUpdatePath = require.resolve('webdriver-manager/selenium/update-config.json', {
paths: [protractorPath],
});
const webdriverUpdate = JSON.parse(readFileSync(webdriverUpdatePath).toString()) as {
chrome: { last: string };
};
return webdriverUpdate.chrome.last;
})();
Promise.all([findFreePort(), findFreePort(), findPackageTars()])
.then(async ([httpPort, httpsPort, packageTars]) => {
@ -237,12 +291,12 @@ async function runSteps(
for (const [stepIndex, relativeName] of steps.entries()) {
// Make sure this is a windows compatible path.
let absoluteName = path.join(e2eRoot, relativeName).replace(/\.ts$/, '');
let absoluteName = path.join(e2eRoot, relativeName).replace(SRC_FILE_EXT_RE, '');
if (/^win/.test(process.platform)) {
absoluteName = absoluteName.replace(/\\/g, path.posix.sep);
}
const name = relativeName.replace(/\.ts$/, '');
const name = relativeName.replace(SRC_FILE_EXT_RE, '');
const start = Date.now();
printHeader(relativeName, stepIndex, steps.length, type);
@ -297,7 +351,7 @@ function printHeader(
type: 'setup' | 'initializer' | 'test',
) {
const text = `${testIndex + 1} of ${count}`;
const fullIndex = testIndex * nbShards + shardId + 1;
const fullIndex = testIndex * nbShards + (shardId ?? 0) + 1;
const shard =
shardId === null || type !== 'test'
? ''
@ -328,7 +382,16 @@ async function findPackageTars(): Promise<{ [pkg: string]: PkgInfo }> {
glob.sync(p, { realpath: true }),
);
const pkgJsons = await Promise.all(pkgs.map((pkg) => extractFile(pkg, './package/package.json')));
const pkgJsons = await Promise.all(
pkgs.map(async (pkg) => {
try {
return await extractFile(pkg, './package/package.json');
} catch (e) {
// TODO(bazel): currently the bazel npm packaging does not contain the standard npm ./package directory
return await extractFile(pkg, './package.json');
}
}),
);
return pkgs.reduce((all, pkg, i) => {
const json = pkgJsons[i].toString('utf8');

View File

@ -1,6 +1,6 @@
"""Re-export of some bazel rules with repository-wide defaults."""
load("@npm//@bazel/concatjs/internal:build_defs.bzl", _ts_library = "ts_library_macro")
load("@npm//@bazel/concatjs:index.bzl", _ts_library = "ts_library")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", _js_library = "js_library", _pkg_npm = "pkg_npm")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("@npm//@angular/build-tooling/bazel:extract_js_module_output.bzl", "extract_js_module_output")