2019-11-19 05:18:28 +03: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-20 02:47:35 +03:00
const util = require ( 'util' ) ;
const url = require ( 'url' ) ;
const inspector = require ( 'inspector' ) ;
2019-11-19 05:18:28 +03:00
const path = require ( 'path' ) ;
const EventEmitter = require ( 'events' ) ;
const Multimap = require ( './Multimap' ) ;
2019-12-20 02:47:35 +03:00
const fs = require ( 'fs' ) ;
2020-01-08 19:16:54 +03:00
const { SourceMapSupport } = require ( './SourceMapSupport' ) ;
2020-02-15 02:21:08 +03:00
const debug = require ( 'debug' ) ;
2020-02-21 09:55:39 +03:00
const { getCallerLocation } = require ( './utils' ) ;
2019-12-20 02:47:35 +03:00
const INFINITE _TIMEOUT = 2147483647 ;
const readFileAsync = util . promisify ( fs . readFile . bind ( fs ) ) ;
2019-11-19 05:18:28 +03: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-21 09:55:39 +03:00
this . location = getCallerLocation ( _ _filename ) ;
2019-11-19 05:18:28 +03: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 22:18:42 +03:00
Focus : 'focus' ,
2020-03-03 01:57:09 +03:00
MarkAsFailing : 'markAsFailing' ,
2020-03-02 22:18:42 +03:00
Flake : 'flake'
2019-11-19 05:18:28 +03:00
} ;
const TestResult = {
Ok : 'ok' ,
2020-03-03 01:57:09 +03:00
MarkedAsFailing : 'markedAsFailing' , // User marked as failed
Skipped : 'skipped' , // User marked as skipped
2019-11-19 05:18:28 +03: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 21:30:02 +03:00
function isTestFailure ( testResult ) {
return testResult === TestResult . Failed || testResult === TestResult . TimedOut || testResult === TestResult . Crashed ;
}
2019-11-19 05:18:28 +03:00
class Test {
2019-12-19 04:11:45 +03:00
constructor ( suite , name , callback , declaredMode , timeout ) {
2019-11-19 05:18:28 +03:00
this . suite = suite ;
this . name = name ;
this . fullName = ( suite . fullName + ' ' + name ) . trim ( ) ;
this . declaredMode = declaredMode ;
this . _userCallback = new UserCallback ( callback , timeout ) ;
this . location = this . _userCallback . location ;
2020-02-21 09:55:39 +03:00
this . timeout = timeout ;
2019-11-19 05:18:28 +03:00
// Test results
this . result = null ;
this . error = null ;
this . startTimestamp = 0 ;
this . endTimestamp = 0 ;
}
}
class Suite {
2019-12-19 04:11:45 +03:00
constructor ( parentSuite , name , declaredMode ) {
2019-11-19 05:18:28 +03:00
this . parentSuite = parentSuite ;
this . name = name ;
this . fullName = ( parentSuite ? parentSuite . fullName + ' ' + name : name ) . trim ( ) ;
this . declaredMode = declaredMode ;
/** @type {!Array<(!Test|!Suite)>} */
this . children = [ ] ;
2020-03-12 04:30:43 +03:00
this . location = getCallerLocation ( _ _filename ) ;
2019-11-19 05:18:28 +03:00
this . beforeAll = null ;
this . beforeEach = null ;
this . afterAll = null ;
this . afterEach = null ;
}
}
2020-03-13 03:32:53 +03: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 21:30:02 +03:00
class TestWorker {
constructor ( testPass , workerId , parallelIndex ) {
this . _testPass = testPass ;
this . _state = { parallelIndex } ;
this . _suiteStack = [ ] ;
2020-03-13 03:32:53 +03:00
this . _terminating = false ;
2020-03-10 21:30:02 +03:00
this . _workerId = workerId ;
2020-03-13 03:32:53 +03:00
this . _runningTestCallback = null ;
this . _runningHookCallback = null ;
this . _runTests = [ ] ;
2020-03-10 21:30:02 +03:00
}
2019-11-19 05:18:28 +03:00
2020-03-13 03:32:53 +03:00
terminate ( terminateHooks ) {
this . _terminating = true ;
if ( this . _runningTestCallback )
this . _runningTestCallback . terminate ( ) ;
if ( terminateHooks && this . _runningHookCallback )
this . _runningHookCallback . terminate ( ) ;
2020-03-10 21:30:02 +03:00
}
2019-11-19 05:18:28 +03:00
2020-03-10 21:30:02 +03:00
_markTerminated ( test ) {
2020-03-13 03:32:53 +03:00
if ( ! this . _terminating )
2020-03-10 21:30:02 +03:00
return false ;
test . result = TestResult . Terminated ;
return true ;
}
async runTest ( test ) {
2020-03-13 03:32:53 +03:00
this . _runTests . push ( test ) ;
2020-03-10 21:30:02 +03:00
if ( this . _markTerminated ( test ) )
return ;
if ( test . declaredMode === TestMode . MarkAsFailing ) {
await this . _testPass . _willStartTest ( this , test ) ;
test . result = TestResult . MarkedAsFailing ;
await this . _testPass . _didFinishTest ( this , test ) ;
return ;
}
if ( test . declaredMode === TestMode . Skip ) {
await this . _testPass . _willStartTest ( this , test ) ;
test . result = TestResult . Skipped ;
await this . _testPass . _didFinishTest ( this , test ) ;
return ;
2019-11-19 05:18:28 +03:00
}
2020-03-10 21:30:02 +03: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-13 03:32:53 +03:00
this . _runningTestCallback = test . _userCallback ;
2020-03-10 21:30:02 +03:00
test . error = await test . _userCallback . run ( this . _state , test ) ;
2020-03-13 03:32:53 +03:00
this . _runningTestCallback = null ;
2020-03-10 21:30:02 +03: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-13 03:32:53 +03:00
this . _runningHookCallback = hook ;
2020-03-10 21:30:02 +03:00
let error = await hook . run ( this . _state , test ) ;
2020-03-13 03:32:53 +03:00
this . _runningHookCallback = null ;
2020-03-10 21:30:02 +03: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-13 03:32:53 +03:00
let message ;
2020-03-10 21:30:02 +03:00
if ( error === TimeoutError ) {
2020-03-13 03:32:53 +03:00
message = ` ${ location } - Timeout Exceeded ${ hook . timeout } ms while running " ${ hookName } " in suite " ${ suite . fullName } " ` ;
error = null ;
2020-03-10 21:30:02 +03:00
} else if ( error === TerminatedError ) {
2020-03-13 03:32:53 +03:00
message = ` ${ location } - TERMINATED while running " ${ hookName } " in suite " ${ suite . fullName } " ` ;
error = null ;
2020-03-10 21:30:02 +03:00
} else {
if ( error . stack )
await this . _testPass . _runner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
2020-03-13 03:32:53 +03:00
message = ` ${ location } - FAILED while running " ${ hookName } " in suite " ${ suite . fullName } ": ` ;
2020-03-10 21:30:02 +03:00
}
2020-03-13 03:32:53 +03:00
await this . _testPass . _didFailHook ( this , suite , hook , hookName , message , error ) ;
2020-03-10 21:30:02 +03: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-13 03:32:53 +03:00
this . _nextWorkerId = 1 ;
2020-03-10 21:30:02 +03:00
this . _parallel = parallel ;
this . _breakOnFailure = breakOnFailure ;
2020-03-13 03:32:53 +03:00
this . _errors = [ ] ;
this . _result = new Result ( ) ;
this . _terminating = false ;
2019-11-19 05:18:28 +03:00
}
2020-03-10 21:30:02 +03:00
async run ( testList ) {
2019-11-19 05:18:28 +03: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 21:16:54 +03:00
createTermination . call ( this , 'uncaughtException' , TestResult . Crashed , 'UNHANDLED ERROR' ) ,
2019-11-19 05:18:28 +03:00
] ;
for ( const termination of terminations )
process . on ( termination . event , termination . handler ) ;
2020-03-10 21:30:02 +03:00
for ( const test of testList ) {
test . result = null ;
test . error = null ;
}
2020-03-13 03:32:53 +03:00
this . _result = new Result ( ) ;
2020-03-10 21:30:02 +03:00
const parallel = Math . min ( this . _parallel , testList . length ) ;
2019-11-19 05:18:28 +03:00
const workerPromises = [ ] ;
2020-03-10 21:30:02 +03: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-19 05:18:28 +03:00
await Promise . all ( workerPromises ) ;
for ( const termination of terminations )
process . removeListener ( termination . event , termination . handler ) ;
2020-03-13 03:32:53 +03:00
if ( this . _runner . failedTests ( ) . length )
this . _result . setResult ( TestResult . Failed , '' ) ;
return this . _result ;
2019-11-19 05:18:28 +03:00
function createTermination ( event , result , message ) {
return {
event ,
message ,
2020-03-13 03:32:53 +03:00
handler : error => this . _terminate ( result , message , event === 'SIGTERM' , event . startsWith ( 'SIG' ) ? null : error )
2019-11-19 05:18:28 +03:00
} ;
}
}
2020-03-10 21:30:02 +03:00
async _runWorker ( testIndex , testList , parallelIndex ) {
let worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
2020-03-13 03:32:53 +03:00
while ( ! worker . _terminating ) {
2020-03-10 21:30:02 +03: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-19 05:18:28 +03:00
break ;
}
2020-03-10 21:30:02 +03: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-13 03:32:53 +03: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 21:30:02 +03:00
return ;
}
worker = new TestWorker ( this , this . _nextWorkerId ++ , parallelIndex ) ;
this . _workers [ parallelIndex ] = worker ;
}
2019-11-19 05:18:28 +03:00
}
2020-03-10 21:30:02 +03:00
await worker . shutdown ( ) ;
2019-11-19 05:18:28 +03:00
}
2020-03-13 03:32:53 +03:00
async _terminate ( result , message , force , error ) {
debug ( 'testrunner' ) ( ` TERMINATED result = ${ result } , message = ${ message } ` ) ;
2020-03-10 21:30:02 +03:00
for ( const worker of this . _workers )
2020-03-13 03:32:53 +03:00
worker . terminate ( force /* terminateHooks */ ) ;
this . _result . setResult ( result , message ) ;
if ( error ) {
if ( error . stack )
await this . _runner . _sourceMapSupport . rewriteStackTraceWithSourceMaps ( error ) ;
this . _result . addError ( message , error ) ;
}
2019-11-19 05:18:28 +03:00
}
2020-03-10 21:30:02 +03: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-13 03:32:53 +03:00
async _didFailHook ( worker , suite , hook , hookName , message , error ) {
2020-03-10 21:30:02 +03:00
debug ( 'testrunner:hook' ) ( ` [ ${ worker . _workerId } ] " ${ hookName } " FAILED for " ${ suite . fullName } " ( ${ hook . location . fileName + ':' + hook . location . lineNumber } ) ` ) ;
2020-03-13 03:32:53 +03:00
this . _result . addError ( message , error , worker ) ;
this . _result . setResult ( TestResult . Crashed , message ) ;
2020-03-10 21:30:02 +03: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-19 05:18:28 +03:00
}
2020-03-07 02:52:24 +03:00
function specBuilder ( defaultTimeout , action ) {
2020-03-02 22:18:42 +03:00
let mode = TestMode . Run ;
let repeat = 1 ;
2020-03-07 02:52:24 +03:00
let timeout = defaultTimeout ;
2020-03-02 22:18:42 +03:00
const func = ( ... args ) => {
for ( let i = 0 ; i < repeat ; ++ i )
2020-03-07 02:52:24 +03:00
action ( mode , timeout , ... args ) ;
2020-03-02 22:18:42 +03:00
mode = TestMode . Run ;
repeat = 1 ;
} ;
func . skip = condition => {
if ( condition )
mode = TestMode . Skip ;
return func ;
} ;
func . fail = condition => {
if ( condition )
2020-03-03 01:57:09 +03:00
mode = TestMode . MarkAsFailing ;
2020-03-02 22:18:42 +03:00
return func ;
} ;
func . flake = condition => {
if ( condition )
mode = TestMode . Flake ;
return func ;
} ;
2020-03-07 02:52:24 +03:00
func . slow = ( ) => {
timeout = 3 * defaultTimeout ;
return func ;
}
2020-03-02 22:18:42 +03:00
func . repeat = count => {
repeat = count ;
return func ;
} ;
return func ;
}
2019-11-19 05:18:28 +03:00
class TestRunner extends EventEmitter {
constructor ( options = { } ) {
super ( ) ;
const {
timeout = 10 * 1000 , // Default timeout is 10 seconds.
parallel = 1 ,
breakOnFailure = false ,
2020-03-12 04:30:43 +03:00
crashIfTestsAreFocusedOnCI = true ,
2019-11-19 05:18:28 +03:00
disableTimeoutWhenInspectorIsEnabled = true ,
} = options ;
2020-03-12 04:30:43 +03:00
this . _crashIfTestsAreFocusedOnCI = crashIfTestsAreFocusedOnCI ;
2020-01-08 19:16:54 +03:00
this . _sourceMapSupport = new SourceMapSupport ( ) ;
2019-11-19 05:18:28 +03:00
this . _rootSuite = new Suite ( null , '' , TestMode . Run ) ;
this . _currentSuite = this . _rootSuite ;
this . _tests = [ ] ;
2020-03-12 04:30:43 +03:00
this . _suites = [ ] ;
2019-12-20 02:47:35 +03:00
this . _timeout = timeout === 0 ? INFINITE _TIMEOUT : timeout ;
2019-11-19 05:18:28 +03:00
this . _parallel = parallel ;
this . _breakOnFailure = breakOnFailure ;
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-20 02:47:35 +03:00
this . _timeout = INFINITE _TIMEOUT ;
2019-11-19 05:18:28 +03:00
}
}
2020-03-07 02:52:24 +03:00
this . describe = specBuilder ( this . _timeout , ( mode , timeout , ... args ) => this . _addSuite ( mode , ... args ) ) ;
this . fdescribe = specBuilder ( this . _timeout , ( mode , timeout , ... args ) => this . _addSuite ( TestMode . Focus , ... args ) ) ;
this . xdescribe = specBuilder ( this . _timeout , ( mode , timeout , ... args ) => this . _addSuite ( TestMode . Skip , ... args ) ) ;
this . it = specBuilder ( this . _timeout , ( mode , timeout , name , callback ) => this . _addTest ( name , callback , mode , timeout ) ) ;
this . fit = specBuilder ( this . _timeout , ( mode , timeout , name , callback ) => this . _addTest ( name , callback , TestMode . Focus , timeout ) ) ;
this . xit = specBuilder ( this . _timeout , ( mode , timeout , name , callback ) => this . _addTest ( name , callback , TestMode . Skip , timeout ) ) ;
this . dit = specBuilder ( this . _timeout , ( mode , timeout , name , callback ) => {
2019-12-20 02:47:35 +03:00
const test = this . _addTest ( name , callback , TestMode . Focus , INFINITE _TIMEOUT ) ;
const N = callback . toString ( ) . split ( '\n' ) . length ;
for ( let i = 0 ; i < N ; ++ i )
this . _debuggerLogBreakpointLines . set ( test . location . filePath , i + test . location . lineNumber ) ;
2020-03-03 00:47:08 +03:00
} ) ;
this . _debuggerLogBreakpointLines = new Multimap ( ) ;
2019-12-20 02:47:35 +03:00
2019-11-19 05:18:28 +03: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' ) ;
}
2019-12-19 04:11:45 +03:00
loadTests ( module , ... args ) {
if ( typeof module . describe === 'function' )
this . _addSuite ( TestMode . Run , '' , module . describe , ... args ) ;
if ( typeof module . fdescribe === 'function' )
this . _addSuite ( TestMode . Focus , '' , module . fdescribe , ... args ) ;
if ( typeof module . xdescribe === 'function' )
this . _addSuite ( TestMode . Skip , '' , module . xdescribe , ... args ) ;
2019-11-19 05:18:28 +03:00
}
2019-12-20 02:47:35 +03:00
_addTest ( name , callback , mode , timeout ) {
2019-11-19 05:18:28 +03:00
let suite = this . _currentSuite ;
2020-03-03 01:57:09 +03:00
let markedAsFailing = suite . declaredMode === TestMode . MarkAsFailing ;
while ( ( suite = suite . parentSuite ) )
markedAsFailing |= suite . declaredMode === TestMode . MarkAsFailing ;
if ( markedAsFailing )
mode = TestMode . MarkAsFailing ;
suite = this . _currentSuite ;
let skip = suite . declaredMode === TestMode . Skip ;
2019-11-19 05:18:28 +03:00
while ( ( suite = suite . parentSuite ) )
2020-03-03 01:57:09 +03:00
skip |= suite . declaredMode === TestMode . Skip ;
if ( skip )
mode = TestMode . Skip ;
2020-03-10 21:30:02 +03:00
2020-03-03 00:47:08 +03:00
const test = new Test ( this . _currentSuite , name , callback , mode , timeout ) ;
2019-11-19 05:18:28 +03:00
this . _currentSuite . children . push ( test ) ;
this . _tests . push ( test ) ;
2019-12-20 02:47:35 +03:00
return test ;
2019-11-19 05:18:28 +03:00
}
2019-12-19 04:11:45 +03:00
_addSuite ( mode , name , callback , ... args ) {
2019-11-19 05:18:28 +03:00
const oldSuite = this . _currentSuite ;
2019-12-19 04:11:45 +03:00
const suite = new Suite ( this . _currentSuite , name , mode ) ;
2020-03-12 04:30:43 +03:00
this . _suites . push ( suite ) ;
2019-11-19 05:18:28 +03:00
this . _currentSuite . children . push ( suite ) ;
this . _currentSuite = suite ;
2020-03-03 00:47:08 +03:00
callback ( ... args ) ;
2019-11-19 05:18:28 +03:00
this . _currentSuite = oldSuite ;
}
_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-13 03:32:53 +03:00
async run ( options = { } ) {
const { totalTimeout = 0 } = options ;
2019-12-20 02:47:35 +03:00
let session = this . _debuggerLogBreakpointLines . size ? await setLogBreakpoints ( this . _debuggerLogBreakpointLines ) : null ;
2020-03-12 04:30:43 +03:00
const runnableTests = this . runnableTests ( ) ;
2019-11-19 05:18:28 +03:00
this . emit ( TestRunner . Events . Started , runnableTests ) ;
2020-03-12 04:30:43 +03:00
2020-03-13 03:32:53 +03:00
let result = new Result ( ) ;
2020-03-12 04:30:43 +03:00
if ( this . _crashIfTestsAreFocusedOnCI && process . env . CI && this . hasFocusedTestsOrSuites ( ) ) {
2020-03-13 03:32:53 +03:00
result . setResult ( TestResult . Crashed , '"focused" tests or suites are probitted on CI' ) ;
2019-11-19 05:18:28 +03:00
} else {
2020-03-13 03:32:53 +03: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-12 04:30:43 +03:00
} ) ;
2020-03-13 03:32:53 +03: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-14 02:30:16 +03:00
}
2019-11-19 05:18:28 +03:00
}
this . emit ( TestRunner . Events . Finished , result ) ;
2019-12-20 02:47:35 +03:00
if ( session )
session . disconnect ( ) ;
2019-11-19 05:18:28 +03:00
return result ;
}
2020-01-09 19:37:19 +03:00
async terminate ( ) {
2019-11-19 05:18:28 +03:00
if ( ! this . _runningPass )
return ;
2020-03-13 03:32:53 +03:00
await this . _runningPass . _terminate ( TestResult . Terminated , 'Terminated with |TestRunner.terminate()| call' , true /* force */ , null /* error */ ) ;
2019-11-19 05:18:28 +03:00
}
timeout ( ) {
return this . _timeout ;
}
2020-03-12 04:30:43 +03:00
runnableTests ( ) {
if ( ! this . hasFocusedTestsOrSuites ( ) )
2019-11-19 05:18:28 +03:00
return this . _tests ;
const tests = [ ] ;
const blacklistSuites = new Set ( ) ;
// First pass: pick "fit" and blacklist parent suites
2020-03-10 21:30:02 +03:00
for ( let i = 0 ; i < this . _tests . length ; i ++ ) {
const test = this . _tests [ i ] ;
2019-11-19 05:18:28 +03:00
if ( test . declaredMode !== TestMode . Focus )
continue ;
2020-03-10 21:30:02 +03:00
tests . push ( { i , test } ) ;
2019-11-19 05:18:28 +03: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 21:30:02 +03:00
for ( let i = 0 ; i < this . _tests . length ; i ++ ) {
const test = this . _tests [ i ] ;
2019-11-19 05:18:28 +03: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 21:30:02 +03:00
tests . push ( { i , test } ) ;
2019-11-19 05:18:28 +03:00
}
2020-03-10 21:30:02 +03:00
tests . sort ( ( a , b ) => a . i - b . i ) ;
return tests . map ( t => t . test ) ;
2019-11-19 05:18:28 +03:00
}
2020-03-12 04:30:43 +03:00
focusedSuites ( ) {
return this . _suites . filter ( suite => suite . declaredMode === 'focus' ) ;
}
focusedTests ( ) {
return this . _tests . filter ( test => test . declaredMode === 'focus' ) ;
}
2019-11-19 05:18:28 +03:00
hasFocusedTestsOrSuites ( ) {
2020-03-12 04:30:43 +03:00
return ! ! this . focusedTests ( ) . length || ! ! this . focusedSuites ( ) . length ;
}
focusMatchingTests ( fullNameRegex ) {
for ( const test of this . _tests ) {
if ( fullNameRegex . test ( test . fullName ) )
test . declaredMode = 'focus' ;
}
2019-11-19 05:18:28 +03:00
}
tests ( ) {
return this . _tests . slice ( ) ;
}
failedTests ( ) {
return this . _tests . filter ( test => test . result === 'failed' || test . result === 'timedout' || test . result === 'crashed' ) ;
}
passedTests ( ) {
return this . _tests . filter ( test => test . result === 'ok' ) ;
}
skippedTests ( ) {
return this . _tests . filter ( test => test . result === 'skipped' ) ;
}
2020-03-03 01:57:09 +03:00
markedAsFailingTests ( ) {
return this . _tests . filter ( test => test . result === 'markedAsFailing' ) ;
}
2019-11-19 05:18:28 +03:00
parallel ( ) {
return this . _parallel ;
}
}
2019-12-20 02:47:35 +03: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-19 05:18:28 +03: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 ;