2019-11-18 18:18:28 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
*
* 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 .
* /
2019-12-19 15:47:35 -08:00
const util = require ( 'util' ) ;
const url = require ( 'url' ) ;
const inspector = require ( 'inspector' ) ;
2019-11-18 18:18:28 -08:00
const EventEmitter = require ( 'events' ) ;
const Multimap = require ( './Multimap' ) ;
2019-12-19 15:47:35 -08:00
const fs = require ( 'fs' ) ;
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-02-20 22:55:39 -08:00
const { getCallerLocation } = require ( './utils' ) ;
2019-12-19 15:47:35 -08:00
2020-03-25 22:42:09 -07:00
const INFINITE _TIMEOUT = 100000000 ;
2019-12-19 15:47:35 -08:00
const readFileAsync = util . promisify ( fs . readFile . bind ( fs ) ) ;
2019-11-18 18:18:28 -08:00
const TimeoutError = new Error ( 'Timeout' ) ;
const TerminatedError = new Error ( 'Terminated' ) ;
const MAJOR _NODEJS _VERSION = parseInt ( process . version . substring ( 1 ) . split ( '.' ) [ 0 ] , 10 ) ;
class UserCallback {
constructor ( callback , timeout ) {
this . _callback = callback ;
this . _terminatePromise = new Promise ( resolve => {
this . _terminateCallback = resolve ;
} ) ;
this . timeout = timeout ;
2020-02-20 22:55:39 -08:00
this . location = getCallerLocation ( _ _filename ) ;
2019-11-18 18:18:28 -08:00
}
async run ( ... args ) {
let timeoutId ;
const timeoutPromise = new Promise ( resolve => {
timeoutId = setTimeout ( resolve . bind ( null , TimeoutError ) , this . timeout ) ;
} ) ;
try {
return await Promise . race ( [
Promise . resolve ( ) . then ( this . _callback . bind ( null , ... args ) ) . then ( ( ) => null ) . catch ( e => e ) ,
timeoutPromise ,
this . _terminatePromise
] ) ;
} catch ( e ) {
return e ;
} finally {
clearTimeout ( timeoutId ) ;
}
}
terminate ( ) {
this . _terminateCallback ( TerminatedError ) ;
}
}
const TestMode = {
Run : 'run' ,
Skip : 'skip' ,
2020-03-02 11:18:42 -08:00
Focus : 'focus' ,
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-10 11:30:02 -07:00
function isTestFailure ( testResult ) {
return testResult === TestResult . Failed || testResult === TestResult . TimedOut || testResult === TestResult . Crashed ;
}
2019-11-18 18:18:28 -08:00
class Test {
2020-03-15 23:10:49 -07:00
constructor ( suite , name , callback , declaredMode , expectation , timeout ) {
2019-11-18 18:18:28 -08:00
this . suite = suite ;
this . name = name ;
this . fullName = ( suite . fullName + ' ' + name ) . trim ( ) ;
this . declaredMode = declaredMode ;
2020-03-15 23:10:49 -07:00
this . expectation = expectation ;
2019-11-18 18:18:28 -08:00
this . _userCallback = new UserCallback ( callback , timeout ) ;
this . location = this . _userCallback . location ;
2020-02-20 22:55:39 -08:00
this . timeout = timeout ;
2019-11-18 18:18:28 -08:00
// Test results
this . result = null ;
this . error = null ;
this . startTimestamp = 0 ;
this . endTimestamp = 0 ;
}
}
class Suite {
2020-03-15 23:10:49 -07:00
constructor ( parentSuite , name , declaredMode , expectation ) {
2019-11-18 18:18:28 -08:00
this . parentSuite = parentSuite ;
this . name = name ;
this . fullName = ( parentSuite ? parentSuite . fullName + ' ' + name : name ) . trim ( ) ;
this . declaredMode = declaredMode ;
2020-03-15 23:10:49 -07:00
this . expectation = expectation ;
2019-11-18 18:18:28 -08:00
/** @type {!Array<(!Test|!Suite)>} */
this . children = [ ] ;
2020-03-11 18:30:43 -07:00
this . location = getCallerLocation ( _ _filename ) ;
2019-11-18 18:18:28 -08:00
this . beforeAll = null ;
this . beforeEach = null ;
this . afterAll = null ;
this . afterEach = null ;
}
}
2020-03-12 17:32:53 -07:00
class Result {
constructor ( ) {
this . result = TestResult . Ok ;
this . exitCode = 0 ;
this . message = '' ;
this . errors = [ ] ;
}
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 ) {
const data = { message , error , tests : [ ] } ;
if ( worker ) {
data . workerId = worker . _workerId ;
data . tests = worker . _runTests . slice ( ) ;
}
this . errors . push ( data ) ;
}
ok ( ) {
return this . result === TestResult . Ok ;
}
}
2020-03-10 11:30:02 -07:00
class TestWorker {
constructor ( testPass , workerId , parallelIndex ) {
this . _testPass = testPass ;
this . _state = { parallelIndex } ;
this . _suiteStack = [ ] ;
2020-03-12 17:32:53 -07:00
this . _terminating = false ;
2020-03-10 11:30:02 -07:00
this . _workerId = workerId ;
2020-03-12 17:32:53 -07:00
this . _runningTestCallback = null ;
this . _runningHookCallback = null ;
this . _runTests = [ ] ;
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 ;
if ( this . _runningTestCallback )
this . _runningTestCallback . terminate ( ) ;
if ( terminateHooks && this . _runningHookCallback )
this . _runningHookCallback . terminate ( ) ;
2020-03-10 11:30:02 -07:00
}
2019-11-18 18:18:28 -08:00
2020-03-10 11:30:02 -07:00
_markTerminated ( test ) {
2020-03-12 17:32:53 -07:00
if ( ! this . _terminating )
2020-03-10 11:30:02 -07:00
return false ;
test . result = TestResult . Terminated ;
return true ;
}
async runTest ( test ) {
2020-03-12 17:32:53 -07:00
this . _runTests . push ( test ) ;
2020-03-10 11:30:02 -07:00
if ( this . _markTerminated ( test ) )
return ;
2020-03-15 23:10:49 -07:00
if ( test . declaredMode === TestMode . Skip ) {
2020-03-10 11:30:02 -07:00
await this . _testPass . _willStartTest ( this , test ) ;
2020-03-15 23:10:49 -07:00
test . result = TestResult . Skipped ;
2020-03-10 11:30:02 -07:00
await this . _testPass . _didFinishTest ( this , test ) ;
return ;
}
2020-03-16 19:12:52 -07:00
if ( test . expectation === TestExpectation . Fail && test . declaredMode !== TestMode . Focus ) {
2020-03-10 11:30:02 -07:00
await this . _testPass . _willStartTest ( this , test ) ;
2020-03-15 23:10:49 -07:00
test . result = TestResult . MarkedAsFailing ;
2020-03-10 11:30:02 -07:00
await this . _testPass . _didFinishTest ( this , test ) ;
return ;
2019-11-18 18:18:28 -08:00
}
2020-03-10 11:30:02 -07:00
const suiteStack = [ ] ;
for ( let suite = test . suite ; suite ; suite = suite . parentSuite )
suiteStack . push ( suite ) ;
suiteStack . reverse ( ) ;
let common = 0 ;
while ( common < suiteStack . length && this . _suiteStack [ common ] === suiteStack [ common ] )
common ++ ;
while ( this . _suiteStack . length > common ) {
if ( this . _markTerminated ( test ) )
return ;
const suite = this . _suiteStack . pop ( ) ;
if ( ! await this . _runHook ( test , suite , 'afterAll' ) )
return ;
}
while ( this . _suiteStack . length < suiteStack . length ) {
if ( this . _markTerminated ( test ) )
return ;
const suite = suiteStack [ this . _suiteStack . length ] ;
this . _suiteStack . push ( suite ) ;
if ( ! await this . _runHook ( test , suite , 'beforeAll' ) )
return ;
}
if ( this . _markTerminated ( test ) )
return ;
// From this point till the end, we have to run all hooks
// no matter what happens.
await this . _testPass . _willStartTest ( this , test ) ;
for ( let i = 0 ; i < this . _suiteStack . length ; i ++ )
await this . _runHook ( test , this . _suiteStack [ i ] , 'beforeEach' ) ;
if ( ! test . error && ! this . _markTerminated ( test ) ) {
await this . _testPass . _willStartTestBody ( this , test ) ;
2020-03-12 17:32:53 -07:00
this . _runningTestCallback = test . _userCallback ;
2020-03-10 11:30:02 -07:00
test . error = await test . _userCallback . run ( this . _state , test ) ;
2020-03-17 09:04:44 -07:00
if ( test . error && test . error . stack )
await this . _testPass . _runner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( test . error ) ;
2020-03-12 17:32:53 -07:00
this . _runningTestCallback = null ;
2020-03-10 11:30:02 -07:00
if ( ! test . error )
test . result = TestResult . Ok ;
else if ( test . error === TimeoutError )
test . result = TestResult . TimedOut ;
else if ( test . error === TerminatedError )
test . result = TestResult . Terminated ;
else
test . result = TestResult . Failed ;
await this . _testPass . _didFinishTestBody ( this , test ) ;
}
for ( let i = this . _suiteStack . length - 1 ; i >= 0 ; i -- )
await this . _runHook ( test , this . _suiteStack [ i ] , 'afterEach' ) ;
await this . _testPass . _didFinishTest ( this , test ) ;
}
async _runHook ( test , suite , hookName ) {
const hook = suite [ hookName ] ;
if ( ! hook )
return true ;
await this . _testPass . _willStartHook ( this , suite , hook , hookName ) ;
2020-03-12 17:32:53 -07:00
this . _runningHookCallback = hook ;
2020-03-10 11:30:02 -07:00
let error = await hook . run ( this . _state , test ) ;
2020-03-12 17:32:53 -07:00
this . _runningHookCallback = null ;
2020-03-10 11:30:02 -07:00
if ( error ) {
const location = ` ${ hook . location . fileName } : ${ hook . location . lineNumber } : ${ hook . location . columnNumber } ` ;
if ( test . result !== TestResult . Terminated ) {
// Prefer terminated result over any hook failures.
test . result = error === TerminatedError ? TestResult . Terminated : TestResult . Crashed ;
}
2020-03-12 17:32:53 -07:00
let message ;
2020-03-10 11:30:02 -07:00
if ( error === TimeoutError ) {
2020-03-12 17:32:53 -07:00
message = ` ${ location } - Timeout Exceeded ${ hook . timeout } ms while running " ${ hookName } " in suite " ${ suite . fullName } " ` ;
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 )
await this . _testPass . _runner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
2020-03-12 17:32:53 -07:00
message = ` ${ location } - FAILED while running " ${ hookName } " in suite " ${ suite . fullName } ": ` ;
2020-03-10 11:30:02 -07:00
}
2020-03-12 17:32:53 -07:00
await this . _testPass . _didFailHook ( this , suite , hook , hookName , message , error ) ;
2020-03-10 11:30:02 -07:00
test . error = error ;
return false ;
}
await this . _testPass . _didCompleteHook ( this , suite , hook , hookName ) ;
return true ;
}
async shutdown ( ) {
while ( this . _suiteStack . length > 0 ) {
const suite = this . _suiteStack . pop ( ) ;
await this . _runHook ( { } , suite , 'afterAll' ) ;
}
}
}
class TestPass {
constructor ( runner , parallel , breakOnFailure ) {
this . _runner = runner ;
this . _workers = [ ] ;
2020-03-12 17:32:53 -07:00
this . _nextWorkerId = 1 ;
2020-03-10 11:30:02 -07:00
this . _parallel = parallel ;
this . _breakOnFailure = breakOnFailure ;
2020-03-12 17:32:53 -07:00
this . _errors = [ ] ;
this . _result = new Result ( ) ;
this . _terminating = false ;
2019-11-18 18:18:28 -08:00
}
2020-03-10 11:30:02 -07:00
async run ( testList ) {
2019-11-18 18:18:28 -08: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' ) ,
2020-03-10 11:16:54 -07:00
createTermination . call ( this , 'uncaughtException' , TestResult . Crashed , 'UNHANDLED ERROR' ) ,
2019-11-18 18:18:28 -08:00
] ;
for ( const termination of terminations )
process . on ( termination . event , termination . handler ) ;
2020-03-10 11:30:02 -07:00
for ( const test of testList ) {
test . result = null ;
test . error = null ;
}
2020-03-12 17:32:53 -07:00
this . _result = new Result ( ) ;
2020-03-10 11:30:02 -07:00
const parallel = Math . min ( this . _parallel , testList . length ) ;
2019-11-18 18:18:28 -08:00
const workerPromises = [ ] ;
2020-03-10 11:30:02 -07:00
for ( let i = 0 ; i < parallel ; ++ i ) {
const initialTestIndex = i * Math . floor ( testList . length / parallel ) ;
workerPromises . push ( this . _runWorker ( initialTestIndex , testList , i ) ) ;
}
2019-11-18 18:18:28 -08:00
await Promise . all ( workerPromises ) ;
for ( const termination of terminations )
process . removeListener ( termination . event , termination . handler ) ;
2020-03-12 17:32:53 -07:00
if ( this . _runner . failedTests ( ) . length )
this . _result . setResult ( TestResult . Failed , '' ) ;
return this . _result ;
2019-11-18 18:18:28 -08:00
function createTermination ( event , result , message ) {
return {
event ,
message ,
2020-03-12 17:32:53 -07:00
handler : error => this . _terminate ( result , message , event === 'SIGTERM' , event . startsWith ( 'SIG' ) ? null : error )
2019-11-18 18:18:28 -08:00
} ;
}
}
2020-03-10 11:30:02 -07:00
async _runWorker ( testIndex , testList , parallelIndex ) {
let worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
2020-03-12 17:32:53 -07:00
while ( ! worker . _terminating ) {
2020-03-10 11:30:02 -07:00
let skipped = 0 ;
while ( skipped < testList . length && testList [ testIndex ] . result !== null ) {
testIndex = ( testIndex + 1 ) % testList . length ;
skipped ++ ;
}
const test = testList [ testIndex ] ;
if ( test . result !== null ) {
// All tests have been run.
2019-11-18 18:18:28 -08:00
break ;
}
2020-03-10 11:30:02 -07:00
// Mark as running so that other workers do not run it again.
test . result = 'running' ;
await worker . runTest ( test ) ;
if ( isTestFailure ( test . result ) ) {
// Something went wrong during test run, let's use a fresh worker.
await worker . shutdown ( ) ;
if ( this . _breakOnFailure ) {
2020-03-12 17:32:53 -07:00
const message = ` Terminating because a test has failed and |testRunner.breakOnFailure| is enabled ` ;
await this . _terminate ( TestResult . Terminated , message , false /* force */ , null /* error */ ) ;
2020-03-10 11:30:02 -07:00
return ;
}
worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
}
2019-11-18 18:18:28 -08:00
}
2020-03-10 11:30:02 -07:00
await worker . shutdown ( ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-12 17:32:53 -07:00
async _terminate ( result , message , force , error ) {
debug ( 'testrunner' ) ( ` TERMINATED result = ${ result } , message = ${ message } ` ) ;
2020-03-10 11:30:02 -07:00
for ( const worker of this . _workers )
2020-03-12 17:32:53 -07:00
worker . terminate ( force /* terminateHooks */ ) ;
this . _result . setResult ( result , message ) ;
2020-03-24 14:40:59 -07:00
if ( this . _result . message === 'SIGINT received' && message === 'SIGTERM received' )
this . _result . message = message ;
2020-03-12 17:32:53 -07:00
if ( error ) {
if ( error . stack )
await this . _runner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
2020-03-23 14:45:21 -07:00
this . _result . addError ( message , error , this . _workers . length === 1 ? this . _workers [ 0 ] : null ) ;
2020-03-12 17:32:53 -07:00
}
2019-11-18 18:18:28 -08:00
}
2020-03-10 11:30:02 -07:00
async _willStartTest ( worker , test ) {
test . startTimestamp = Date . now ( ) ;
this . _runner . emit ( TestRunner . Events . TestStarted , test , worker . _workerId ) ;
}
async _didFinishTest ( worker , test ) {
test . endTimestamp = Date . now ( ) ;
this . _runner . emit ( TestRunner . Events . TestFinished , test , worker . _workerId ) ;
}
async _willStartTestBody ( worker , test ) {
debug ( 'testrunner:test' ) ( ` [ ${ worker . _workerId } ] starting " ${ test . fullName } " ( ${ test . location . fileName + ':' + test . location . lineNumber } ) ` ) ;
}
async _didFinishTestBody ( worker , test ) {
debug ( 'testrunner:test' ) ( ` [ ${ worker . _workerId } ] ${ test . result . toUpperCase ( ) } " ${ test . fullName } " ( ${ test . location . fileName + ':' + test . location . lineNumber } ) ` ) ;
}
async _willStartHook ( worker , suite , hook , hookName ) {
debug ( 'testrunner:hook' ) ( ` [ ${ worker . _workerId } ] " ${ hookName } " started for " ${ suite . fullName } " ( ${ hook . location . fileName + ':' + hook . location . lineNumber } ) ` ) ;
}
2020-03-12 17:32:53 -07:00
async _didFailHook ( worker , suite , hook , hookName , message , error ) {
2020-03-10 11:30:02 -07:00
debug ( 'testrunner:hook' ) ( ` [ ${ worker . _workerId } ] " ${ hookName } " FAILED for " ${ suite . fullName } " ( ${ hook . location . fileName + ':' + hook . location . lineNumber } ) ` ) ;
2020-03-24 14:40:59 -07:00
if ( message )
this . _result . addError ( message , error , worker ) ;
2020-03-12 17:32:53 -07:00
this . _result . setResult ( TestResult . Crashed , message ) ;
2020-03-10 11:30:02 -07:00
}
async _didCompleteHook ( worker , suite , hook , hookName ) {
debug ( 'testrunner:hook' ) ( ` [ ${ worker . _workerId } ] " ${ hookName } " OK for " ${ suite . fullName } " ( ${ hook . location . fileName + ':' + hook . location . lineNumber } ) ` ) ;
}
2019-11-18 18:18:28 -08:00
}
2020-03-25 22:42:09 -07:00
// TODO: merge spec with Test/Suite.
function createSpec ( name , callback ) {
let timeout = INFINITE _TIMEOUT ;
2020-03-02 11:18:42 -08:00
let repeat = 1 ;
2020-03-25 22:42:09 -07:00
let expectation = TestExpectation . Ok ;
let mode = TestMode . Run ;
2020-03-25 14:40:57 -07:00
const spec = {
Modes : { ... TestMode } ,
Expectations : { ... TestExpectation } ,
2020-03-25 22:42:09 -07:00
name ( ) {
return name ;
} ,
callback ( ) {
return callback ;
} ,
2020-03-25 14:40:57 -07:00
mode ( ) {
return mode ;
} ,
setMode ( m ) {
if ( mode !== TestMode . Focus )
mode = m ;
} ,
expectations ( ) {
return [ expectation ] ;
} ,
setExpectations ( e ) {
if ( Array . isArray ( e ) ) {
if ( e . length > 1 )
2020-03-25 22:42:09 -07:00
throw new Error ( 'Only a single expectation is currently supported' ) ;
2020-03-25 14:40:57 -07:00
e = e [ 0 ] ;
}
expectation = e ;
} ,
timeout ( ) {
return timeout ;
} ,
setTimeout ( t ) {
timeout = t ;
} ,
repeat ( ) {
return repeat ;
} ,
setRepeat ( r ) {
repeat = r ;
2020-03-25 22:42:09 -07:00
} ,
2020-03-02 11:18:42 -08:00
} ;
2020-03-25 22:42:09 -07:00
return spec ;
2020-03-02 11:18:42 -08:00
}
2019-11-18 18:18:28 -08:00
class TestRunner extends EventEmitter {
constructor ( options = { } ) {
super ( ) ;
const {
timeout = 10 * 1000 , // Default timeout is 10 seconds.
parallel = 1 ,
breakOnFailure = false ,
2020-03-11 18:30:43 -07:00
crashIfTestsAreFocusedOnCI = true ,
2019-11-18 18:18:28 -08:00
disableTimeoutWhenInspectorIsEnabled = true ,
} = options ;
2020-03-11 18:30:43 -07:00
this . _crashIfTestsAreFocusedOnCI = crashIfTestsAreFocusedOnCI ;
2020-01-08 16:16:54 +00:00
this . _sourceMapSupport = new SourceMapSupport ( ) ;
2019-11-18 18:18:28 -08:00
this . _rootSuite = new Suite ( null , '' , TestMode . Run ) ;
this . _currentSuite = this . _rootSuite ;
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-25 22:42:09 -07:00
this . _modifiers = new Map ( ) ;
this . _attributes = new Map ( ) ;
2019-11-18 18:18:28 -08:00
if ( MAJOR _NODEJS _VERSION >= 8 && disableTimeoutWhenInspectorIsEnabled ) {
if ( inspector . url ( ) ) {
console . log ( 'TestRunner detected inspector; overriding certain properties to be debugger-friendly' ) ;
console . log ( ' - timeout = 0 (Infinite)' ) ;
2019-12-19 15:47:35 -08:00
this . _timeout = INFINITE _TIMEOUT ;
2019-11-18 18:18:28 -08:00
}
}
2020-03-02 13:47:08 -08:00
this . _debuggerLogBreakpointLines = new Multimap ( ) ;
2019-12-19 15:47:35 -08:00
2019-11-18 18:18:28 -08:00
this . beforeAll = this . _addHook . bind ( this , 'beforeAll' ) ;
this . beforeEach = this . _addHook . bind ( this , 'beforeEach' ) ;
this . afterAll = this . _addHook . bind ( this , 'afterAll' ) ;
this . afterEach = this . _addHook . bind ( this , 'afterEach' ) ;
2020-03-25 14:40:57 -07:00
2020-03-25 22:42:09 -07:00
this . describe = this . _specBuilder ( [ ] , true ) ;
this . it = this . _specBuilder ( [ ] , false ) ;
this . _attributes . set ( 'debug' , t => {
t . setMode ( t . Modes . Focus ) ;
t . setTimeout ( INFINITE _TIMEOUT ) ;
const N = t . callback ( ) . toString ( ) . split ( '\n' ) . length ;
const location = getCallerLocation ( _ _filename ) ;
for ( let line = 0 ; line < N ; ++ line )
this . _debuggerLogBreakpointLines . set ( location . filePath , line + location . lineNumber ) ;
2020-03-25 14:40:57 -07:00
} ) ;
2020-03-25 22:42:09 -07:00
this . _modifiers . set ( 'skip' , ( t , condition ) => condition && t . setMode ( t . Modes . Skip ) ) ;
this . _modifiers . set ( 'fail' , ( t , condition ) => condition && t . setExpectations ( t . Expectations . Fail ) ) ;
this . _modifiers . set ( 'slow' , ( t , condition ) => condition && t . setTimeout ( t . timeout ( ) * 3 ) ) ;
this . _modifiers . set ( 'repeat' , ( t , count ) => t . setRepeat ( count ) ) ;
this . _attributes . set ( 'focus' , t => t . setMode ( t . Modes . Focus ) ) ;
this . fdescribe = this . describe . focus ;
this . xdescribe = this . describe . skip ( true ) ;
this . fit = this . it . focus ;
this . xit = this . it . skip ( true ) ;
this . dit = this . it . debug ;
}
_specBuilder ( callbacks , isSuite ) {
return new Proxy ( ( ) => { } , {
apply : ( target , thisArg , [ name , callback ] ) => {
const spec = createSpec ( name , callback ) ;
spec . setTimeout ( this . _timeout ) ;
for ( const { callback , args } of callbacks )
callback ( spec , ... args ) ;
if ( isSuite )
this . _addSuite ( spec , [ ] ) ;
else
this . _addTest ( spec ) ;
} ,
get : ( obj , prop ) => {
if ( this . _modifiers . has ( prop ) )
return ( ... args ) => this . _specBuilder ( [ ... callbacks , { callback : this . _modifiers . get ( prop ) , args } ] , isSuite ) ;
if ( this . _attributes . has ( prop ) )
return this . _specBuilder ( [ ... callbacks , { callback : this . _attributes . get ( prop ) , args : [ ] } ] , isSuite ) ;
return obj [ prop ] ;
} ,
} ) ;
2020-03-25 14:40:57 -07:00
}
modifier ( name , callback ) {
2020-03-25 22:42:09 -07:00
this . _modifiers . set ( name , callback ) ;
2020-03-25 14:40:57 -07:00
}
attribute ( name , callback ) {
2020-03-25 22:42:09 -07:00
this . _attributes . set ( name , callback ) ;
2019-11-18 18:18:28 -08:00
}
2019-12-18 17:11:45 -08:00
loadTests ( module , ... args ) {
2020-03-25 22:42:09 -07:00
if ( typeof module . describe === 'function' ) {
const spec = createSpec ( '' , module . describe ) ;
spec . setMode ( spec . Modes . Run ) ;
this . _addSuite ( spec , args ) ;
}
if ( typeof module . fdescribe === 'function' ) {
const spec = createSpec ( '' , module . fdescribe ) ;
spec . setMode ( spec . Modes . Focus ) ;
this . _addSuite ( spec , args ) ;
}
if ( typeof module . xdescribe === 'function' ) {
const spec = createSpec ( '' , module . xdescribe ) ;
spec . setMode ( spec . Modes . Skip ) ;
this . _addSuite ( spec , args ) ;
}
}
_addTest ( spec ) {
for ( let i = 0 ; i < spec . repeat ( ) ; i ++ ) {
let expectation = spec . expectations ( ) [ 0 ] ;
let mode = spec . mode ( ) ;
for ( let suite = this . _currentSuite ; suite ; suite = suite . parentSuite ) {
if ( suite . expectation === TestExpectation . Fail )
expectation = TestExpectation . Fail ;
if ( suite . declaredMode === TestMode . Skip )
mode = TestMode . Skip ;
}
const test = new Test ( this . _currentSuite , spec . name ( ) , spec . callback ( ) , mode , expectation , spec . timeout ( ) ) ;
this . _currentSuite . children . push ( test ) ;
this . _tests . push ( test ) ;
}
}
_addSuite ( spec , args ) {
for ( let i = 0 ; i < spec . repeat ( ) ; i ++ ) {
const oldSuite = this . _currentSuite ;
const suite = new Suite ( this . _currentSuite , spec . name ( ) , spec . mode ( ) , spec . expectations ( ) [ 0 ] ) ;
this . _suites . push ( suite ) ;
this . _currentSuite . children . push ( suite ) ;
this . _currentSuite = suite ;
spec . callback ( ) ( ... args ) ;
this . _currentSuite = oldSuite ;
2020-03-15 23:10:49 -07:00
}
2019-11-18 18:18:28 -08:00
}
_addHook ( hookName , callback ) {
assert ( this . _currentSuite [ hookName ] === null , ` Only one ${ hookName } hook available per suite ` ) ;
const hook = new UserCallback ( callback , this . _timeout ) ;
this . _currentSuite [ hookName ] = hook ;
}
2020-03-12 17:32:53 -07:00
async run ( options = { } ) {
const { totalTimeout = 0 } = options ;
2019-12-19 15:47:35 -08:00
let session = this . _debuggerLogBreakpointLines . size ? await setLogBreakpoints ( this . _debuggerLogBreakpointLines ) : null ;
2020-03-11 18:30:43 -07:00
const runnableTests = this . runnableTests ( ) ;
2019-11-18 18:18:28 -08:00
this . emit ( TestRunner . Events . Started , runnableTests ) ;
2020-03-11 18:30:43 -07:00
2020-03-12 17:32:53 -07:00
let result = new Result ( ) ;
2020-03-11 18:30:43 -07:00
if ( this . _crashIfTestsAreFocusedOnCI && process . env . CI && this . hasFocusedTestsOrSuites ( ) ) {
2020-03-12 17:32:53 -07:00
result . setResult ( TestResult . Crashed , '"focused" tests or suites are probitted on CI' ) ;
2019-11-18 18:18:28 -08:00
} else {
2020-03-12 17:32:53 -07:00
let timeoutId ;
const timeoutPromise = new Promise ( resolve => {
const timeoutResult = new Result ( ) ;
timeoutResult . setResult ( TestResult . Crashed , ` Total timeout of ${ totalTimeout } ms reached. ` ) ;
if ( totalTimeout )
timeoutId = setTimeout ( resolve . bind ( null , timeoutResult ) , totalTimeout ) ;
2020-03-11 18:30:43 -07:00
} ) ;
2020-03-12 17:32:53 -07:00
try {
this . _runningPass = new TestPass ( this , this . _parallel , this . _breakOnFailure ) ;
result = await Promise . race ( [
this . _runningPass . run ( runnableTests ) . catch ( e => { console . error ( e ) ; throw e ; } ) ,
timeoutPromise ,
] ) ;
this . _runningPass = null ;
} finally {
clearTimeout ( timeoutId ) ;
2020-01-13 15:30:16 -08:00
}
2019-11-18 18:18:28 -08:00
}
this . emit ( TestRunner . Events . Finished , result ) ;
2019-12-19 15:47:35 -08:00
if ( session )
session . disconnect ( ) ;
2019-11-18 18:18:28 -08:00
return result ;
}
2020-01-09 16:37:19 +00:00
async terminate ( ) {
2019-11-18 18:18:28 -08:00
if ( ! this . _runningPass )
return ;
2020-03-12 17:32:53 -07:00
await this . _runningPass . _terminate ( TestResult . Terminated , 'Terminated with |TestRunner.terminate()| call' , true /* force */ , null /* error */ ) ;
2019-11-18 18:18:28 -08:00
}
timeout ( ) {
return this . _timeout ;
}
2020-03-11 18:30:43 -07:00
runnableTests ( ) {
if ( ! this . hasFocusedTestsOrSuites ( ) )
2019-11-18 18:18:28 -08:00
return this . _tests ;
const tests = [ ] ;
const blacklistSuites = new Set ( ) ;
// First pass: pick "fit" and blacklist parent suites
2020-03-10 11:30:02 -07:00
for ( let i = 0 ; i < this . _tests . length ; i ++ ) {
const test = this . _tests [ i ] ;
2019-11-18 18:18:28 -08:00
if ( test . declaredMode !== TestMode . Focus )
continue ;
2020-03-10 11:30:02 -07:00
tests . push ( { i , test } ) ;
2019-11-18 18:18:28 -08:00
for ( let suite = test . suite ; suite ; suite = suite . parentSuite )
blacklistSuites . add ( suite ) ;
}
// Second pass: pick all tests that belong to non-blacklisted "fdescribe"
2020-03-10 11:30:02 -07:00
for ( let i = 0 ; i < this . _tests . length ; i ++ ) {
const test = this . _tests [ i ] ;
2019-11-18 18:18:28 -08:00
let insideFocusedSuite = false ;
for ( let suite = test . suite ; suite ; suite = suite . parentSuite ) {
if ( ! blacklistSuites . has ( suite ) && suite . declaredMode === TestMode . Focus ) {
insideFocusedSuite = true ;
break ;
}
}
if ( insideFocusedSuite )
2020-03-10 11:30:02 -07:00
tests . push ( { i , test } ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-10 11:30:02 -07:00
tests . sort ( ( a , b ) => a . i - b . i ) ;
return tests . map ( t => t . test ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-11 18:30:43 -07:00
focusedSuites ( ) {
2020-03-15 23:10:49 -07:00
return this . _suites . filter ( suite => suite . declaredMode === TestMode . Focus ) ;
2020-03-11 18:30:43 -07:00
}
focusedTests ( ) {
2020-03-15 23:10:49 -07:00
return this . _tests . filter ( test => test . declaredMode === TestMode . Focus ) ;
2020-03-11 18:30:43 -07:00
}
2019-11-18 18:18:28 -08:00
hasFocusedTestsOrSuites ( ) {
2020-03-11 18:30:43 -07:00
return ! ! this . focusedTests ( ) . length || ! ! this . focusedSuites ( ) . length ;
}
focusMatchingTests ( fullNameRegex ) {
for ( const test of this . _tests ) {
if ( fullNameRegex . test ( test . fullName ) )
2020-03-15 23:10:49 -07:00
test . declaredMode = TestMode . Focus ;
2020-03-11 18:30:43 -07:00
}
2019-11-18 18:18:28 -08:00
}
tests ( ) {
return this . _tests . slice ( ) ;
}
failedTests ( ) {
2020-03-15 23:10:49 -07:00
return this . _tests . filter ( test => test . result === TestResult . Failed || test . result === TestResult . TimedOut || test . result === TestResult . Crashed ) ;
2019-11-18 18:18:28 -08:00
}
passedTests ( ) {
2020-03-15 23:10:49 -07:00
return this . _tests . filter ( test => test . result === TestResult . Ok ) ;
2019-11-18 18:18:28 -08:00
}
skippedTests ( ) {
2020-03-15 23:10:49 -07:00
return this . _tests . filter ( test => test . result === TestResult . Skipped ) ;
2019-11-18 18:18:28 -08:00
}
2020-03-02 14:57:09 -08:00
markedAsFailingTests ( ) {
2020-03-15 23:10:49 -07:00
return this . _tests . filter ( test => test . result === TestResult . MarkedAsFailing ) ;
2020-03-02 14:57:09 -08:00
}
2019-11-18 18:18:28 -08:00
parallel ( ) {
return this . _parallel ;
}
}
2019-12-19 15:47:35 -08:00
async function setLogBreakpoints ( debuggerLogBreakpoints ) {
const session = new inspector . Session ( ) ;
session . connect ( ) ;
const postAsync = util . promisify ( session . post . bind ( session ) ) ;
await postAsync ( 'Debugger.enable' ) ;
const setBreakpointCommands = [ ] ;
for ( const filePath of debuggerLogBreakpoints . keysArray ( ) ) {
const lineNumbers = debuggerLogBreakpoints . get ( filePath ) ;
const lines = ( await readFileAsync ( filePath , 'utf8' ) ) . split ( '\n' ) ;
for ( const lineNumber of lineNumbers ) {
setBreakpointCommands . push ( postAsync ( 'Debugger.setBreakpointByUrl' , {
url : url . pathToFileURL ( filePath ) ,
lineNumber ,
condition : ` console.log(' ${ String ( lineNumber + 1 ) . padStart ( 6 , ' ' ) } | ' + ${ JSON . stringify ( lines [ lineNumber ] ) } ) ` ,
} ) . catch ( e => { } ) ) ;
} ;
}
await Promise . all ( setBreakpointCommands ) ;
return session ;
}
2019-11-18 18:18:28 -08:00
/ * *
* @ param { * } value
* @ param { string = } message
* /
function assert ( value , message ) {
if ( ! value )
throw new Error ( message ) ;
}
TestRunner . Events = {
Started : 'started' ,
Finished : 'finished' ,
TestStarted : 'teststarted' ,
TestFinished : 'testfinished' ,
} ;
module . exports = TestRunner ;