Skip to content

Commit 5a9059b

Browse files
gregmagolanAndrewKushnir
authored andcommitted
build: share Saucelabs browsers between karma test targets using background Saucelabs daemon and custom karma launcher (angular#49200)
This upgrades the Saucelabs Bazel step on CI to use the more efficient Saucelabs daemon PR Close angular#49200
1 parent 85b4941 commit 5a9059b

25 files changed

+940
-264
lines changed

Diff for: .circleci/config.yml

+10-4
Original file line numberDiff line numberDiff line change
@@ -308,18 +308,21 @@ jobs:
308308
# container for this job. This is necessary because we launch a lot of browsers concurrently
309309
# and therefore the tunnel and Karma need to process a lot of file requests and tests.
310310
resource_class: xlarge
311+
environment:
312+
NUMBER_OF_PARALLEL_BROWSERS: 2
311313
steps:
312314
- custom_attach_workspace
313315
- init_environment
314316
- init_saucelabs_environment
317+
- run:
318+
name: Start Saucelabs daemon service
319+
command: yarn bazel run //tools/saucelabs-daemon/background-service -- ${NUMBER_OF_PARALLEL_BROWSERS}
320+
background: true
315321
- run:
316322
name: Run Bazel tests on Saucelabs
317-
# See /tools/saucelabs/README.md for more info
318323
command: |
319-
yarn bazel run //tools/saucelabs:sauce_service_setup
320324
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
321-
yarn bazel test --config=saucelabs ${TESTS}
322-
yarn bazel run //tools/saucelabs:sauce_service_stop
325+
yarn bazel test --config=saucelabs --jobs=${NUMBER_OF_PARALLEL_BROWSERS} ${TESTS}
323326
no_output_timeout: 40m
324327
- notify_webhook_on_fail:
325328
webhook_url_env_var: SLACK_DEV_INFRA_CI_FAILURES_WEBHOOK_URL
@@ -674,6 +677,9 @@ workflows:
674677
- build-npm-packages:
675678
requires:
676679
- setup
680+
- saucelabs:
681+
requires:
682+
- setup
677683
- legacy-unit-tests-saucelabs:
678684
requires:
679685
- setup

Diff for: .pullapprove.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,7 @@ groups:
11661166
'tools/legacy-saucelabs/**/{*,.*}',
11671167
'tools/rxjs/**/{*,.*}',
11681168
'tools/saucelabs/**/{*,.*}',
1169+
'tools/saucelabs-daemon/**/{*,.*}',
11691170
'tools/symbol-extractor/**/{*,.*}',
11701171
'tools/testing/**/{*,.*}',
11711172
'tools/tslint/**/{*,.*}',

Diff for: BUILD.bazel

+18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
load("//tools:defaults.bzl", "nodejs_binary")
2+
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
23
load("//:yarn.bzl", "YARN_PATH")
34

45
package(default_visibility = ["//visibility:public"])
@@ -26,6 +27,14 @@ alias(
2627
actual = "//packages:tsconfig-build.json",
2728
)
2829

30+
js_library(
31+
name = "browser-providers",
32+
srcs = [
33+
"browser-providers.conf.d.ts",
34+
"browser-providers.conf.js",
35+
],
36+
)
37+
2938
filegroup(
3039
name = "angularjs_scripts",
3140
srcs = [
@@ -61,3 +70,12 @@ nodejs_binary(
6170
"//integration:__subpackages__",
6271
],
6372
)
73+
74+
alias(
75+
name = "sauce_connect",
76+
actual = select({
77+
"@npm//@angular/build-tooling/bazel/constraints:linux_x64": "@sauce_connect_linux_amd64//:bin/sc",
78+
"@npm//@angular/build-tooling/bazel/constraints:macos_x64": "@sauce_connect_mac//:bin/sc",
79+
"@npm//@angular/build-tooling/bazel/constraints:macos_arm64": "@sauce_connect_mac//:bin/sc",
80+
}),
81+
)

Diff for: WORKSPACE

+17
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,20 @@ register_toolchains(
215215
"@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_arm64_toolchain",
216216
"@npm//@angular/build-tooling/bazel/git-toolchain:git_windows_toolchain",
217217
)
218+
219+
# Fetch sauce connect (tool to open Saucelabs tunnel for Saucelabs browser tests)
220+
http_archive(
221+
name = "sauce_connect_linux_amd64",
222+
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
223+
sha256 = "26b9c3630f441b47854b6032f7eca6f1d88d3f62e50ee44c27015d71a5155c36",
224+
strip_prefix = "sc-4.8.2-linux",
225+
url = "https://saucelabs.com/downloads/sc-4.8.2-linux.tar.gz",
226+
)
227+
228+
http_archive(
229+
name = "sauce_connect_mac",
230+
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
231+
sha256 = "28277ce81ef9ab84f5b87b526258920a8ead44789a5034346e872629bbf38089",
232+
strip_prefix = "sc-4.8.2-osx",
233+
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
234+
)

Diff for: browser-providers.conf.d.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
type CustomLauncher = {
2+
base: string;
3+
browserName: string;
4+
platformName: string;
5+
platformVersion: string;
6+
deviceName: string;
7+
appiumVersion: string;
8+
extendedDebugging: boolean;
9+
}
10+
11+
type CustomLaunchers = {
12+
[string]: CustomLauncher;
13+
};
14+
15+
type SauceAliases = {
16+
[string]: string[];
17+
};
18+
19+
export const customLaunchers: CustomLaunchers;
20+
export const sauceAliases: SauceAliases;

Diff for: karma-js.conf.js

+27-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ const {hostname} = require('os');
1212
const seed = process.env.JASMINE_RANDOM_SEED || String(Math.random()).slice(-5);
1313
console.info(`Jasmine random seed: ${seed}`);
1414

15+
const isBazel = !!process.env.TEST_TARGET;
16+
17+
if (!process.env.KARMA_WEB_TEST_MODE && isBazel && process.env.TEST_TARGET.includes('_saucelabs')) {
18+
console.info(`Saucelabs target detected: ${process.env.TEST_TARGET}`);
19+
process.env.KARMA_WEB_TEST_MODE = 'SL_REQUIRED';
20+
}
21+
1522
module.exports = function(config) {
1623
const conf = {
1724
frameworks: ['jasmine'],
@@ -50,7 +57,6 @@ module.exports = function(config) {
5057

5158
plugins: [
5259
'karma-jasmine',
53-
'karma-sauce-launcher',
5460
'karma-chrome-launcher',
5561
'karma-sourcemap-loader',
5662
],
@@ -99,20 +105,29 @@ module.exports = function(config) {
99105
set: () => {},
100106
});
101107

102-
if (process.env['SAUCE_TUNNEL_IDENTIFIER']) {
103-
console.log(`SAUCE_TUNNEL_IDENTIFIER: ${process.env.SAUCE_TUNNEL_IDENTIFIER}`);
108+
if (isBazel) {
109+
// Add the custom Saucelabs daemon to the plugins
110+
const saucelabsDaemonLauncher = require('./tools/saucelabs-daemon/launcher/index.cjs').default;
111+
conf.plugins.push(saucelabsDaemonLauncher);
112+
} else {
113+
conf.plugins.push('karma-sauce-launcher');
104114

105-
const tunnelIdentifier = process.env['SAUCE_TUNNEL_IDENTIFIER'];
115+
if (process.env['SAUCE_TUNNEL_IDENTIFIER']) {
116+
console.log(`SAUCE_TUNNEL_IDENTIFIER: ${process.env.SAUCE_TUNNEL_IDENTIFIER}`);
106117

107-
// Setup the Saucelabs plugin so that it can launch browsers using the proper tunnel.
108-
conf.sauceLabs.build = tunnelIdentifier;
109-
conf.sauceLabs.tunnelIdentifier = tunnelIdentifier;
118+
const tunnelIdentifier = process.env['SAUCE_TUNNEL_IDENTIFIER'];
110119

111-
// Patch the `saucelabs` package so that `karma-sauce-launcher` does not attempt downloading
112-
// the test logs from upstream and tries re-uploading them with the Karma enhanced details.
113-
// This slows-down tests/browser restarting and can decrease stability.
114-
// https://github.com/karma-runner/karma-sauce-launcher/blob/59b0c5c877448e064ad56449cd906743721c6b62/src/launcher/launcher.ts#L72-L79.
115-
require('saucelabs').default.prototype.downloadJobAsset = () => Promise.resolve('<FAKE-LOGS>');
120+
// Setup the Saucelabs plugin so that it can launch browsers using the proper tunnel.
121+
conf.sauceLabs.build = tunnelIdentifier;
122+
conf.sauceLabs.tunnelIdentifier = tunnelIdentifier;
123+
124+
// Patch the `saucelabs` package so that `karma-sauce-launcher` does not attempt downloading
125+
// the test logs from upstream and tries re-uploading them with the Karma enhanced details.
126+
// This slows-down tests/browser restarting and can decrease stability.
127+
// https://github.com/karma-runner/karma-sauce-launcher/blob/59b0c5c877448e064ad56449cd906743721c6b62/src/launcher/launcher.ts#L72-L79.
128+
require('saucelabs').default.prototype.downloadJobAsset = () =>
129+
Promise.resolve('<FAKE-LOGS>');
130+
}
116131
}
117132

118133
// For SauceLabs jobs, we set up a domain which resolves to the machine which launched

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@types/jasminewd2": "^2.0.8",
8989
"@types/node": "^16.11.7",
9090
"@types/selenium-webdriver": "3.0.7",
91+
"@types/selenium-webdriver4": "npm:@types/[email protected]",
9192
"@types/semver": "^7.3.4",
9293
"@types/shelljs": "^0.8.6",
9394
"@types/systemjs": "0.19.32",
@@ -135,6 +136,7 @@
135136
"rollup-plugin-sourcemaps": "^0.6.3",
136137
"rxjs": "^6.6.7",
137138
"selenium-webdriver": "3.5.0",
139+
"selenium-webdriver4": "npm:[email protected]",
138140
"semver-dsl": "^1.0.1",
139141
"shelljs": "^0.8.5",
140142
"source-map": "0.7.4",

Diff for: packages/zone.js/test/karma_test.bzl

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def karma_test(name, env_srcs, env_deps, env_entry_point, test_srcs, test_deps,
100100
configuration_env_vars = ["KARMA_WEB_TEST_MODE"],
101101
data = [
102102
"//:browser-providers.conf.js",
103+
"//tools/saucelabs-daemon/launcher:launcher_cjs",
103104
],
104105
static_files = [
105106
":assets/sample.json",

Diff for: tools/defaults.bzl

+2-4
Original file line numberDiff line numberDiff line change
@@ -321,23 +321,21 @@ def karma_web_test_suite(
321321
# Add a saucelabs target for Karma tests in `//packages/`.
322322
if native.package_name().startswith("packages/"):
323323
_karma_web_test(
324-
name = "saucelabs_%s" % name,
324+
name = "{}_saucelabs".format(name),
325325
# Default timeout is moderate (5min). This causes the test to be terminated while
326326
# Saucelabs browsers keep running. Ultimately resulting in failing tests and browsers
327327
# unnecessarily being acquired. Our specified Saucelabs idle timeout is 10min, so we use
328328
# Bazel's long timeout (15min). This ensures that Karma can shut down properly.
329329
timeout = "long",
330330
config_file = "//:karma-js.conf.js",
331331
deps = [
332-
"@npm//karma-sauce-launcher",
333332
":%s_bundle" % name,
334333
],
335334
data = data + [
336335
"//:browser-providers.conf.js",
336+
"//tools/saucelabs-daemon/launcher:launcher_cjs",
337337
],
338-
karma = "//tools/saucelabs:karma-saucelabs",
339338
tags = tags + [
340-
"exclusive",
341339
"manual",
342340
"no-remote-exec",
343341
"saucelabs",

Diff for: tools/saucelabs-daemon/BUILD.bazel

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "saucelabs-daemon",
7+
srcs = [
8+
"browser.ts",
9+
"ipc-defaults.ts",
10+
"ipc-messages.ts",
11+
],
12+
)

Diff for: tools/saucelabs-daemon/README.md

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Saucelabs testing with Bazel
2+
3+
## Local testing
4+
5+
1. Set up your `SAUCE_USERNAME`, `SAUCE_ACCESS_KEY` & `SAUCE_TUNNEL_IDENTIFIER` environment variables.
6+
These are required. You can find the values for `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` in `/.circleci/env.sh`. `SAUCE_TUNNEL_IDENTIFIER` can be set to any unique value.
7+
8+
If you are having trouble running Saucelabs tests locally you can contact [Joey Perrott](https://github.com/josephperrott) or [Paul Gschwendtner](https://github.com/devversion) for support.
9+
10+
2. Once you have your environment variables set up, run the setup task in the root of the repo:
11+
12+
``` bash
13+
yarn bazel run //tools/saucelabs-daemon/background-service -- <number_of_browsers>
14+
```
15+
16+
This will start a daemon process that will connect to Saucelabs and provision browsers
17+
once you start running your first test target.
18+
19+
3. In another terminal, you can run a particular test target through SauceLabs by suffixing the target name with "_saucelabs".
20+
21+
For example, `packages/core/test:test_web` becomes `packages/core/test:test_web_saucelabs`.
22+
23+
```
24+
yarn bazel test //packages/core/test:test_web_saucelabs
25+
```
26+
27+
## Additional test features
28+
29+
To see the test output while the tests are running (as these are long tests), add the `--test_output=streamed` option.
30+
Note, this option will also prevent bazel from using the test cache and will force the test to run.
31+
32+
For running all Saucelabs tests in the project, `bazel query` is used to gather up all karma Saucelabs test labels because they are otherwise hidden by the `manual` tag.
33+
34+
Running all karma tests in Saucelabs:
35+
36+
Start the saucelabs-daemon background service in one terminal window:
37+
38+
``` bash
39+
yarn bazel run //tools/saucelabs-daemon/background-service -- <number_of_browsers>
40+
```
41+
42+
In a second terminal window, run all of the saucelabs test targets:
43+
44+
``` bash
45+
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
46+
yarn bazel test --config=saucelabs --jobs=<number_of_browsers> ${TESTS}
47+
```
48+
49+
NB: The number of parallel Bazel tests specified by `--jobs=<number_of_browsers>` must not exceed the number parallel browsers requested when starting the daemon.
50+
51+
## Under the hood
52+
53+
The `//tools/saucelabs-daemon/background-service` target does not start the Sauce Connect proxy at start-up, but instead listens for the start signal from the saucelabs karma launcher.
54+
This signal is sent by saucelabs-daemon custom karma launcher `tools/saucelabs-daemon/launcher/launcher.ts`.
55+
This is necessary as the Sauce Connect Proxy process must be started outside of `bazel test` as Bazel will automatically kill any processes spawned during a test when that tests completes, which would prevent the tunnel & provisioned browsers from being shared by multiple tests.
56+
57+
The karma_web_test rule for saucelabs must have a few important tags:
58+
59+
* `no-remote-exec` as they cannot be executed remotely since tests need to communicate with the daemon.
60+
* `manual` so they are not automatically tested with `//...`
61+
* `saucelabs` so that they can be easily gathered up for testing in a `bazel query`
62+
63+
These are added automatically the by `karma_web_test_suite` macro in `tools/defaults.bzl`.
64+
65+
## Debugging
66+
67+
**Q: How do I get the tests to run on IE? I only see Chromium.**
68+
69+
If you see something like this at the end of your test output, it means you're not actually running SauceLabs:
70+
71+
```
72+
INFO: Build completed successfully, 43 total actions
73+
/packages/core/test:test_web_chromium
74+
```
75+
76+
This is a common error caused by forgetting to suffix your test target with "_saucelabs".
77+
78+
For example, `/packages/core/test:test_web` becomes `/packages/core/test:test_web_saucelabs`.
79+
80+
**Q: How can I tell that the SauceLabs connection was successfully made?**
81+
82+
There is a dashboard at saucelabs.com where you can see active tunnel connections (Angular has an account).
83+
As soon as you actually run the test target (not after the setup task), you should see an active tunnel connection under the SAUCE_TUNNEL_IDENTIFICATION_KEY you entered.
84+
If a tunnel connection is not there, you are not actually connecting with SauceLabs.
85+
86+
Note: It may *look* like the tests are running because of the Bazel output.
87+
The progress Bazel is showing does not mean that SauceLabs is connected.
88+
If the tests are actually running, you should see the "..." test report for passing tests.
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
load("//tools:defaults.bzl", "nodejs_binary", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "background-service_lib",
7+
srcs = glob(["*.ts"]),
8+
deps = [
9+
"//:browser-providers",
10+
"//tools/saucelabs-daemon",
11+
"@npm//@types/node",
12+
"@npm//@types/selenium-webdriver4",
13+
"@npm//chalk",
14+
"@npm//selenium-webdriver4",
15+
],
16+
)
17+
18+
nodejs_binary(
19+
name = "background-service",
20+
data = [
21+
":background-service_lib",
22+
"//:sauce_connect",
23+
],
24+
entry_point = ":cli.ts",
25+
templated_args = [
26+
"$(rootpath //:sauce_connect)",
27+
],
28+
)

0 commit comments

Comments
 (0)