@@ -5,6 +5,7 @@ const globby = require('globby');
5
5
const xSpawn = require ( 'cross-spawn' ) ;
6
6
const treeKill = require ( 'tree-kill' ) ;
7
7
const shelljs = require ( 'shelljs' ) ;
8
+ const findFreePort = require ( 'find-free-port' ) ;
8
9
9
10
shelljs . set ( '-e' ) ;
10
11
@@ -15,6 +16,8 @@ const PROTRACTOR_CONFIG_FILENAME = path.join(__dirname, './shared/protractor.con
15
16
const SJS_SPEC_FILENAME = 'e2e-spec.ts' ;
16
17
const CLI_SPEC_FILENAME = 'e2e/src/app.e2e-spec.ts' ;
17
18
const EXAMPLE_CONFIG_FILENAME = 'example-config.json' ;
19
+ const DEFAULT_CLI_EXAMPLE_PORT = 4200 ;
20
+ const DEFAULT_CLI_SPECS_CONCURRENCY = 1 ;
18
21
const IGNORED_EXAMPLES = [
19
22
// temporary ignores
20
23
@@ -51,6 +54,9 @@ if (argv.ivy) {
51
54
* e.g. --shard=0/2 // the even specs: 0, 2, 4, etc
52
55
* e.g. --shard=1/2 // the odd specs: 1, 3, 5, etc
53
56
* 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.
54
60
*/
55
61
function runE2e ( ) {
56
62
if ( argv . setup ) {
@@ -65,7 +71,8 @@ function runE2e() {
65
71
const outputFile = path . join ( AIO_PATH , './protractor-results.txt' ) ;
66
72
67
73
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 ) )
69
76
. then ( ( status ) => {
70
77
reportStatus ( status , outputFile ) ;
71
78
if ( status . failed . length > 0 ) {
@@ -80,7 +87,7 @@ function runE2e() {
80
87
81
88
// Finds all of the *e2e-spec.tests under the examples folder along with the corresponding apps
82
89
// 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 ) {
84
91
const shardParts = shard ? shard . split ( '/' ) : [ 0 , 1 ] ;
85
92
const shardModulo = parseInt ( shardParts [ 0 ] , 10 ) ;
86
93
const shardDivider = parseInt ( shardParts [ 1 ] , 10 ) ;
@@ -91,8 +98,12 @@ function findAndRunE2eTests(filter, outputFile, shard) {
91
98
header += ` Filter: ${ filter ? filter : 'All tests' } \n\n` ;
92
99
fs . writeFileSync ( outputFile , header ) ;
93
100
94
- // Run the tests sequentially.
95
101
const status = { passed : [ ] , failed : [ ] } ;
102
+ const updateStatus = ( specPath , passed ) => {
103
+ const arr = passed ? status . passed : status . failed ;
104
+ arr . push ( specPath ) ;
105
+ } ;
106
+
96
107
return getE2eSpecs ( EXAMPLES_PATH , filter )
97
108
. then ( e2eSpecPaths => {
98
109
console . log ( 'All e2e specs:' ) ;
@@ -111,22 +122,29 @@ function findAndRunE2eTests(filter, outputFile, shard) {
111
122
( promise , specPath ) => {
112
123
return promise . then ( ( ) => {
113
124
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 ) ) ;
118
127
} ) ;
119
128
} ,
120
129
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
+ }
130
148
} ) ;
131
149
} )
132
150
. then ( ( ) => {
@@ -218,30 +236,46 @@ function runProtractorAoT(appDir, outputFile) {
218
236
// fileName; then shut down the example.
219
237
// All protractor output is appended to the outputFile.
220
238
// 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
+
223
244
// `--no-webdriver-update` is needed to preserve the ChromeDriver version already installed.
224
245
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` ;
226
251
227
252
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
+
228
258
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 ) ;
230
261
return currSpawn . promise . then (
231
262
( ) => Promise . resolve ( finish ( currSpawn . proc . pid , true ) ) ,
232
263
( ) => Promise . reject ( finish ( currSpawn . proc . pid , false ) ) ) ;
233
264
} ) ;
234
265
} , Promise . resolve ( ) ) ;
235
266
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
+ } ) ;
245
279
}
246
280
247
281
// Report final status.
@@ -275,29 +309,32 @@ function reportStatus(status, outputFile) {
275
309
}
276
310
277
311
// 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 ) ) {
279
314
let proc ;
280
315
const promise = new Promise ( ( resolve , reject ) => {
281
316
let descr = command + ' ' + args . join ( ' ' ) ;
282
- console . log ( 'running: ' + descr ) ;
317
+ let processOutput = '' ;
318
+ printMessage ( `running: ${ descr } \n` ) ;
283
319
try {
284
320
proc = xSpawn . spawn ( command , args , options ) ;
285
321
} catch ( e ) {
286
322
console . log ( e ) ;
287
323
reject ( e ) ;
288
324
return { proc : null , promise} ;
289
325
}
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
+
292
329
proc . on ( 'close' , function ( returnCode ) {
293
- console . log ( `completed: ${ descr } \n` ) ;
330
+ printMessage ( `completed: ${ descr } \n \n` ) ;
294
331
// Many tasks (e.g., tsc) complete but are actually errors;
295
332
// Confirm return code is zero.
296
333
returnCode === 0 || ignoreClose ? resolve ( 0 ) : reject ( returnCode ) ;
297
334
} ) ;
298
335
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` ) ;
301
338
reject ( data ) ;
302
339
} ) ;
303
340
} ) ;
0 commit comments