2019-11-18 18:18:28 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
2020-03-29 21:38:30 -07:00
* Modifications copyright ( c ) Microsoft Corporation .
2019-11-18 18:18:28 -08:00
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2020-01-08 16:16:54 +00:00
const { SourceMapSupport } = require ( './SourceMapSupport' ) ;
2020-02-14 15:21:08 -08:00
const debug = require ( 'debug' ) ;
2020-03-29 21:38:30 -07:00
const Location = require ( './Location' ) ;
2019-12-19 15:47:35 -08:00
2020-03-25 22:42:09 -07:00
const INFINITE _TIMEOUT = 100000000 ;
2019-11-18 18:18:28 -08:00
const TimeoutError = new Error ( 'Timeout' ) ;
const TerminatedError = new Error ( 'Terminated' ) ;
2020-03-26 14:43:28 -07:00
function runUserCallback ( callback , timeout , args ) {
let terminateCallback ;
let timeoutId ;
const promise = Promise . race ( [
Promise . resolve ( ) . then ( callback . bind ( null , ... args ) ) . then ( ( ) => null ) . catch ( e => e ) ,
new Promise ( resolve => {
timeoutId = setTimeout ( resolve . bind ( null , TimeoutError ) , timeout ) ;
} ) ,
new Promise ( resolve => terminateCallback = resolve ) ,
] ) . catch ( e => e ) . finally ( ( ) => clearTimeout ( timeoutId ) ) ;
const terminate = ( ) => terminateCallback ( TerminatedError ) ;
return { promise , terminate } ;
2019-11-18 18:18:28 -08:00
}
2020-03-15 23:10:49 -07:00
const TestExpectation = {
Ok : 'ok' ,
Fail : 'fail' ,
2019-11-18 18:18:28 -08:00
} ;
const TestResult = {
Ok : 'ok' ,
2020-03-02 14:57:09 -08:00
MarkedAsFailing : 'markedAsFailing' , // User marked as failed
Skipped : 'skipped' , // User marked as skipped
2019-11-18 18:18:28 -08:00
Failed : 'failed' , // Exception happened during running
TimedOut : 'timedout' , // Timeout Exceeded while running
Terminated : 'terminated' , // Execution terminated
Crashed : 'crashed' , // If testrunner crashed due to this test
} ;
2020-03-27 22:57:22 -07:00
function createHook ( callback , name ) {
2020-03-29 21:38:30 -07:00
const location = Location . getCallerLocation ( _ _filename ) ;
2020-03-27 22:57:22 -07:00
return { name , body : callback , location } ;
}
2019-11-18 18:18:28 -08:00
class Test {
2020-03-26 22:47:13 -07:00
constructor ( suite , name , callback , location ) {
this . _suite = suite ;
this . _name = name ;
this . _fullName = ( suite . fullName ( ) + ' ' + name ) . trim ( ) ;
2020-03-28 14:25:57 -07:00
this . _skipped = false ;
this . _focused = false ;
2020-03-26 22:47:13 -07:00
this . _expectation = TestExpectation . Ok ;
this . _body = callback ;
this . _location = location ;
this . _timeout = INFINITE _TIMEOUT ;
this . _repeat = 1 ;
2020-03-27 22:57:22 -07:00
this . _hooks = [ ] ;
2020-04-01 10:49:47 -07:00
this . _environments = [ ] ;
2020-03-26 22:47:13 -07:00
this . Expectations = { ... TestExpectation } ;
}
suite ( ) {
return this . _suite ;
}
name ( ) {
return this . _name ;
}
fullName ( ) {
return this . _fullName ;
}
location ( ) {
return this . _location ;
}
body ( ) {
return this . _body ;
}
2020-03-28 14:25:57 -07:00
skipped ( ) {
return this . _skipped ;
2020-03-26 22:47:13 -07:00
}
2020-03-28 14:25:57 -07:00
setSkipped ( skipped ) {
this . _skipped = skipped ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-28 14:25:57 -07:00
}
focused ( ) {
return this . _focused ;
}
setFocused ( focused ) {
this . _focused = focused ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
timeout ( ) {
return this . _timeout ;
}
setTimeout ( timeout ) {
this . _timeout = timeout ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
expectation ( ) {
return this . _expectation ;
}
setExpectation ( expectation ) {
this . _expectation = expectation ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
repeat ( ) {
return this . _repeat ;
}
setRepeat ( repeat ) {
this . _repeat = repeat ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
2020-03-27 22:57:22 -07:00
before ( callback ) {
this . _hooks . push ( createHook ( callback , 'before' ) ) ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-27 22:57:22 -07:00
}
after ( callback ) {
this . _hooks . push ( createHook ( callback , 'after' ) ) ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-27 22:57:22 -07:00
}
hooks ( name ) {
return this . _hooks . filter ( hook => ! name || hook . name === name ) ;
}
2020-04-01 10:49:47 -07:00
2020-04-03 09:48:01 -07:00
addEnvironment ( environment ) {
2020-04-02 13:12:22 -07:00
const parents = new Set ( ) ;
for ( let parent = environment ; ! ( parent instanceof Suite ) ; parent = parent . parentEnvironment ( ) )
parents . add ( parent ) ;
for ( const env of this . _environments ) {
for ( let parent = env ; ! ( parent instanceof Suite ) ; parent = parent . parentEnvironment ( ) ) {
if ( parents . has ( parent ) )
throw new Error ( ` Cannot use environments " ${ environment . name ( ) } " and " ${ env . name ( ) } " that share a parent environment " ${ parent . fullName ( ) } " in test " ${ this . fullName ( ) } " ` ) ;
}
}
const environmentParentSuite = environment . parentSuite ( ) ;
2020-04-01 10:49:47 -07:00
for ( let suite = this . suite ( ) ; suite ; suite = suite . parentSuite ( ) ) {
2020-04-02 13:12:22 -07:00
if ( suite === environmentParentSuite ) {
2020-04-01 10:49:47 -07:00
this . _environments . push ( environment ) ;
2020-04-02 13:12:22 -07:00
return this ;
2020-04-01 10:49:47 -07:00
}
}
throw new Error ( ` Cannot use environment " ${ environment . name ( ) } " from suite " ${ environment . parentSuite ( ) . fullName ( ) } " in unrelated test " ${ this . fullName ( ) } " ` ) ;
}
2020-04-03 09:48:01 -07:00
removeEnvironment ( environment ) {
const index = this . _environments . indexOf ( environment ) ;
if ( index === - 1 )
throw new Error ( ` Environment " ${ environment . name ( ) } " cannot be removed because it was not added to the test " ${ test . fullName ( ) } " ` ) ;
this . _environments . splice ( index , 1 ) ;
return this ;
}
2019-11-18 18:18:28 -08:00
}
2020-04-01 10:49:47 -07:00
class Environment {
2020-04-02 13:12:22 -07:00
constructor ( parentEnvironment , name , location ) {
this . _parentEnvironment = parentEnvironment ;
this . _parentSuite = parentEnvironment ;
if ( parentEnvironment && ! ( parentEnvironment instanceof Suite ) )
this . _parentSuite = parentEnvironment . parentSuite ( ) ;
2020-03-26 22:47:13 -07:00
this . _name = name ;
2020-04-02 13:12:22 -07:00
this . _fullName = ( parentEnvironment ? parentEnvironment . fullName ( ) + ' ' + name : name ) . trim ( ) ;
2020-03-26 22:47:13 -07:00
this . _location = location ;
2020-03-27 22:57:22 -07:00
this . _hooks = [ ] ;
2020-03-26 22:47:13 -07:00
}
2020-04-02 13:12:22 -07:00
parentEnvironment ( ) {
return this . _parentEnvironment ;
}
2020-03-26 22:47:13 -07:00
parentSuite ( ) {
return this . _parentSuite ;
}
name ( ) {
return this . _name ;
}
fullName ( ) {
return this . _fullName ;
}
2020-04-01 10:49:47 -07:00
beforeEach ( callback ) {
this . _hooks . push ( createHook ( callback , 'beforeEach' ) ) ;
return this ;
}
afterEach ( callback ) {
this . _hooks . push ( createHook ( callback , 'afterEach' ) ) ;
return this ;
}
beforeAll ( callback ) {
this . _hooks . push ( createHook ( callback , 'beforeAll' ) ) ;
return this ;
}
afterAll ( callback ) {
this . _hooks . push ( createHook ( callback , 'afterAll' ) ) ;
return this ;
}
hooks ( name ) {
return this . _hooks . filter ( hook => ! name || hook . name === name ) ;
}
}
class Suite extends Environment {
constructor ( parentSuite , name , location ) {
super ( parentSuite , name , location ) ;
this . _skipped = false ;
this . _focused = false ;
this . _expectation = TestExpectation . Ok ;
this . _repeat = 1 ;
this . Expectations = { ... TestExpectation } ;
}
2020-03-28 14:25:57 -07:00
skipped ( ) {
return this . _skipped ;
2020-03-26 22:47:13 -07:00
}
2020-03-28 14:25:57 -07:00
setSkipped ( skipped ) {
this . _skipped = skipped ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-28 14:25:57 -07:00
}
focused ( ) {
return this . _focused ;
}
setFocused ( focused ) {
this . _focused = focused ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
location ( ) {
return this . _location ;
}
expectation ( ) {
return this . _expectation ;
}
setExpectation ( expectation ) {
this . _expectation = expectation ;
2020-03-29 11:37:45 -07:00
return this ;
2020-03-26 22:47:13 -07:00
}
repeat ( ) {
return this . _repeat ;
}
setRepeat ( repeat ) {
this . _repeat = repeat ;
2020-03-29 11:37:45 -07:00
return this ;
2019-11-18 18:18:28 -08:00
}
}
2020-03-28 14:25:57 -07:00
class TestRun {
constructor ( test ) {
this . _test = test ;
this . _result = null ;
this . _error = null ;
this . _startTimestamp = 0 ;
this . _endTimestamp = 0 ;
this . _workerId = null ;
}
finished ( ) {
return this . _result !== null && this . _result !== 'running' ;
}
isFailure ( ) {
return this . _result === TestResult . Failed || this . _result === TestResult . TimedOut || this . _result === TestResult . Crashed ;
}
ok ( ) {
return this . _result === TestResult . Ok ;
}
result ( ) {
return this . _result ;
}
error ( ) {
return this . _error ;
}
duration ( ) {
return this . _endTimestamp - this . _startTimestamp ;
}
test ( ) {
return this . _test ;
}
workerId ( ) {
return this . _workerId ;
}
}
2020-03-12 17:32:53 -07:00
class Result {
constructor ( ) {
this . result = TestResult . Ok ;
this . exitCode = 0 ;
this . message = '' ;
this . errors = [ ] ;
2020-03-28 14:25:57 -07:00
this . runs = [ ] ;
2020-03-12 17:32:53 -07:00
}
setResult ( result , message ) {
if ( ! this . ok ( ) )
return ;
this . result = result ;
this . message = message || '' ;
if ( result === TestResult . Ok )
this . exitCode = 0 ;
else if ( result === TestResult . Terminated )
this . exitCode = 130 ;
else if ( result === TestResult . Crashed )
this . exitCode = 2 ;
else
this . exitCode = 1 ;
}
addError ( message , error , worker ) {
2020-03-28 14:25:57 -07:00
const data = { message , error , runs : [ ] } ;
if ( worker )
data . runs = worker . _runs . slice ( ) ;
2020-03-12 17:32:53 -07:00
this . errors . push ( data ) ;
}
ok ( ) {
return this . result === TestResult . Ok ;
}
}
2020-03-10 11:30:02 -07:00
class TestWorker {
2020-04-03 15:47:25 -07:00
constructor ( testRunner , workerId , parallelIndex ) {
this . _testRunner = testRunner ;
2020-03-10 11:30:02 -07:00
this . _state = { parallelIndex } ;
2020-04-01 10:49:47 -07:00
this . _environmentStack = [ ] ;
2020-03-12 17:32:53 -07:00
this . _terminating = false ;
2020-03-10 11:30:02 -07:00
this . _workerId = workerId ;
2020-03-26 14:43:28 -07:00
this . _runningTestTerminate = null ;
this . _runningHookTerminate = null ;
2020-03-28 14:25:57 -07:00
this . _runs = [ ] ;
2020-03-10 11:30:02 -07:00
}
2019-11-18 18:18:28 -08:00
2020-03-12 17:32:53 -07:00
terminate ( terminateHooks ) {
this . _terminating = true ;
2020-03-26 14:43:28 -07:00
if ( this . _runningTestTerminate )
this . _runningTestTerminate ( ) ;
if ( terminateHooks && this . _runningHookTerminate )
this . _runningHookTerminate ( ) ;
2020-03-10 11:30:02 -07:00
}
2019-11-18 18:18:28 -08:00
2020-03-28 14:25:57 -07:00
_markTerminated ( testRun ) {
2020-03-12 17:32:53 -07:00
if ( ! this . _terminating )
2020-03-10 11:30:02 -07:00
return false ;
2020-03-28 14:25:57 -07:00
testRun . _result = TestResult . Terminated ;
2020-03-10 11:30:02 -07:00
return true ;
}
2020-03-28 14:25:57 -07:00
async run ( testRun ) {
this . _runs . push ( testRun ) ;
2020-03-12 17:32:53 -07:00
2020-03-28 14:25:57 -07:00
const test = testRun . test ( ) ;
let skipped = test . skipped ( ) && ! test . focused ( ) ;
2020-03-27 15:35:14 -07:00
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
2020-03-28 14:25:57 -07:00
skipped = skipped || ( suite . skipped ( ) && ! suite . focused ( ) ) ;
2020-03-27 15:35:14 -07:00
if ( skipped ) {
2020-03-28 14:25:57 -07:00
await this . _willStartTestRun ( testRun ) ;
testRun . _result = TestResult . Skipped ;
await this . _didFinishTestRun ( testRun ) ;
2020-03-10 11:30:02 -07:00
return ;
}
2020-03-27 15:35:14 -07:00
let expectedToFail = test . expectation ( ) === TestExpectation . Fail ;
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
expectedToFail = expectedToFail || ( suite . expectation ( ) === TestExpectation . Fail ) ;
2020-03-28 14:25:57 -07:00
if ( expectedToFail && ! test . focused ( ) ) {
await this . _willStartTestRun ( testRun ) ;
testRun . _result = TestResult . MarkedAsFailing ;
await this . _didFinishTestRun ( testRun ) ;
2020-03-10 11:30:02 -07:00
return ;
2019-11-18 18:18:28 -08:00
}
2020-04-01 10:49:47 -07:00
const environmentStack = [ ] ;
2020-03-26 22:47:13 -07:00
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
2020-04-01 10:49:47 -07:00
environmentStack . push ( suite ) ;
environmentStack . reverse ( ) ;
2020-04-02 13:12:22 -07:00
for ( const environment of test . _environments ) {
const insert = [ ] ;
for ( let parent = environment ; ! ( parent instanceof Suite ) ; parent = parent . parentEnvironment ( ) )
insert . push ( parent ) ;
environmentStack . splice ( environmentStack . indexOf ( environment . parentSuite ( ) ) + 1 , 0 , ... insert . reverse ( ) ) ;
}
2020-03-10 11:30:02 -07:00
let common = 0 ;
2020-04-01 10:49:47 -07:00
while ( common < environmentStack . length && this . _environmentStack [ common ] === environmentStack [ common ] )
2020-03-10 11:30:02 -07:00
common ++ ;
2020-04-01 10:49:47 -07:00
while ( this . _environmentStack . length > common ) {
2020-03-28 14:25:57 -07:00
if ( this . _markTerminated ( testRun ) )
2020-03-10 11:30:02 -07:00
return ;
2020-04-01 10:49:47 -07:00
const environment = this . _environmentStack . pop ( ) ;
for ( const hook of environment . hooks ( 'afterAll' ) ) {
if ( ! await this . _runHook ( testRun , hook , environment . fullName ( ) ) )
2020-03-27 22:57:22 -07:00
return ;
}
2020-03-10 11:30:02 -07:00
}
2020-04-01 10:49:47 -07:00
while ( this . _environmentStack . length < environmentStack . length ) {
2020-03-28 14:25:57 -07:00
if ( this . _markTerminated ( testRun ) )
2020-03-10 11:30:02 -07:00
return ;
2020-04-01 10:49:47 -07:00
const environment = environmentStack [ this . _environmentStack . length ] ;
this . _environmentStack . push ( environment ) ;
for ( const hook of environment . hooks ( 'beforeAll' ) ) {
if ( ! await this . _runHook ( testRun , hook , environment . fullName ( ) ) )
2020-03-27 22:57:22 -07:00
return ;
}
2020-03-10 11:30:02 -07:00
}
2020-03-28 14:25:57 -07:00
if ( this . _markTerminated ( testRun ) )
2020-03-10 11:30:02 -07:00
return ;
// From this point till the end, we have to run all hooks
// no matter what happens.
2020-03-28 14:25:57 -07:00
await this . _willStartTestRun ( testRun ) ;
2020-04-01 10:49:47 -07:00
for ( const environment of this . _environmentStack ) {
for ( const hook of environment . hooks ( 'beforeEach' ) )
await this . _runHook ( testRun , hook , environment . fullName ( ) , true ) ;
2020-03-27 22:57:22 -07:00
}
for ( const hook of test . hooks ( 'before' ) )
2020-03-28 14:25:57 -07:00
await this . _runHook ( testRun , hook , test . fullName ( ) , true ) ;
2020-03-10 11:30:02 -07:00
2020-03-28 14:25:57 -07:00
if ( ! testRun . _error && ! this . _markTerminated ( testRun ) ) {
await this . _willStartTestBody ( testRun ) ;
2020-03-26 22:47:13 -07:00
const { promise , terminate } = runUserCallback ( test . body ( ) , test . timeout ( ) , [ this . _state , test ] ) ;
2020-03-26 14:43:28 -07:00
this . _runningTestTerminate = terminate ;
2020-03-28 14:25:57 -07:00
testRun . _error = await promise ;
2020-03-26 14:43:28 -07:00
this . _runningTestTerminate = null ;
2020-03-28 14:25:57 -07:00
if ( testRun . _error && testRun . _error . stack )
2020-04-03 15:47:25 -07:00
await this . _testRunner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( testRun . _error ) ;
2020-03-28 14:25:57 -07:00
if ( ! testRun . _error )
testRun . _result = TestResult . Ok ;
else if ( testRun . _error === TimeoutError )
testRun . _result = TestResult . TimedOut ;
else if ( testRun . _error === TerminatedError )
testRun . _result = TestResult . Terminated ;
2020-03-10 11:30:02 -07:00
else
2020-03-28 14:25:57 -07:00
testRun . _result = TestResult . Failed ;
await this . _didFinishTestBody ( testRun ) ;
2020-03-10 11:30:02 -07:00
}
2020-03-27 22:57:22 -07:00
for ( const hook of test . hooks ( 'after' ) )
2020-03-28 14:25:57 -07:00
await this . _runHook ( testRun , hook , test . fullName ( ) , true ) ;
2020-04-01 10:49:47 -07:00
for ( const environment of this . _environmentStack . slice ( ) . reverse ( ) ) {
for ( const hook of environment . hooks ( 'afterEach' ) )
await this . _runHook ( testRun , hook , environment . fullName ( ) , true ) ;
2020-03-27 22:57:22 -07:00
}
2020-03-28 14:25:57 -07:00
await this . _didFinishTestRun ( testRun ) ;
2020-03-10 11:30:02 -07:00
}
2020-03-28 14:25:57 -07:00
async _runHook ( testRun , hook , fullName , passTest = false ) {
await this . _willStartHook ( hook , fullName ) ;
2020-04-03 15:47:25 -07:00
const timeout = this . _testRunner . _timeout ;
2020-03-28 14:25:57 -07:00
const { promise , terminate } = runUserCallback ( hook . body , timeout , passTest ? [ this . _state , testRun . test ( ) ] : [ this . _state ] ) ;
2020-03-26 14:43:28 -07:00
this . _runningHookTerminate = terminate ;
let error = await promise ;
this . _runningHookTerminate = null ;
2020-03-10 11:30:02 -07:00
if ( error ) {
2020-03-28 14:25:57 -07:00
if ( testRun && testRun . _result !== TestResult . Terminated ) {
2020-03-10 11:30:02 -07:00
// Prefer terminated result over any hook failures.
2020-03-28 14:25:57 -07:00
testRun . _result = error === TerminatedError ? TestResult . Terminated : TestResult . Crashed ;
2020-03-10 11:30:02 -07:00
}
2020-03-12 17:32:53 -07:00
let message ;
2020-03-10 11:30:02 -07:00
if ( error === TimeoutError ) {
2020-03-29 21:38:30 -07:00
message = ` ${ hook . location . toDetailedString ( ) } - Timeout Exceeded ${ timeout } ms while running " ${ hook . name } " in " ${ fullName } " ` ;
2020-03-12 17:32:53 -07:00
error = null ;
2020-03-10 11:30:02 -07:00
} else if ( error === TerminatedError ) {
2020-03-24 14:40:59 -07:00
// Do not report termination details - it's just noise.
message = '' ;
2020-03-12 17:32:53 -07:00
error = null ;
2020-03-10 11:30:02 -07:00
} else {
if ( error . stack )
2020-04-03 15:47:25 -07:00
await this . _testRunner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
2020-03-29 21:38:30 -07:00
message = ` ${ hook . location . toDetailedString ( ) } - FAILED while running " ${ hook . name } " in suite " ${ fullName } ": ` ;
2020-03-10 11:30:02 -07:00
}
2020-03-28 14:25:57 -07:00
await this . _didFailHook ( hook , fullName , message , error ) ;
if ( testRun )
testRun . _error = error ;
2020-03-10 11:30:02 -07:00
return false ;
}
2020-03-28 14:25:57 -07:00
await this . _didCompleteHook ( hook , fullName ) ;
2020-03-10 11:30:02 -07:00
return true ;
}
2020-03-28 14:25:57 -07:00
async _willStartTestRun ( testRun ) {
testRun . _startTimestamp = Date . now ( ) ;
testRun . _workerId = this . _workerId ;
2020-04-03 15:47:25 -07:00
await this . _testRunner . _delegate . onTestRunStarted ( testRun ) ;
2020-03-28 14:25:57 -07:00
}
async _didFinishTestRun ( testRun ) {
testRun . _endTimestamp = Date . now ( ) ;
testRun . _workerId = this . _workerId ;
2020-04-03 15:47:25 -07:00
await this . _testRunner . _delegate . onTestRunFinished ( testRun ) ;
2020-03-28 14:25:57 -07:00
}
async _willStartTestBody ( testRun ) {
2020-03-29 21:38:30 -07:00
debug ( 'testrunner:test' ) ( ` [ ${ this . _workerId } ] starting " ${ testRun . test ( ) . fullName ( ) } " ( ${ testRun . test ( ) . location ( ) } ) ` ) ;
2020-03-28 14:25:57 -07:00
}
async _didFinishTestBody ( testRun ) {
2020-03-29 21:38:30 -07:00
debug ( 'testrunner:test' ) ( ` [ ${ this . _workerId } ] ${ testRun . _result . toUpperCase ( ) } " ${ testRun . test ( ) . fullName ( ) } " ( ${ testRun . test ( ) . location ( ) } ) ` ) ;
2020-03-28 14:25:57 -07:00
}
async _willStartHook ( hook , fullName ) {
2020-03-29 21:38:30 -07:00
debug ( 'testrunner:hook' ) ( ` [ ${ this . _workerId } ] " ${ hook . name } " started for " ${ fullName } " ( ${ hook . location } ) ` ) ;
2020-03-28 14:25:57 -07:00
}
async _didFailHook ( hook , fullName , message , error ) {
2020-03-29 21:38:30 -07:00
debug ( 'testrunner:hook' ) ( ` [ ${ this . _workerId } ] " ${ hook . name } " FAILED for " ${ fullName } " ( ${ hook . location } ) ` ) ;
2020-03-28 14:25:57 -07:00
if ( message )
2020-04-03 15:47:25 -07:00
this . _testRunner . _result . addError ( message , error , this ) ;
this . _testRunner . _result . setResult ( TestResult . Crashed , message ) ;
2020-03-28 14:25:57 -07:00
}
async _didCompleteHook ( hook , fullName ) {
2020-03-29 21:38:30 -07:00
debug ( 'testrunner:hook' ) ( ` [ ${ this . _workerId } ] " ${ hook . name } " OK for " ${ fullName } " ( ${ hook . location } ) ` ) ;
2020-03-28 14:25:57 -07:00
}
2020-03-10 11:30:02 -07:00
async shutdown ( ) {
2020-04-01 10:49:47 -07:00
while ( this . _environmentStack . length > 0 ) {
const environment = this . _environmentStack . pop ( ) ;
for ( const hook of environment . hooks ( 'afterAll' ) )
await this . _runHook ( null , hook , environment . fullName ( ) ) ;
2020-03-10 11:30:02 -07:00
}
}
}
2020-04-03 15:47:25 -07:00
class TestRunner {
2019-11-18 18:18:28 -08:00
constructor ( options = { } ) {
const {
timeout = 10 * 1000 , // Default timeout is 10 seconds.
parallel = 1 ,
breakOnFailure = false ,
2020-03-11 18:30:43 -07:00
crashIfTestsAreFocusedOnCI = true ,
2020-03-28 08:49:00 -07:00
installCommonHelpers = true ,
2019-11-18 18:18:28 -08:00
} = options ;
2020-03-11 18:30:43 -07:00
this . _crashIfTestsAreFocusedOnCI = crashIfTestsAreFocusedOnCI ;
2020-01-08 16:16:54 +00:00
this . _sourceMapSupport = new SourceMapSupport ( ) ;
2020-03-29 21:38:30 -07:00
this . _rootSuite = new Suite ( null , '' , new Location ( ) ) ;
2020-04-01 10:49:47 -07:00
this . _currentEnvironment = this . _rootSuite ;
2019-11-18 18:18:28 -08:00
this . _tests = [ ] ;
2020-03-11 18:30:43 -07:00
this . _suites = [ ] ;
2019-12-19 15:47:35 -08:00
this . _timeout = timeout === 0 ? INFINITE _TIMEOUT : timeout ;
2019-11-18 18:18:28 -08:00
this . _parallel = parallel ;
this . _breakOnFailure = breakOnFailure ;
2020-03-26 22:47:13 -07:00
this . _suiteModifiers = new Map ( ) ;
this . _suiteAttributes = new Map ( ) ;
this . _testModifiers = new Map ( ) ;
this . _testAttributes = new Map ( ) ;
2020-04-03 15:47:25 -07:00
this . _nextWorkerId = 1 ;
this . _workers = [ ] ;
this . _terminating = false ;
this . _result = null ;
this . _delegate = {
async onStarted ( testRuns ) { } ,
async onFinished ( result ) { } ,
async onTestRunStarted ( testRun ) { } ,
async onTestRunFinished ( testRun ) { } ,
} ;
2019-11-18 18:18:28 -08:00
2020-04-01 10:49:47 -07:00
this . beforeAll = ( callback ) => this . _currentEnvironment . beforeAll ( callback ) ;
this . beforeEach = ( callback ) => this . _currentEnvironment . beforeEach ( callback ) ;
this . afterAll = ( callback ) => this . _currentEnvironment . afterAll ( callback ) ;
this . afterEach = ( callback ) => this . _currentEnvironment . afterEach ( callback ) ;
2020-03-25 14:40:57 -07:00
2020-03-26 22:47:13 -07:00
this . describe = this . _suiteBuilder ( [ ] ) ;
this . it = this . _testBuilder ( [ ] ) ;
2020-04-01 10:49:47 -07:00
this . environment = ( name , callback ) => {
const location = Location . getCallerLocation ( _ _filename ) ;
const environment = new Environment ( this . _currentEnvironment , name , location ) ;
this . _currentEnvironment = environment ;
callback ( ) ;
2020-04-02 13:12:22 -07:00
this . _currentEnvironment = environment . parentEnvironment ( ) ;
2020-04-01 10:49:47 -07:00
return environment ;
} ;
2020-03-29 11:37:45 -07:00
this . Expectations = { ... TestExpectation } ;
2020-03-25 22:42:09 -07:00
2020-03-28 08:49:00 -07:00
if ( installCommonHelpers ) {
2020-03-29 11:37:45 -07:00
this . fdescribe = this . _suiteBuilder ( [ { callback : s => s . setFocused ( true ) , args : [ ] } ] ) ;
this . xdescribe = this . _suiteBuilder ( [ { callback : s => s . setSkipped ( true ) , args : [ ] } ] ) ;
this . fit = this . _testBuilder ( [ { callback : t => t . setFocused ( true ) , args : [ ] } ] ) ;
this . xit = this . _testBuilder ( [ { callback : t => t . setSkipped ( true ) , args : [ ] } ] ) ;
2020-03-28 08:49:00 -07:00
}
2020-03-25 22:42:09 -07:00
}
2020-03-26 22:47:13 -07:00
_suiteBuilder ( callbacks ) {
return new Proxy ( ( name , callback , ... suiteArgs ) => {
2020-04-01 10:49:47 -07:00
if ( ! ( this . _currentEnvironment instanceof Suite ) )
throw new Error ( ` Cannot define a suite inside an environment ` ) ;
2020-03-29 21:38:30 -07:00
const location = Location . getCallerLocation ( _ _filename ) ;
2020-04-01 10:49:47 -07:00
const suite = new Suite ( this . _currentEnvironment , name , location ) ;
2020-03-26 22:47:13 -07:00
for ( const { callback , args } of callbacks )
callback ( suite , ... args ) ;
2020-04-01 10:49:47 -07:00
this . _currentEnvironment = suite ;
2020-03-28 14:25:57 -07:00
callback ( ... suiteArgs ) ;
this . _suites . push ( suite ) ;
2020-04-01 10:49:47 -07:00
this . _currentEnvironment = suite . parentSuite ( ) ;
2020-03-29 11:37:45 -07:00
return suite ;
2020-03-26 22:47:13 -07:00
} , {
get : ( obj , prop ) => {
if ( this . _suiteModifiers . has ( prop ) )
return ( ... args ) => this . _suiteBuilder ( [ ... callbacks , { callback : this . _suiteModifiers . get ( prop ) , args } ] ) ;
if ( this . _suiteAttributes . has ( prop ) )
return this . _suiteBuilder ( [ ... callbacks , { callback : this . _suiteAttributes . get ( prop ) , args : [ ] } ] ) ;
return obj [ prop ] ;
2020-03-25 22:42:09 -07:00
} ,
2020-03-26 22:47:13 -07:00
} ) ;
}
_testBuilder ( callbacks ) {
return new Proxy ( ( name , callback ) => {
2020-04-01 10:49:47 -07:00
if ( ! ( this . _currentEnvironment instanceof Suite ) )
throw new Error ( ` Cannot define a test inside an environment ` ) ;
2020-03-29 21:38:30 -07:00
const location = Location . getCallerLocation ( _ _filename ) ;
2020-04-01 10:49:47 -07:00
const test = new Test ( this . _currentEnvironment , name , callback , location ) ;
2020-03-26 22:47:13 -07:00
test . setTimeout ( this . _timeout ) ;
for ( const { callback , args } of callbacks )
callback ( test , ... args ) ;
2020-03-28 14:25:57 -07:00
this . _tests . push ( test ) ;
2020-03-29 11:37:45 -07:00
return test ;
2020-03-26 22:47:13 -07:00
} , {
2020-03-25 22:42:09 -07:00
get : ( obj , prop ) => {
2020-03-26 22:47:13 -07:00
if ( this . _testModifiers . has ( prop ) )
return ( ... args ) => this . _testBuilder ( [ ... callbacks , { callback : this . _testModifiers . get ( prop ) , args } ] ) ;
if ( this . _testAttributes . has ( prop ) )
return this . _testBuilder ( [ ... callbacks , { callback : this . _testAttributes . get ( prop ) , args : [ ] } ] ) ;
2020-03-25 22:42:09 -07:00
return obj [ prop ] ;
} ,
} ) ;
2020-03-25 14:40:57 -07:00
}
2020-03-26 22:47:13 -07:00
testModifier ( name , callback ) {
this . _testModifiers . set ( name , callback ) ;
2020-03-25 14:40:57 -07:00
}
2020-03-26 22:47:13 -07:00
testAttribute ( name , callback ) {
this . _testAttributes . set ( name , callback ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-26 22:47:13 -07:00
suiteModifier ( name , callback ) {
this . _suiteModifiers . set ( name , callback ) ;
2020-03-25 22:42:09 -07:00
}
2020-03-26 22:47:13 -07:00
suiteAttribute ( name , callback ) {
this . _suiteAttributes . set ( name , callback ) ;
2020-03-25 22:42:09 -07:00
}
2020-04-03 15:47:25 -07:00
setDelegate ( delegate ) {
this . _delegate = delegate ;
}
2020-03-12 17:32:53 -07:00
async run ( options = { } ) {
const { totalTimeout = 0 } = options ;
2020-03-28 14:25:57 -07:00
const testRuns = [ ] ;
for ( const test of this . _testsToRun ( ) ) {
let repeat = test . repeat ( ) ;
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
repeat *= suite . repeat ( ) ;
for ( let i = 0 ; i < repeat ; i ++ )
testRuns . push ( new TestRun ( test ) ) ;
}
2020-03-11 18:30:43 -07:00
2020-04-03 15:47:25 -07:00
this . _result = new Result ( ) ;
2020-03-11 18:30:43 -07:00
if ( this . _crashIfTestsAreFocusedOnCI && process . env . CI && this . hasFocusedTestsOrSuites ( ) ) {
2020-04-03 15:47:25 -07:00
await this . _delegate . onStarted ( [ ] ) ;
this . _result . setResult ( TestResult . Crashed , '"focused" tests or suites are probitted on CI' ) ;
await this . _delegate . onFinished ( this . _result ) ;
2019-11-18 18:18:28 -08:00
} else {
2020-04-03 15:47:25 -07:00
await this . _delegate . onStarted ( testRuns ) ;
this . _result . runs = testRuns ;
2020-03-12 17:32:53 -07:00
let timeoutId ;
2020-03-26 22:47:13 -07:00
if ( totalTimeout ) {
timeoutId = setTimeout ( ( ) => {
2020-04-03 15:47:25 -07:00
this . _terminate ( TestResult . Terminated , ` Total timeout of ${ totalTimeout } ms reached. ` , true /* force */ , null /* error */ ) ;
2020-03-26 22:47:13 -07:00
} , totalTimeout ) ;
}
2020-04-03 15:47:25 -07:00
const terminations = [
createTermination . call ( this , 'SIGINT' , TestResult . Terminated , 'SIGINT received' ) ,
createTermination . call ( this , 'SIGHUP' , TestResult . Terminated , 'SIGHUP received' ) ,
createTermination . call ( this , 'SIGTERM' , TestResult . Terminated , 'SIGTERM received' ) ,
createTermination . call ( this , 'unhandledRejection' , TestResult . Crashed , 'UNHANDLED PROMISE REJECTION' ) ,
createTermination . call ( this , 'uncaughtException' , TestResult . Crashed , 'UNHANDLED ERROR' ) ,
] ;
for ( const termination of terminations )
process . on ( termination . event , termination . handler ) ;
const parallel = Math . min ( this . _parallel , testRuns . length ) ;
const workerPromises = [ ] ;
for ( let i = 0 ; i < parallel ; ++ i ) {
const initialTestRunIndex = i * Math . floor ( testRuns . length / parallel ) ;
workerPromises . push ( this . _runWorker ( initialTestRunIndex , testRuns , i ) ) ;
}
await Promise . all ( workerPromises ) ;
for ( const termination of terminations )
process . removeListener ( termination . event , termination . handler ) ;
if ( testRuns . some ( run => run . isFailure ( ) ) )
this . _result . setResult ( TestResult . Failed , '' ) ;
clearTimeout ( timeoutId ) ;
await this . _delegate . onFinished ( this . _result ) ;
function createTermination ( event , result , message ) {
return {
event ,
message ,
handler : error => this . _terminate ( result , message , event === 'SIGTERM' , event . startsWith ( 'SIG' ) ? null : error ) ,
} ;
2020-01-13 15:30:16 -08:00
}
2019-11-18 18:18:28 -08:00
}
2020-04-03 15:47:25 -07:00
const result = this . _result ;
this . _result = null ;
this . _workers = [ ] ;
this . _terminating = false ;
2019-11-18 18:18:28 -08:00
return result ;
}
2020-04-03 15:47:25 -07:00
async _runWorker ( testRunIndex , testRuns , parallelIndex ) {
let worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
while ( ! this . _terminating ) {
let skipped = 0 ;
while ( skipped < testRuns . length && testRuns [ testRunIndex ] . _result !== null ) {
testRunIndex = ( testRunIndex + 1 ) % testRuns . length ;
skipped ++ ;
}
const testRun = testRuns [ testRunIndex ] ;
if ( testRun . _result !== null ) {
// All tests have been run.
break ;
}
// Mark as running so that other workers do not run it again.
testRun . _result = 'running' ;
await worker . run ( testRun ) ;
if ( testRun . isFailure ( ) ) {
// Something went wrong during test run, let's use a fresh worker.
await worker . shutdown ( ) ;
if ( this . _breakOnFailure ) {
const message = ` Terminating because a test has failed and |testRunner.breakOnFailure| is enabled ` ;
await this . _terminate ( TestResult . Terminated , message , false /* force */ , null /* error */ ) ;
return ;
}
worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
}
}
await worker . shutdown ( ) ;
}
async _terminate ( result , message , force , error ) {
debug ( 'testrunner' ) ( ` TERMINATED result = ${ result } , message = ${ message } ` ) ;
this . _terminating = true ;
for ( const worker of this . _workers )
worker . terminate ( force /* terminateHooks */ ) ;
this . _result . setResult ( result , message ) ;
if ( this . _result . message === 'SIGINT received' && message === 'SIGTERM received' )
this . _result . message = message ;
if ( error ) {
if ( error . stack )
await this . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
this . _result . addError ( message , error , this . _workers . length === 1 ? this . _workers [ 0 ] : null ) ;
}
}
2020-03-28 14:25:57 -07:00
_testsToRun ( ) {
2020-03-27 15:35:14 -07:00
if ( ! this . hasFocusedTestsOrSuites ( ) )
2020-03-28 14:25:57 -07:00
return this . _tests ;
2020-03-27 15:35:14 -07:00
const notFocusedSuites = new Set ( ) ;
// Mark parent suites of focused tests as not focused.
for ( const test of this . _tests ) {
2020-03-28 14:25:57 -07:00
if ( test . focused ( ) ) {
2020-03-27 15:35:14 -07:00
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
notFocusedSuites . add ( suite ) ;
}
}
// Pick all tests that are focused or belong to focused suites.
const tests = [ ] ;
for ( const test of this . _tests ) {
2020-03-28 14:25:57 -07:00
let focused = test . focused ( ) ;
2020-03-27 15:35:14 -07:00
for ( let suite = test . suite ( ) ; suite ; suite = suite . parentSuite ( ) )
2020-03-28 14:25:57 -07:00
focused = focused || ( suite . focused ( ) && ! notFocusedSuites . has ( suite ) ) ;
2020-03-27 15:35:14 -07:00
if ( focused )
tests . push ( test ) ;
}
return tests ;
}
2020-01-09 16:37:19 +00:00
async terminate ( ) {
2020-04-03 15:47:25 -07:00
if ( ! this . _result )
2019-11-18 18:18:28 -08:00
return ;
2020-04-03 15:47:25 -07:00
await this . _terminate ( TestResult . Terminated , 'Terminated with |TestRunner.terminate()| call' , true /* force */ , null /* error */ ) ;
2019-11-18 18:18:28 -08:00
}
timeout ( ) {
return this . _timeout ;
}
hasFocusedTestsOrSuites ( ) {
2020-03-28 14:25:57 -07:00
return this . _tests . some ( test => test . focused ( ) ) || this . _suites . some ( suite => suite . focused ( ) ) ;
2020-03-11 18:30:43 -07:00
}
focusMatchingTests ( fullNameRegex ) {
for ( const test of this . _tests ) {
2020-03-26 22:47:13 -07:00
if ( fullNameRegex . test ( test . fullName ( ) ) )
2020-03-28 14:25:57 -07:00
test . setFocused ( true ) ;
2020-03-11 18:30:43 -07:00
}
2019-11-18 18:18:28 -08:00
}
tests ( ) {
return this . _tests . slice ( ) ;
}
2020-03-28 14:25:57 -07:00
suites ( ) {
return this . _suites . slice ( ) ;
2020-03-02 14:57:09 -08:00
}
2019-11-18 18:18:28 -08:00
parallel ( ) {
return this . _parallel ;
}
}
module . exports = TestRunner ;