2021-06-06 17:09:53 -07:00
/ * *
* Copyright Microsoft Corporation . 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 .
* /
import rimraf from 'rimraf' ;
import util from 'util' ;
2021-08-28 07:19:45 -07:00
import colors from 'colors/safe' ;
2021-06-06 17:09:53 -07:00
import { EventEmitter } from 'events' ;
2022-01-28 17:39:42 -08:00
import { serializeError , prependToTestError , formatLocation } from './util' ;
2021-08-02 17:17:20 -07:00
import { TestBeginPayload , TestEndPayload , RunPayload , TestEntry , DonePayload , WorkerInitParams , StepBeginPayload , StepEndPayload } from './ipc' ;
2021-06-06 17:09:53 -07:00
import { setCurrentTestInfo } from './globals' ;
import { Loader } from './loader' ;
2021-07-19 14:54:18 -07:00
import { Modifier , Suite , TestCase } from './test' ;
2022-01-28 17:39:42 -08:00
import { Annotations , TestError , TestInfo , TestStepInternal , WorkerInfo } from './types' ;
2021-06-06 17:09:53 -07:00
import { ProjectImpl } from './project' ;
2021-11-15 13:17:26 -08:00
import { FixtureRunner } from './fixtures' ;
2022-01-28 17:39:42 -08:00
import { raceAgainstTimeout } from 'playwright-core/lib/utils/async' ;
import { TestInfoImpl } from './testInfo' ;
2021-06-06 17:09:53 -07:00
const removeFolderAsync = util . promisify ( rimraf ) ;
export class WorkerRunner extends EventEmitter {
private _params : WorkerInitParams ;
private _loader ! : Loader ;
private _project ! : ProjectImpl ;
private _workerInfo ! : WorkerInfo ;
private _fixtureRunner : FixtureRunner ;
2022-01-28 17:39:42 -08:00
private _failedTest : TestInfoImpl | undefined ;
2021-07-07 12:04:43 -07:00
private _fatalError : TestError | undefined ;
2021-06-06 17:09:53 -07:00
private _entries = new Map < string , TestEntry > ( ) ;
2021-07-28 15:43:37 -07:00
private _isStopped = false ;
2021-08-10 10:54:05 -07:00
private _runFinished = Promise . resolve ( ) ;
2022-01-28 17:39:42 -08:00
_currentTest : TestInfoImpl | null = null ;
2021-06-06 17:09:53 -07:00
constructor ( params : WorkerInitParams ) {
super ( ) ;
this . _params = params ;
this . _fixtureRunner = new FixtureRunner ( ) ;
}
2021-08-10 10:54:05 -07:00
stop ( ) : Promise < void > {
if ( ! this . _isStopped ) {
this . _isStopped = true ;
// Interrupt current action.
2022-01-28 17:39:42 -08:00
this . _currentTest ? . _timeoutRunner . interrupt ( ) ;
2021-08-10 10:54:05 -07:00
// TODO: mark test as 'interrupted' instead.
2022-01-28 17:39:42 -08:00
if ( this . _currentTest && this . _currentTest . status === 'passed' )
this . _currentTest . status = 'skipped' ;
2021-07-30 13:12:49 -07:00
}
2021-08-10 10:54:05 -07:00
return this . _runFinished ;
2021-06-06 17:09:53 -07:00
}
async cleanup() {
2021-07-07 12:04:43 -07:00
// We have to load the project to get the right deadline below.
2021-07-12 11:59:58 -05:00
await this . _loadIfNeeded ( ) ;
2021-09-28 10:56:50 -07:00
await this . _teardownScopes ( ) ;
if ( this . _fatalError )
this . emit ( 'teardownError' , { error : this._fatalError } ) ;
}
private async _teardownScopes() {
2021-06-06 17:09:53 -07:00
// TODO: separate timeout for teardown?
2022-01-25 11:22:28 -08:00
const result = await raceAgainstTimeout ( async ( ) = > {
2021-06-06 17:09:53 -07:00
await this . _fixtureRunner . teardownScope ( 'test' ) ;
await this . _fixtureRunner . teardownScope ( 'worker' ) ;
2022-01-25 11:22:28 -08:00
} , this . _project . config . timeout ) ;
2021-08-31 10:50:30 -07:00
if ( result . timedOut && ! this . _fatalError )
this . _fatalError = { message : colors.red ( ` Timeout of ${ this . _project . config . timeout } ms exceeded while shutting down environment ` ) } ;
2021-06-06 17:09:53 -07:00
}
unhandledError ( error : Error | any ) {
2021-10-29 13:36:12 -07:00
// Usually, we do not differentiate between errors in the control flow
// and unhandled errors - both lead to the test failing. This is good for regular tests,
// so that you can, e.g. expect() from inside an event handler. The test fails,
// and we restart the worker.
//
// However, for tests marked with test.fail(), this is a problem. Unhandled error
// could come either from the user test code (legit failure), or from a fixture or
// a test runner. In the latter case, the worker state could be messed up,
// and continuing to run tests in the same worker is problematic. Therefore,
// we turn this into a fatal error and restart the worker anyway.
2022-01-28 17:39:42 -08:00
if ( this . _currentTest && this . _currentTest . _test . _type === 'test' && this . _currentTest . expectedStatus !== 'failed' ) {
this . _currentTest . _failWithError ( serializeError ( error ) ) ;
2021-06-06 17:09:53 -07:00
} else {
// No current test - fatal error.
2021-08-10 10:54:05 -07:00
if ( ! this . _fatalError )
this . _fatalError = serializeError ( error ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-06-06 17:09:53 -07:00
}
2021-07-12 11:59:58 -05:00
private async _loadIfNeeded() {
2021-06-06 17:09:53 -07:00
if ( this . _loader )
return ;
2021-07-12 11:59:58 -05:00
this . _loader = await Loader . deserialize ( this . _params . loader ) ;
2021-06-06 17:09:53 -07:00
this . _project = this . _loader . projects ( ) [ this . _params . projectIndex ] ;
this . _workerInfo = {
workerIndex : this._params.workerIndex ,
2021-11-01 10:37:34 -07:00
parallelIndex : this._params.parallelIndex ,
2021-06-06 17:09:53 -07:00
project : this._project.config ,
config : this._loader.fullConfig ( ) ,
} ;
}
async run ( runPayload : RunPayload ) {
2021-08-29 11:21:06 -07:00
let runFinishedCallback = ( ) = > { } ;
this . _runFinished = new Promise ( f = > runFinishedCallback = f ) ;
2021-08-10 10:54:05 -07:00
try {
this . _entries = new Map ( runPayload . entries . map ( e = > [ e . testId , e ] ) ) ;
await this . _loadIfNeeded ( ) ;
const fileSuite = await this . _loader . loadTestFile ( runPayload . file ) ;
const suite = this . _project . cloneFileSuite ( fileSuite , this . _params . repeatEachIndex , test = > {
if ( ! this . _entries . has ( test . _id ) )
return false ;
return true ;
} ) ;
2021-11-15 13:17:26 -08:00
if ( suite ) {
const firstPool = suite . allTests ( ) [ 0 ] . _pool ! ;
this . _fixtureRunner . setPool ( firstPool ) ;
2021-08-10 10:54:05 -07:00
await this . _runSuite ( suite , [ ] ) ;
}
2021-09-28 10:56:50 -07:00
if ( this . _failedTest )
await this . _teardownScopes ( ) ;
2021-08-10 10:54:05 -07:00
} catch ( e ) {
// In theory, we should run above code without any errors.
// However, in the case we screwed up, or loadTestFile failed in the worker
// but not in the runner, let's do a fatal error.
this . unhandledError ( e ) ;
} finally {
2021-09-28 10:56:50 -07:00
if ( this . _failedTest ) {
// Now that we did run all hooks and teared down scopes, we can
// report the failure, possibly with any error details revealed by teardown.
2021-12-15 10:39:49 -08:00
this . emit ( 'testEnd' , buildTestEndPayload ( this . _failedTest ) ) ;
2021-09-28 10:56:50 -07:00
}
2021-06-06 17:09:53 -07:00
this . _reportDone ( ) ;
2021-08-29 11:21:06 -07:00
runFinishedCallback ( ) ;
2021-06-06 17:09:53 -07:00
}
}
2021-07-02 15:49:05 -07:00
private async _runSuite ( suite : Suite , annotations : Annotations ) {
2021-08-10 10:54:05 -07:00
// When stopped, do not run a suite. But if we have started running the suite with hooks,
// always finish the hooks.
2021-06-06 17:09:53 -07:00
if ( this . _isStopped )
return ;
2021-07-02 15:49:05 -07:00
annotations = annotations . concat ( suite . _annotations ) ;
for ( const beforeAllModifier of suite . _modifiers ) {
if ( ! this . _fixtureRunner . dependsOnWorkerFixturesOnly ( beforeAllModifier . fn , beforeAllModifier . location ) )
continue ;
// TODO: separate timeout for beforeAll modifiers?
2022-01-25 11:22:28 -08:00
const result = await raceAgainstTimeout ( ( ) = > this . _fixtureRunner . resolveParametersAndRunFunction ( beforeAllModifier . fn , this . _workerInfo , undefined ) , this . _project . config . timeout ) ;
2021-07-02 15:49:05 -07:00
if ( result . timedOut ) {
2021-08-23 09:21:40 -07:00
if ( ! this . _fatalError )
2021-12-18 09:32:41 -08:00
this . _fatalError = serializeError ( new Error ( ` Timeout of ${ this . _project . config . timeout } ms exceeded while running ${ beforeAllModifier . type } modifier \ n at ${ formatLocation ( beforeAllModifier . location ) } ` ) ) ;
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-12-22 09:59:58 -08:00
} else if ( ! ! result . result ) {
2021-07-02 15:49:05 -07:00
annotations . push ( { type : beforeAllModifier . type , description : beforeAllModifier.description } ) ;
2021-12-22 09:59:58 -08:00
}
2021-07-02 15:49:05 -07:00
}
2021-12-15 10:39:49 -08:00
for ( const hook of suite . hooks ) {
2021-08-09 13:26:33 -07:00
if ( hook . _type !== 'beforeAll' )
2021-06-06 17:09:53 -07:00
continue ;
2021-08-09 13:26:33 -07:00
const firstTest = suite . allTests ( ) [ 0 ] ;
await this . _runTestOrAllHook ( hook , annotations , this . _entries . get ( firstTest . _id ) ? . retry || 0 ) ;
2021-06-06 17:09:53 -07:00
}
for ( const entry of suite . _entries ) {
2021-08-09 13:26:33 -07:00
if ( entry instanceof Suite ) {
2021-07-02 15:49:05 -07:00
await this . _runSuite ( entry , annotations ) ;
2021-08-09 13:26:33 -07:00
} else {
const runEntry = this . _entries . get ( entry . _id ) ;
2021-08-10 10:54:05 -07:00
if ( runEntry && ! this . _isStopped )
2021-08-09 13:26:33 -07:00
await this . _runTestOrAllHook ( entry , annotations , runEntry . retry ) ;
}
2021-06-06 17:09:53 -07:00
}
2021-12-15 10:39:49 -08:00
for ( const hook of suite . hooks ) {
2021-08-09 13:26:33 -07:00
if ( hook . _type !== 'afterAll' )
2021-06-06 17:09:53 -07:00
continue ;
2021-08-09 13:26:33 -07:00
await this . _runTestOrAllHook ( hook , annotations , 0 ) ;
2021-06-06 17:09:53 -07:00
}
}
2021-08-09 13:26:33 -07:00
private async _runTestOrAllHook ( test : TestCase , annotations : Annotations , retry : number ) {
2021-08-02 17:17:20 -07:00
let lastStepId = 0 ;
2022-01-28 17:39:42 -08:00
const testInfo = new TestInfoImpl ( this . _loader , this . _params , test , retry , data = > {
const stepId = ` ${ data . category } @ ${ data . title } @ ${ ++ lastStepId } ` ;
let callbackHandled = false ;
const step : TestStepInternal = {
. . . data ,
complete : ( error? : Error | TestError ) = > {
if ( callbackHandled )
return ;
callbackHandled = true ;
if ( error instanceof Error )
error = serializeError ( error ) ;
const payload : StepEndPayload = {
testId : test._id ,
stepId ,
wallTime : Date.now ( ) ,
error ,
} ;
this . emit ( 'stepEnd' , payload ) ;
2021-11-23 09:30:53 -08:00
}
2022-01-28 17:39:42 -08:00
} ;
const hasLocation = data . location && ! data . location . file . includes ( '@playwright' ) ;
// Sanitize location that comes from user land, it might have extra properties.
const location = data . location && hasLocation ? { file : data.location.file , line : data.location.line , column : data.location.column } : undefined ;
const payload : StepBeginPayload = {
testId : test._id ,
stepId ,
. . . data ,
location ,
wallTime : Date.now ( ) ,
} ;
this . emit ( 'stepBegin' , payload ) ;
return step ;
} ) ;
2021-06-29 13:33:13 -07:00
// Inherit test.setTimeout() from parent suites.
2021-10-19 08:38:04 -07:00
for ( let suite : Suite | undefined = test . parent ; suite ; suite = suite . parent ) {
2021-06-29 13:33:13 -07:00
if ( suite . _timeout !== undefined ) {
testInfo . setTimeout ( suite . _timeout ) ;
break ;
}
}
2021-07-02 15:49:05 -07:00
// Process annotations defined on parent suites.
for ( const annotation of annotations ) {
testInfo . annotations . push ( annotation ) ;
switch ( annotation . type ) {
case 'fixme' :
case 'skip' :
testInfo . expectedStatus = 'skipped' ;
break ;
case 'fail' :
if ( testInfo . expectedStatus !== 'skipped' )
testInfo . expectedStatus = 'failed' ;
break ;
case 'slow' :
testInfo . setTimeout ( testInfo . timeout * 3 ) ;
break ;
}
}
2022-01-28 17:39:42 -08:00
this . _currentTest = testInfo ;
2021-08-10 10:54:05 -07:00
setCurrentTestInfo ( testInfo ) ;
2022-01-28 17:39:42 -08:00
this . emit ( 'testBegin' , buildTestBeginPayload ( testInfo ) ) ;
2021-06-06 17:09:53 -07:00
if ( testInfo . expectedStatus === 'skipped' ) {
testInfo . status = 'skipped' ;
2022-01-28 17:39:42 -08:00
this . emit ( 'testEnd' , buildTestEndPayload ( testInfo ) ) ;
2021-06-06 17:09:53 -07:00
return ;
}
// Update the fixture pool - it may differ between tests, but only in test-scoped fixtures.
2021-07-15 22:02:10 -07:00
this . _fixtureRunner . setPool ( test . _pool ! ) ;
2021-06-06 17:09:53 -07:00
2022-01-28 17:39:42 -08:00
await testInfo . _runWithTimeout ( ( ) = > this . _runTestWithBeforeHooks ( test , testInfo ) ) ;
2021-08-10 10:54:05 -07:00
2022-01-25 11:22:28 -08:00
if ( testInfo . status === 'timedOut' ) {
2021-08-10 10:54:05 -07:00
// A timed-out test gets a full additional timeout to run after hooks.
2022-01-28 17:39:42 -08:00
testInfo . _timeoutRunner . resetTimeout ( testInfo . timeout ) ;
2021-06-06 17:09:53 -07:00
}
2022-01-28 17:39:42 -08:00
await testInfo . _runWithTimeout ( ( ) = > this . _runAfterHooks ( test , testInfo ) ) ;
2021-06-06 17:09:53 -07:00
2021-08-10 10:54:05 -07:00
this . _currentTest = null ;
setCurrentTestInfo ( null ) ;
2021-07-28 15:43:37 -07:00
2021-09-28 10:56:50 -07:00
const isFailure = testInfo . status !== 'skipped' && testInfo . status !== testInfo . expectedStatus ;
2021-08-25 12:19:50 -07:00
if ( isFailure ) {
2021-12-15 10:39:49 -08:00
// Delay reporting testEnd result until after teardownScopes is done.
2022-01-28 17:39:42 -08:00
this . _failedTest = testInfo ;
2021-12-15 10:39:49 -08:00
if ( test . _type !== 'test' ) {
// beforeAll/afterAll hook failure skips any remaining tests in the worker.
2021-11-18 14:36:55 -08:00
if ( ! this . _fatalError )
2021-08-28 07:19:45 -07:00
this . _fatalError = testInfo . error ;
2021-11-18 14:36:55 -08:00
// Keep any error we have, and add "timeout" message.
if ( testInfo . status === 'timedOut' )
2021-12-18 09:32:41 -08:00
this . _fatalError = prependToTestError ( this . _fatalError , colors . red ( ` Timeout of ${ testInfo . timeout } ms exceeded in ${ test . _type } hook. \ n ` ) , test . location ) ;
2021-08-28 07:19:45 -07:00
}
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-12-15 10:39:49 -08:00
} else {
2022-01-28 17:39:42 -08:00
this . emit ( 'testEnd' , buildTestEndPayload ( testInfo ) ) ;
2021-06-06 17:09:53 -07:00
}
2021-09-28 10:56:50 -07:00
const preserveOutput = this . _loader . fullConfig ( ) . preserveOutput === 'always' ||
( this . _loader . fullConfig ( ) . preserveOutput === 'failures-only' && isFailure ) ;
if ( ! preserveOutput )
await removeFolderAsync ( testInfo . outputDir ) . catch ( e = > { } ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-09 13:26:33 -07:00
private async _runTestWithBeforeHooks ( test : TestCase , testInfo : TestInfoImpl ) {
2021-09-16 15:51:27 -07:00
const step = testInfo . _addStep ( {
category : 'hook' ,
title : 'Before Hooks' ,
canHaveChildren : true ,
forceNoParent : true
} ) ;
2022-01-28 17:39:42 -08:00
const maybeError = await testInfo . _runFn ( async ( ) = > {
2022-01-25 16:46:50 -08:00
if ( test . _type === 'test' ) {
const beforeEachModifiers : Modifier [ ] = [ ] ;
for ( let s : Suite | undefined = test . parent ; s ; s = s . parent ) {
const modifiers = s . _modifiers . filter ( modifier = > ! this . _fixtureRunner . dependsOnWorkerFixturesOnly ( modifier . fn , modifier . location ) ) ;
beforeEachModifiers . push ( . . . modifiers . reverse ( ) ) ;
}
beforeEachModifiers . reverse ( ) ;
for ( const modifier of beforeEachModifiers ) {
const result = await this . _fixtureRunner . resolveParametersAndRunFunction ( modifier . fn , this . _workerInfo , testInfo ) ;
testInfo [ modifier . type ] ( ! ! result , modifier . description ! ) ;
}
await this . _runHooks ( test . parent ! , 'beforeEach' , testInfo ) ;
}
2021-06-06 17:09:53 -07:00
2022-01-10 20:25:56 -08:00
const params = await this . _fixtureRunner . resolveParametersForFunction ( test . fn , this . _workerInfo , testInfo ) ;
step . complete ( ) ; // Report fixture hooks step as completed.
const fn = test . fn ; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
await fn ( params , testInfo ) ;
2022-01-28 17:39:42 -08:00
} , 'allowSkips' ) ;
2022-01-25 16:46:50 -08:00
step . complete ( maybeError ) ; // Second complete is a no-op.
2021-06-06 17:09:53 -07:00
}
2021-08-02 17:17:20 -07:00
private async _runAfterHooks ( test : TestCase , testInfo : TestInfoImpl ) {
2022-01-10 20:25:56 -08:00
const step = testInfo . _addStep ( {
category : 'hook' ,
title : 'After Hooks' ,
canHaveChildren : true ,
forceNoParent : true
} ) ;
let teardownError1 : TestError | undefined ;
if ( test . _type === 'test' )
2022-01-28 17:39:42 -08:00
teardownError1 = await testInfo . _runFn ( ( ) = > this . _runHooks ( test . parent ! , 'afterEach' , testInfo ) ) ;
2022-01-10 20:25:56 -08:00
// Continue teardown even after the failure.
2022-01-28 17:39:42 -08:00
const teardownError2 = await testInfo . _runFn ( ( ) = > this . _fixtureRunner . teardownScope ( 'test' ) ) ;
2022-01-10 20:25:56 -08:00
step . complete ( teardownError1 || teardownError2 ) ;
2021-06-06 17:09:53 -07:00
}
private async _runHooks ( suite : Suite , type : 'beforeEach' | 'afterEach' , testInfo : TestInfo ) {
const all = [ ] ;
for ( let s : Suite | undefined = suite ; s ; s = s . parent ) {
2021-08-09 13:26:33 -07:00
const funcs = s . _eachHooks . filter ( e = > e . type === type ) . map ( e = > e . fn ) ;
2021-06-06 17:09:53 -07:00
all . push ( . . . funcs . reverse ( ) ) ;
}
if ( type === 'beforeEach' )
all . reverse ( ) ;
let error : Error | undefined ;
for ( const hook of all ) {
try {
2022-01-10 20:25:56 -08:00
await this . _fixtureRunner . resolveParametersAndRunFunction ( hook , this . _workerInfo , testInfo ) ;
2021-06-06 17:09:53 -07:00
} catch ( e ) {
// Always run all the hooks, and capture the first error.
error = error || e ;
}
}
if ( error )
throw error ;
}
private _reportDone() {
2021-09-27 21:38:19 -07:00
const donePayload : DonePayload = { fatalError : this._fatalError } ;
2021-06-06 17:09:53 -07:00
this . emit ( 'done' , donePayload ) ;
2021-08-31 10:50:30 -07:00
this . _fatalError = undefined ;
2021-09-27 21:38:19 -07:00
this . _failedTest = undefined ;
2021-06-06 17:09:53 -07:00
}
}
2022-01-28 17:39:42 -08:00
function buildTestBeginPayload ( testInfo : TestInfoImpl ) : TestBeginPayload {
2021-06-06 17:09:53 -07:00
return {
2022-01-28 17:39:42 -08:00
testId : testInfo._test._id ,
startWallTime : testInfo._startWallTime ,
2021-06-06 17:09:53 -07:00
} ;
}
2022-01-28 17:39:42 -08:00
function buildTestEndPayload ( testInfo : TestInfoImpl ) : TestEndPayload {
2021-06-06 17:09:53 -07:00
return {
2022-01-28 17:39:42 -08:00
testId : testInfo._test._id ,
2021-06-06 17:09:53 -07:00
duration : testInfo.duration ,
status : testInfo.status ! ,
error : testInfo.error ,
expectedStatus : testInfo.expectedStatus ,
annotations : testInfo.annotations ,
timeout : testInfo.timeout ,
2021-07-16 13:48:37 -07:00
attachments : testInfo.attachments.map ( a = > ( {
name : a.name ,
contentType : a.contentType ,
path : a.path ,
body : a.body?.toString ( 'base64' )
} ) )
2021-06-06 17:09:53 -07:00
} ;
}