Skip to content

Commit e20a29a

Browse files
devversionkara
authored andcommitted
build(docs-infra): support running cli docs examples concurrently (angular#29103)
PR Close angular#29103
1 parent 76119b8 commit e20a29a

File tree

7 files changed

+86
-40
lines changed

7 files changed

+86
-40
lines changed

aio/content/examples/component-interaction/example-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"cmd": "yarn",
55
"args": [
66
"e2e",
7-
"--no-webdriver-update"
7+
"--no-webdriver-update",
8+
"--port={PORT}"
89
]
910
}
1011
]

aio/content/examples/dependency-injection/example-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"cmd": "yarn",
55
"args": [
66
"e2e",
7-
"--no-webdriver-update"
7+
"--no-webdriver-update",
8+
"--port={PORT}"
89
]
910
}
1011
]

aio/content/examples/i18n/example-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"cmd": "yarn",
66
"args": [
77
"e2e",
8-
"--no-webdriver-update"
8+
"--no-webdriver-update",
9+
"--port={PORT}"
910
]
1011
}
1112
]

aio/content/examples/service-worker-getting-started/example-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"projectType": "service-worker",
33
"e2e": [
4-
{"cmd": "yarn", "args": ["e2e", "--no-webdriver-update"]},
4+
{"cmd": "yarn", "args": ["e2e", "--no-webdriver-update", "--port={PORT}"]},
55
{"cmd": "yarn", "args": ["build", "--prod"]},
66
{"cmd": "node", "args": ["--eval", "assert(fs.existsSync('./dist/ngsw.json'), 'ngsw.json is missing')"]},
77
{"cmd": "node", "args": ["--eval", "assert(fs.existsSync('./dist/ngsw-worker.js'), 'ngsw-worker.js is missing')"]},

aio/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"entities": "^1.1.1",
119119
"eslint": "^3.19.0",
120120
"eslint-plugin-jasmine": "^2.2.0",
121+
"find-free-port": "^2.0.0",
121122
"firebase-tools": "^5.1.1",
122123
"fs-extra": "^2.1.2",
123124
"globby": "^6.1.0",

aio/tools/examples/run-example-e2e.js

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const globby = require('globby');
55
const xSpawn = require('cross-spawn');
66
const treeKill = require('tree-kill');
77
const shelljs = require('shelljs');
8+
const findFreePort = require('find-free-port');
89

910
shelljs.set('-e');
1011

@@ -15,6 +16,8 @@ const PROTRACTOR_CONFIG_FILENAME = path.join(__dirname, './shared/protractor.con
1516
const SJS_SPEC_FILENAME = 'e2e-spec.ts';
1617
const CLI_SPEC_FILENAME = 'e2e/src/app.e2e-spec.ts';
1718
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
19+
const DEFAULT_CLI_EXAMPLE_PORT = 4200;
20+
const DEFAULT_CLI_SPECS_CONCURRENCY = 1;
1821
const IGNORED_EXAMPLES = [
1922
// temporary ignores
2023

@@ -51,6 +54,9 @@ if (argv.ivy) {
5154
* e.g. --shard=0/2 // the even specs: 0, 2, 4, etc
5255
* e.g. --shard=1/2 // the odd specs: 1, 3, 5, etc
5356
* e.g. --shard=1/3 // the second of every three specs: 1, 4, 7, etc
57+
*
58+
* --cliSpecsConcurrency Amount of CLI example specs that should be executed concurrently.
59+
* By default runs specs sequentially.
5460
*/
5561
function runE2e() {
5662
if (argv.setup) {
@@ -65,7 +71,8 @@ function runE2e() {
6571
const outputFile = path.join(AIO_PATH, './protractor-results.txt');
6672

6773
return Promise.resolve()
68-
.then(() => findAndRunE2eTests(argv.filter, outputFile, argv.shard))
74+
.then(() => findAndRunE2eTests(argv.filter, outputFile, argv.shard,
75+
argv.cliSpecsConcurrency || DEFAULT_CLI_SPECS_CONCURRENCY))
6976
.then((status) => {
7077
reportStatus(status, outputFile);
7178
if (status.failed.length > 0) {
@@ -80,7 +87,7 @@ function runE2e() {
8087

8188
// Finds all of the *e2e-spec.tests under the examples folder along with the corresponding apps
8289
// that they should run under. Then run each app/spec collection sequentially.
83-
function findAndRunE2eTests(filter, outputFile, shard) {
90+
function findAndRunE2eTests(filter, outputFile, shard, cliSpecsConcurrency) {
8491
const shardParts = shard ? shard.split('/') : [0, 1];
8592
const shardModulo = parseInt(shardParts[0], 10);
8693
const shardDivider = parseInt(shardParts[1], 10);
@@ -91,8 +98,12 @@ function findAndRunE2eTests(filter, outputFile, shard) {
9198
header += ` Filter: ${filter ? filter : 'All tests'}\n\n`;
9299
fs.writeFileSync(outputFile, header);
93100

94-
// Run the tests sequentially.
95101
const status = {passed: [], failed: []};
102+
const updateStatus = (specPath, passed) => {
103+
const arr = passed ? status.passed : status.failed;
104+
arr.push(specPath);
105+
};
106+
96107
return getE2eSpecs(EXAMPLES_PATH, filter)
97108
.then(e2eSpecPaths => {
98109
console.log('All e2e specs:');
@@ -111,22 +122,29 @@ function findAndRunE2eTests(filter, outputFile, shard) {
111122
(promise, specPath) => {
112123
return promise.then(() => {
113124
const examplePath = path.dirname(specPath);
114-
return runE2eTestsSystemJS(examplePath, outputFile).then(ok => {
115-
const arr = ok ? status.passed : status.failed;
116-
arr.push(examplePath);
117-
});
125+
return runE2eTestsSystemJS(examplePath, outputFile)
126+
.then(passed => updateStatus(examplePath, passed));
118127
});
119128
},
120129
Promise.resolve())
121-
.then(() => {
122-
return e2eSpecPaths.cli.reduce((promise, specPath) => {
123-
return promise.then(() => {
124-
return runE2eTestsCLI(specPath, outputFile).then(ok => {
125-
const arr = ok ? status.passed : status.failed;
126-
arr.push(specPath);
127-
});
128-
});
129-
}, Promise.resolve());
130+
.then(async () => {
131+
const specQueue = [...e2eSpecPaths.cli];
132+
// Determine free ports for the amount of pending CLI specs before starting
133+
// any tests. This is necessary because ports can stuck in the "TIME_WAIT"
134+
// state after others specs which used that port exited. This works around
135+
// this potential race condition which surfaces on Windows.
136+
const ports = await findFreePort(4000, 6000, '127.0.0.1', specQueue.length);
137+
// Enable buffering of the process output in case multiple CLI specs will
138+
// be executed concurrently. This means that we can can print out the full
139+
// output at once without interfering with other CLI specs printing as well.
140+
const bufferOutput = cliSpecsConcurrency > 1;
141+
while (specQueue.length) {
142+
const chunk = specQueue.splice(0, cliSpecsConcurrency);
143+
await Promise.all(chunk.map((testDir, index) => {
144+
return runE2eTestsCLI(testDir, outputFile, bufferOutput, ports.pop())
145+
.then(passed => updateStatus(testDir, passed));
146+
}));
147+
}
130148
});
131149
})
132150
.then(() => {
@@ -218,30 +236,46 @@ function runProtractorAoT(appDir, outputFile) {
218236
// fileName; then shut down the example.
219237
// All protractor output is appended to the outputFile.
220238
// CLI version
221-
function runE2eTestsCLI(appDir, outputFile) {
222-
console.log(`\n\n=========== Running aio example tests for: ${appDir}`);
239+
function runE2eTestsCLI(appDir, outputFile, bufferOutput, port) {
240+
if (!bufferOutput) {
241+
console.log(`\n\n=========== Running aio example tests for: ${appDir}`);
242+
}
243+
223244
// `--no-webdriver-update` is needed to preserve the ChromeDriver version already installed.
224245
const config = loadExampleConfig(appDir);
225-
const commands = config.e2e || [{cmd: 'yarn', args: ['e2e', '--prod', '--no-webdriver-update']}];
246+
const commands = config.e2e || [{
247+
cmd: 'yarn',
248+
args: ['e2e', '--prod', '--no-webdriver-update', `--port=${port || DEFAULT_CLI_EXAMPLE_PORT}`]
249+
}];
250+
let bufferedOutput = `\n\n============== AIO example output for: ${appDir}\n\n`;
226251

227252
const e2eSpawnPromise = commands.reduce((prevSpawnPromise, {cmd, args}) => {
253+
// Replace the port placeholder with the specified port if present. Specs that
254+
// define their e2e test commands in the example config are able to use the
255+
// given available port. This ensures that the CLI tests can be run concurrently.
256+
args = args.map(a => a.replace('{PORT}', port || DEFAULT_CLI_EXAMPLE_PORT));
257+
228258
return prevSpawnPromise.then(() => {
229-
const currSpawn = spawnExt(cmd, args, {cwd: appDir});
259+
const currSpawn = spawnExt(cmd, args, {cwd: appDir}, false,
260+
bufferOutput ? msg => bufferedOutput += msg : undefined);
230261
return currSpawn.promise.then(
231262
() => Promise.resolve(finish(currSpawn.proc.pid, true)),
232263
() => Promise.reject(finish(currSpawn.proc.pid, false)));
233264
});
234265
}, Promise.resolve());
235266

236-
return e2eSpawnPromise.then(
237-
() => {
238-
fs.appendFileSync(outputFile, `Passed: ${appDir}\n\n`);
239-
return true;
240-
},
241-
() => {
242-
fs.appendFileSync(outputFile, `Failed: ${appDir}\n\n`);
243-
return false;
244-
});
267+
return e2eSpawnPromise.then(() => {
268+
fs.appendFileSync(outputFile, `Passed: ${appDir}\n\n`);
269+
return true;
270+
}, () => {
271+
fs.appendFileSync(outputFile, `Failed: ${appDir}\n\n`);
272+
return false;
273+
}).then(passed => {
274+
if (bufferOutput) {
275+
process.stdout.write(bufferedOutput);
276+
}
277+
return passed;
278+
});
245279
}
246280

247281
// Report final status.
@@ -275,29 +309,32 @@ function reportStatus(status, outputFile) {
275309
}
276310

277311
// Returns both a promise and the spawned process so that it can be killed if needed.
278-
function spawnExt(command, args, options, ignoreClose = false) {
312+
function spawnExt(command, args, options, ignoreClose = false,
313+
printMessage = msg => process.stdout.write(msg)) {
279314
let proc;
280315
const promise = new Promise((resolve, reject) => {
281316
let descr = command + ' ' + args.join(' ');
282-
console.log('running: ' + descr);
317+
let processOutput = '';
318+
printMessage(`running: ${descr}\n`);
283319
try {
284320
proc = xSpawn.spawn(command, args, options);
285321
} catch (e) {
286322
console.log(e);
287323
reject(e);
288324
return {proc: null, promise};
289325
}
290-
proc.stdout.on('data', function(data) { process.stdout.write(data.toString()); });
291-
proc.stderr.on('data', function(data) { process.stdout.write(data.toString()); });
326+
proc.stdout.on('data', printMessage);
327+
proc.stderr.on('data', printMessage);
328+
292329
proc.on('close', function(returnCode) {
293-
console.log(`completed: ${descr} \n`);
330+
printMessage(`completed: ${descr}\n\n`);
294331
// Many tasks (e.g., tsc) complete but are actually errors;
295332
// Confirm return code is zero.
296333
returnCode === 0 || ignoreClose ? resolve(0) : reject(returnCode);
297334
});
298335
proc.on('error', function(data) {
299-
console.log(`completed with error: ${descr} \n`);
300-
console.log(data.toString());
336+
printMessage(`completed with error: ${descr}\n\n`);
337+
printMessage(`${data.toString()}\n`);
301338
reject(data);
302339
});
303340
});

aio/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3760,6 +3760,11 @@ find-cache-dir@^2.0.0:
37603760
make-dir "^1.0.0"
37613761
pkg-dir "^3.0.0"
37623762

3763+
find-free-port@^2.0.0:
3764+
version "2.0.0"
3765+
resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-2.0.0.tgz#4b22e5f6579eb1a38c41ac6bcb3efed1b6da9b1b"
3766+
integrity sha1-SyLl9leesaOMQaxryz7+0bbamxs=
3767+
37633768
find-up@^1.0.0:
37643769
version "1.1.2"
37653770
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"

0 commit comments

Comments
 (0)