Skip to content

Commit 462047e

Browse files
authored
Separate Saucelabs cross-browser tests by package (firebase#1756)
Set up Saucelabs tests to run separately, on CI or locally.
1 parent 9f6dbc8 commit 462047e

File tree

7 files changed

+240
-63
lines changed

7 files changed

+240
-63
lines changed

.travis.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,15 @@ matrix:
4242
jobs:
4343
allow_failures:
4444
- script: yarn test:saucelabs
45-
- script: yarn test:saucelabs --database-firestore-only
4645
include:
4746
- name: Node.js and Browser (Chrome) Test
4847
stage: test
4948
script: xvfb-run yarn test
5049
after_success: yarn test:coverage
51-
- name: Cross Browser Test for SDKs except Database and Firestore
50+
- name: Cross Browser Test for SDKs
5251
stage: test
5352
script: yarn test:saucelabs
5453
if: type = push
55-
- name: Cross Browser Test for Database and Firestore SDK
56-
stage: test
57-
script: yarn test:saucelabs --database-firestore-only
58-
if: type = push
5954
- stage: deploy
6055
script: skip
6156
# NPM Canary Build Config

config/karma.saucelabs.js

Lines changed: 98 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,98 @@
1616
*/
1717

1818
const argv = require('yargs').argv;
19-
const glob = require('glob');
20-
const karma = require('karma');
2119
const path = require('path');
2220
const karmaBase = require('./karma.base');
2321

22+
// karma.conf.js test configuration file to run.
23+
const testConfigFile = argv['testConfigFile'];
24+
if (!testConfigFile) {
25+
console.error('No test file path provided.');
26+
process.exit(1);
27+
}
28+
29+
/**
30+
* Custom SauceLabs Launchers
31+
*/
32+
const browserMap = {
33+
// Desktop
34+
Chrome_Windows: seleniumLauncher('chrome', 'Windows 10', 'latest'),
35+
Firefox_Windows: seleniumLauncher('firefox', 'Windows 10', 'latest'),
36+
Safari_macOS: seleniumLauncher('safari', 'macOS 10.13', 'latest'),
37+
Edge_Windows: seleniumLauncher('MicrosoftEdge', 'Windows 10', 'latest'),
38+
IE_Windows: seleniumLauncher('internet explorer', 'Windows 10', 'latest')
39+
40+
// Mobile
41+
// Safari_iOS: appiumLauncher('Safari', 'iPhone Simulator', 'iOS', '11.2'),
42+
// Chrome_Android: appiumLauncher('Chrome', 'Android Emulator', 'Android', '6.0')
43+
};
44+
45+
/**
46+
* Any special options per package.
47+
*/
48+
const packageConfigs = {
49+
messaging: {
50+
// Messaging currently only supports these browsers.
51+
browsers: ['Chrome_Windows', 'Firefox_Windows', 'Edge_Windows']
52+
}
53+
};
54+
55+
/**
56+
* Gets the browser/launcher map for this package.
57+
*
58+
* @param {string} packageName Name of package being tested (e.g., "firestore")
59+
*/
60+
function getSauceLabsBrowsers(packageName) {
61+
if (packageConfigs[packageName]) {
62+
const filteredBrowserMap = {};
63+
for (const browserKey in browserMap) {
64+
if (packageConfigs[packageName].browsers.includes(browserKey)) {
65+
filteredBrowserMap[browserKey] = browserMap[browserKey];
66+
}
67+
}
68+
return filteredBrowserMap;
69+
} else {
70+
return browserMap;
71+
}
72+
}
73+
74+
/**
75+
* Get package name from package path command line arg.
76+
*/
77+
function getPackageLabels() {
78+
const match = testConfigFile.match(
79+
/([a-zA-Z]+)\/([a-zA-Z]+)\/karma\.conf\.js/
80+
);
81+
return {
82+
type: match[1],
83+
name: match[2]
84+
};
85+
}
86+
2487
/**
2588
* Gets a list of file patterns for test, defined individually
2689
* in karma.conf.js in each package under worksapce packages or
2790
* integration.
2891
*/
2992
function getTestFiles() {
3093
let root = path.resolve(__dirname, '..');
31-
configs = argv['database-firestore-only']
32-
? glob.sync('packages/{database,firestore}/karma.conf.js')
33-
: glob.sync('{packages,integration}/*/karma.conf.js', {
34-
// Excluded due to flakiness or long run time.
35-
ignore: [
36-
'packages/database/*',
37-
'packages/firestore/*',
38-
'integration/firestore/*',
39-
'integration/messaging/*'
40-
]
41-
});
42-
files = configs.map(x => {
43-
let patterns = require(path.join(root, x)).files;
44-
let dirname = path.dirname(x);
45-
return patterns.map(p => path.join(dirname, p));
46-
});
47-
return [].concat(...files);
94+
const { name: packageName } = getPackageLabels();
95+
let patterns = require(path.join(root, testConfigFile)).files;
96+
let dirname = path.dirname(testConfigFile);
97+
return { packageName, files: patterns.map(p => path.join(dirname, p)) };
4898
}
4999

50100
function seleniumLauncher(browserName, platform, version) {
101+
const { name, type } = getPackageLabels();
102+
const testName =
103+
type === 'integration'
104+
? `${type}-${name}-${browserName}`
105+
: `${name}-${browserName}`;
51106
return {
52107
base: 'SauceLabs',
53108
browserName: browserName,
54109
extendedDebugging: 'true',
110+
name: testName,
55111
recordLogs: 'true',
56112
recordVideo: 'true',
57113
recordScreenshots: 'true',
@@ -81,27 +137,32 @@ function appiumLauncher(
81137
};
82138
}
83139

84-
/**
85-
* Custom SauceLabs Launchers
86-
*/
87-
const sauceLabsBrowsers = {
88-
// Desktop
89-
Chrome_Windows: seleniumLauncher('chrome', 'Windows 10', 'latest'),
90-
Firefox_Windows: seleniumLauncher('firefox', 'Windows 10', 'latest'),
91-
Safari_macOS: seleniumLauncher('safari', 'macOS 10.13', 'latest'),
92-
Edge_Windows: seleniumLauncher('MicrosoftEdge', 'Windows 10', 'latest'),
93-
IE_Windows: seleniumLauncher('internet explorer', 'Windows 10', 'latest')
94-
95-
// Mobile
96-
// Safari_iOS: appiumLauncher('Safari', 'iPhone Simulator', 'iOS', '11.2'),
97-
// Chrome_Android: appiumLauncher('Chrome', 'Android Emulator', 'Android', '6.0')
98-
};
99-
100140
module.exports = function(config) {
141+
const { packageName, files: testFiles } = getTestFiles();
142+
const sauceLabsBrowsers = getSauceLabsBrowsers(packageName);
143+
144+
const sauceLabsConfig = {
145+
tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER + '-' + packageName,
146+
build: process.env.TRAVIS_BUILD_NUMBER || argv['buildNumber'],
147+
username: process.env.SAUCE_USERNAME,
148+
accessKey: process.env.SAUCE_ACCESS_KEY,
149+
startConnect: true,
150+
connectOptions: {
151+
// Realtime Database uses WebSockets to connect to firebaseio.com
152+
// so we have to set noSslBumpDomains. Theoretically SSL Bumping
153+
// only needs to be disabled for 'firebaseio.com'. However, we are
154+
// seeing much longer test time with that configuration, so leave
155+
// it as 'all' for now.
156+
// See https://wiki.saucelabs.com/display/DOCS/Troubleshooting+Sauce+Connect
157+
// for more details.
158+
noSslBumpDomains: 'all'
159+
}
160+
};
161+
101162
const karmaConfig = Object.assign({}, karmaBase, {
102163
basePath: '../',
103164

104-
files: ['packages/polyfill/index.ts', ...getTestFiles()],
165+
files: ['packages/polyfill/index.ts', ...testFiles],
105166

106167
logLevel: config.LOG_INFO,
107168

@@ -151,22 +212,7 @@ module.exports = function(config) {
151212
overviewColumn: false
152213
},
153214

154-
sauceLabs: {
155-
tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER,
156-
username: process.env.SAUCE_USERNAME,
157-
accessKey: process.env.SAUCE_ACCESS_KEY,
158-
startConnect: true,
159-
connectOptions: {
160-
// Realtime Database uses WebSockets to connect to firebaseio.com
161-
// so we have to set noSslBumpDomains. Theoretically SSL Bumping
162-
// only needs to be disabled for 'firebaseio.com'. However, we are
163-
// seeing much longer test time with that configuration, so leave
164-
// it as 'all' for now.
165-
// See https://wiki.saucelabs.com/display/DOCS/Troubleshooting+Sauce+Connect
166-
// for more details.
167-
noSslBumpDomains: 'all'
168-
}
169-
}
215+
sauceLabs: sauceLabsConfig
170216
});
171217

172218
config.set(karmaConfig);

config/webpack.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ module.exports = {
3030
loader: 'ts-loader',
3131
options: {
3232
compilerOptions: {
33-
module: 'commonjs'
33+
module: 'commonjs',
34+
downlevelIteration: true
3435
}
3536
}
3637
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"pretest:coverage": "mkdirp coverage",
3030
"test:coverage": "lcov-result-merger 'packages/**/lcov.info' | coveralls",
3131
"test:setup": "node tools/config.js",
32-
"pretest:saucelabs": "lerna run --parallel pretest",
33-
"test:saucelabs": "karma start config/karma.saucelabs.js --single-run",
32+
"test:saucelabs": "node scripts/run_saucelabs.js",
33+
"test:saucelabs:single": "karma start config/karma.saucelabs.js --single-run",
3434
"docgen:js": "node scripts/docgen/generate-docs.js --api js",
3535
"docgen:node": "node scripts/docgen/generate-docs.js --api node",
3636
"docgen": "yarn docgen:js; yarn docgen:node",

packages/installations/karma.conf.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717

1818
const karmaBase = require('../../config/karma.base');
1919

20+
const files = ['src/**/*.test.ts'];
21+
2022
module.exports = function(config) {
2123
config.set({
2224
...karmaBase,
23-
files: ['src/**/*.test.ts'],
25+
files,
2426
preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] },
2527
frameworks: ['mocha']
2628
});
2729
};
30+
31+
module.exports.files = files;

packages/performance/karma.conf.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616

1717
const karmaBase = require('../../config/karma.base');
1818

19+
const files = [`test/**/*`, 'src/**/*.test.ts'];
20+
1921
module.exports = function(config) {
2022
config.set({
2123
...karmaBase,
2224
// files to load into karma
23-
files: [`test/**/*`, 'src/**/*.test.ts'],
25+
files,
2426
preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] },
2527
// frameworks to use
2628
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
2729
frameworks: ['mocha']
2830
});
2931
};
32+
33+
module.exports.files = files;

scripts/run_saucelabs.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
const { spawn } = require('child-process-promise');
19+
const { exists } = require('mz/fs');
20+
const yargs = require('yargs');
21+
const glob = require('glob');
22+
const path = require('path');
23+
24+
// Check for 'configFiles' flag to run on specified karma.conf.js files instead
25+
// of on all files.
26+
const { configFiles } = yargs
27+
.option('configFiles', {
28+
default: [],
29+
describe: 'specify individual karma.conf.js files to run on',
30+
type: 'array'
31+
})
32+
.version(false)
33+
.help().argv;
34+
35+
// Get all karma.conf.js files that need to be run.
36+
// runNextTest() pulls filenames one-by-one from this queue.
37+
const testFiles = configFiles.length
38+
? configFiles
39+
: glob
40+
.sync(`{packages,integration}/*/karma.conf.js`)
41+
// Automated tests in integration/firestore are currently disabled.
42+
.filter(name => !name.includes('integration/firestore'));
43+
44+
// Get CI build number or generate one if running locally.
45+
const buildNumber =
46+
process.env.TRAVIS_BUILD_NUMBER ||
47+
`local_${process.env.USER}_${new Date().getTime()}`;
48+
49+
/**
50+
* Runs a set of SauceLabs browser tests based on this karma config file.
51+
*
52+
* @param {string} testFile Path to karma.conf.js file that defines this test
53+
* group.
54+
*/
55+
async function runTest(testFile) {
56+
// Run pretest if this dir has a package.json with a pretest script.
57+
const testFileDir =
58+
path.resolve(__dirname, '../') + '/' + path.dirname(testFile);
59+
const pkgPath = testFileDir + '/package.json';
60+
if (await exists(pkgPath)) {
61+
const pkg = require(pkgPath);
62+
if (pkg.scripts.pretest) {
63+
await spawn('yarn', ['--cwd', testFileDir, 'pretest'], {
64+
stdio: 'inherit'
65+
});
66+
}
67+
}
68+
69+
const promise = spawn('yarn', [
70+
'test:saucelabs:single',
71+
'--testConfigFile',
72+
testFile,
73+
'--buildNumber',
74+
buildNumber
75+
]);
76+
const childProcess = promise.childProcess;
77+
let exitCode = 0;
78+
79+
childProcess.stdout.on('data', data => {
80+
console.log(`[${testFile}]:`, data.toString());
81+
});
82+
83+
// Lerna's normal output goes to stderr for some reason.
84+
childProcess.stderr.on('data', data => {
85+
console.log(`[${testFile}]:`, data.toString());
86+
});
87+
88+
// Capture exit code of this single package test run
89+
childProcess.on('exit', code => {
90+
exitCode = code;
91+
});
92+
93+
return promise
94+
.then(() => {
95+
console.log(`[${testFile}] ******* DONE *******`);
96+
return exitCode;
97+
})
98+
.catch(err => {
99+
console.error(`[${testFile}] ERROR:`, err.message);
100+
return exitCode;
101+
});
102+
}
103+
104+
/**
105+
* Runs next file in testFiles queue as long as there are files in the queue.
106+
*
107+
* @param {number} maxExitCode Current highest exit code from all processes
108+
* run so far. When main process is complete, it will exit with highest code
109+
* of all child processes. This allows any failing test to result in a CI
110+
* build failure for the whole Saucelabs run.
111+
*/
112+
async function runNextTest(maxExitCode = 0) {
113+
// When test queue is empty, exit with code 0 if no tests failed or
114+
// 1 if any tests failed.
115+
if (!testFiles.length) process.exit(maxExitCode);
116+
const nextFile = testFiles.shift();
117+
let exitCode;
118+
try {
119+
exitCode = await runTest(nextFile);
120+
} catch (e) {
121+
console.error(`[${nextFile}] ERROR:`, e.message);
122+
exitCode = 1;
123+
}
124+
runNextTest(Math.max(exitCode, maxExitCode));
125+
}
126+
127+
runNextTest();

0 commit comments

Comments
 (0)