2021-06-06 17:09:53 -07:00
/ * *
* Copyright 2019 Google Inc . All rights reserved .
* Modifications copyright ( c ) Microsoft Corporation .
*
* 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 * as fs from 'fs' ;
import * as path from 'path' ;
2022-10-10 16:42:48 -07:00
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner' ;
import { colors , minimatch , rimraf } from 'playwright-core/lib/utilsBundle' ;
2021-06-06 17:09:53 -07:00
import { promisify } from 'util' ;
2022-10-10 16:42:48 -07:00
import type { FullResult , Reporter , TestError } from '../types/testReporter' ;
2022-04-06 13:57:14 -08:00
import type { TestGroup } from './dispatcher' ;
import { Dispatcher } from './dispatcher' ;
2021-06-06 17:09:53 -07:00
import { Loader } from './loader' ;
2022-10-10 16:42:48 -07:00
import type { TestRunnerPlugin } from './plugins' ;
import { setRunnerToAddPluginsTo } from './plugins' ;
import { dockerPlugin } from './plugins/dockerPlugin' ;
import { webServerPluginsForConfig } from './plugins/webServerPlugin' ;
2022-11-21 09:23:28 -08:00
import { formatError } from './reporters/base' ;
2021-06-06 17:09:53 -07:00
import DotReporter from './reporters/dot' ;
2022-10-10 16:42:48 -07:00
import EmptyReporter from './reporters/empty' ;
2021-10-04 14:02:56 +05:30
import GitHubReporter from './reporters/github' ;
2022-10-10 16:42:48 -07:00
import HtmlReporter from './reporters/html' ;
2021-06-06 17:09:53 -07:00
import JSONReporter from './reporters/json' ;
import JUnitReporter from './reporters/junit' ;
2022-10-10 16:42:48 -07:00
import LineReporter from './reporters/line' ;
import ListReporter from './reporters/list' ;
import { Multiplexer } from './reporters/multiplexer' ;
2022-04-07 19:18:22 -08:00
import { SigIntWatcher } from './sigIntWatcher' ;
2022-10-10 16:42:48 -07:00
import type { TestCase } from './test' ;
import { Suite } from './test' ;
import type { Config , FullConfigInternal , FullProjectInternal , ReporterInternal } from './types' ;
2022-10-19 15:05:59 -07:00
import { createFileMatcher , createFileMatcherFromFilters , createTitleMatcher , serializeError } from './util' ;
2022-10-12 14:34:22 -07:00
import type { Matcher , TestFileFilter } from './util' ;
2021-06-06 17:09:53 -07:00
2022-02-23 15:10:11 -07:00
const removeFolderAsync = promisify ( rimraf ) ;
2021-06-06 17:09:53 -07:00
const readDirAsync = promisify ( fs . readdir ) ;
const readFileAsync = promisify ( fs . readFile ) ;
2022-01-05 13:44:29 -08:00
export const kDefaultConfigFiles = [ 'playwright.config.ts' , 'playwright.config.js' , 'playwright.config.mjs' ] ;
2021-06-06 17:09:53 -07:00
2022-01-05 15:49:01 -08:00
type RunOptions = {
listOnly? : boolean ;
2022-09-29 16:39:21 -07:00
testFileFilters : TestFileFilter [ ] ;
testTitleMatcher : Matcher ;
2022-01-05 15:49:01 -08:00
projectFilter? : string [ ] ;
2022-08-29 15:46:34 -07:00
passWithNoTests? : boolean ;
2022-01-05 15:49:01 -08:00
} ;
2022-04-29 12:32:39 -08:00
export type ConfigCLIOverrides = {
forbidOnly? : boolean ;
fullyParallel? : boolean ;
globalTimeout? : number ;
maxFailures? : number ;
outputDir? : string ;
quiet? : boolean ;
repeatEach? : number ;
retries? : number ;
reporter? : string ;
shard ? : { current : number , total : number } ;
timeout? : number ;
2022-09-01 05:34:36 -07:00
ignoreSnapshots? : boolean ;
2022-04-29 12:32:39 -08:00
updateSnapshots ? : 'all' | 'none' | 'missing' ;
workers? : number ;
projects ? : { name : string , use? : any } [ ] ,
use? : any ;
} ;
2021-06-06 17:09:53 -07:00
export class Runner {
private _loader : Loader ;
2022-08-05 13:41:00 -07:00
private _reporter ! : ReporterInternal ;
2022-05-03 13:25:56 -08:00
private _plugins : TestRunnerPlugin [ ] = [ ] ;
2021-06-06 17:09:53 -07:00
2022-04-29 12:32:39 -08:00
constructor ( configCLIOverrides? : ConfigCLIOverrides ) {
this . _loader = new Loader ( configCLIOverrides ) ;
2022-05-03 13:25:56 -08:00
setRunnerToAddPluginsTo ( this ) ;
}
addPlugin ( plugin : TestRunnerPlugin ) {
this . _plugins . push ( plugin ) ;
2022-01-05 13:44:29 -08:00
}
2022-04-29 12:32:39 -08:00
async loadConfigFromResolvedFile ( resolvedConfigFile : string ) : Promise < FullConfigInternal > {
2022-04-25 09:40:58 -08:00
return await this . _loader . loadConfigFile ( resolvedConfigFile ) ;
2022-03-01 12:56:26 -08:00
}
2022-04-28 16:22:20 -07:00
loadEmptyConfig ( configFileOrDirectory : string ) : Promise < Config > {
2022-03-01 12:56:26 -08:00
return this . _loader . loadEmptyConfig ( configFileOrDirectory ) ;
}
static resolveConfigFile ( configFileOrDirectory : string ) : string | null {
const resolveConfig = ( configFile : string ) = > {
2022-01-12 19:52:40 -08:00
if ( fs . existsSync ( configFile ) )
2022-03-01 12:56:26 -08:00
return configFile ;
2022-01-05 13:44:29 -08:00
} ;
2022-03-01 12:56:26 -08:00
const resolveConfigFileFromDirectory = ( directory : string ) = > {
2022-01-05 13:44:29 -08:00
for ( const configName of kDefaultConfigFiles ) {
2022-03-01 12:56:26 -08:00
const configFile = resolveConfig ( path . resolve ( directory , configName ) ) ;
if ( configFile )
return configFile ;
2022-01-05 13:44:29 -08:00
}
} ;
if ( ! fs . existsSync ( configFileOrDirectory ) )
throw new Error ( ` ${ configFileOrDirectory } does not exist ` ) ;
if ( fs . statSync ( configFileOrDirectory ) . isDirectory ( ) ) {
// When passed a directory, look for a config file inside.
2022-03-01 12:56:26 -08:00
const configFile = resolveConfigFileFromDirectory ( configFileOrDirectory ) ;
if ( configFile )
return configFile ;
2022-01-05 13:44:29 -08:00
// If there is no config, assume this as a root testing directory.
2022-03-01 12:56:26 -08:00
return null ;
2022-01-05 13:44:29 -08:00
} else {
// When passed a file, it must be a config file.
2022-03-01 12:56:26 -08:00
const configFile = resolveConfig ( configFileOrDirectory ) ;
return configFile ! ;
2022-01-05 13:44:29 -08:00
}
2021-06-06 17:09:53 -07:00
}
2021-07-20 01:10:43 +02:00
private async _createReporter ( list : boolean ) {
2021-07-20 15:03:01 -05:00
const defaultReporters : { [ key in BuiltInReporter ] : new ( arg : any ) = > Reporter } = {
2021-07-20 01:10:43 +02:00
dot : list ? ListModeReporter : DotReporter ,
line : list ? ListModeReporter : LineReporter ,
list : list ? ListModeReporter : ListReporter ,
2021-10-04 14:02:56 +05:30
github : GitHubReporter ,
2021-06-06 17:09:53 -07:00
json : JSONReporter ,
junit : JUnitReporter ,
null : EmptyReporter ,
2021-10-14 10:17:35 -08:00
html : HtmlReporter ,
2021-06-06 17:09:53 -07:00
} ;
2021-09-13 20:34:46 -07:00
const reporters : Reporter [ ] = [ ] ;
2021-06-06 17:09:53 -07:00
for ( const r of this . _loader . fullConfig ( ) . reporter ) {
const [ name , arg ] = r ;
if ( name in defaultReporters ) {
reporters . push ( new defaultReporters [ name as keyof typeof defaultReporters ] ( arg ) ) ;
} else {
2021-07-12 11:59:58 -05:00
const reporterConstructor = await this . _loader . loadReporter ( name ) ;
2021-06-06 17:09:53 -07:00
reporters . push ( new reporterConstructor ( arg ) ) ;
}
}
2022-02-03 16:10:39 -08:00
if ( process . env . PW_TEST_REPORTER ) {
const reporterConstructor = await this . _loader . loadReporter ( process . env . PW_TEST_REPORTER ) ;
reporters . push ( new reporterConstructor ( ) ) ;
}
2021-11-03 08:25:16 -07:00
const someReporterPrintsToStdio = reporters . some ( r = > {
const prints = r . printsToStdio ? r . printsToStdio ( ) : true ;
return prints ;
} ) ;
if ( reporters . length && ! someReporterPrintsToStdio ) {
2021-11-11 13:27:50 -08:00
// Add a line/dot/list-mode reporter for convenience.
2021-11-03 08:25:16 -07:00
// Important to put it first, jsut in case some other reporter stalls onEnd.
2021-11-11 13:27:50 -08:00
if ( list )
reporters . unshift ( new ListModeReporter ( ) ) ;
else
2022-04-28 12:29:21 +01:00
reporters . unshift ( ! process . env . CI ? new LineReporter ( { omitFailures : true } ) : new DotReporter ( ) ) ;
2021-11-03 08:25:16 -07:00
}
2021-06-06 17:09:53 -07:00
return new Multiplexer ( reporters ) ;
}
2022-09-29 16:39:21 -07:00
async runAllTests ( options : RunOptions ) : Promise < FullResult > {
2022-01-05 15:49:01 -08:00
this . _reporter = await this . _createReporter ( ! ! options . listOnly ) ;
2022-01-31 17:09:04 -08:00
const config = this . _loader . fullConfig ( ) ;
2022-08-04 08:09:54 -07:00
const result = await raceAgainstTimeout ( ( ) = > this . _run ( options ) , config . globalTimeout ) ;
2022-01-31 17:09:04 -08:00
let fullResult : FullResult ;
if ( result . timedOut ) {
2022-12-21 09:36:59 -08:00
this . _reporter . onError ? . ( createStacklessError (
` Timed out waiting ${ config . globalTimeout / 1000 } s for the entire test run ` ) ) ;
2022-01-31 17:09:04 -08:00
fullResult = { status : 'timedout' } ;
} else {
fullResult = result . result ;
2021-06-06 17:09:53 -07:00
}
2022-01-31 17:09:04 -08:00
await this . _reporter . onEnd ? . ( fullResult ) ;
// Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456.
// See https://github.com/nodejs/node/issues/12921
await new Promise < void > ( resolve = > process . stdout . write ( '' , ( ) = > resolve ( ) ) ) ;
await new Promise < void > ( resolve = > process . stderr . write ( '' , ( ) = > resolve ( ) ) ) ;
2022-08-05 13:41:00 -07:00
await this . _reporter . _onExit ? . ( ) ;
2022-01-31 17:09:04 -08:00
return fullResult ;
2021-06-06 17:09:53 -07:00
}
2022-11-08 12:05:00 -08:00
async listTestFiles ( projectNames : string [ ] | undefined ) : Promise < any > {
2022-10-10 17:56:18 -07:00
const projects = this . _collectProjects ( projectNames ) ;
2022-11-18 11:35:29 -08:00
const { filesByProject } = await this . _collectFiles ( projects , [ ] ) ;
2022-01-21 19:11:22 -08:00
const report : any = {
projects : [ ]
} ;
for ( const [ project , files ] of filesByProject ) {
report . projects . push ( {
2022-11-08 12:05:00 -08:00
. . . sanitizeConfigForJSON ( project , new Set ( ) ) ,
2022-11-17 16:31:04 -08:00
files
2022-01-21 19:11:22 -08:00
} ) ;
}
return report ;
}
2022-10-10 17:56:18 -07:00
private _collectProjects ( projectNames? : string [ ] ) : FullProjectInternal [ ] {
const fullConfig = this . _loader . fullConfig ( ) ;
if ( ! projectNames )
return [ . . . fullConfig . projects ] ;
2022-09-28 18:45:01 -07:00
const projectsToFind = new Set < string > ( ) ;
const unknownProjects = new Map < string , string > ( ) ;
projectNames . forEach ( n = > {
const name = n . toLocaleLowerCase ( ) ;
projectsToFind . add ( name ) ;
unknownProjects . set ( name , n ) ;
} ) ;
2022-04-29 15:05:08 -08:00
const projects = fullConfig . projects . filter ( project = > {
const name = project . name . toLocaleLowerCase ( ) ;
2022-09-28 18:45:01 -07:00
unknownProjects . delete ( name ) ;
2021-09-02 09:29:55 -07:00
return projectsToFind . has ( name ) ;
2021-06-06 17:09:53 -07:00
} ) ;
2022-09-28 18:45:01 -07:00
if ( unknownProjects . size ) {
2022-04-29 15:05:08 -08:00
const names = fullConfig . projects . map ( p = > p . name ) . filter ( name = > ! ! name ) ;
2021-06-06 17:09:53 -07:00
if ( ! names . length )
throw new Error ( ` No named projects are specified in the configuration file ` ) ;
2021-09-02 09:29:55 -07:00
const unknownProjectNames = Array . from ( unknownProjects . values ( ) ) . map ( n = > ` " ${ n } " ` ) . join ( ', ' ) ;
throw new Error ( ` Project(s) ${ unknownProjectNames } not found. Available named projects: ${ names . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } ` ) ;
2021-06-06 17:09:53 -07:00
}
2022-09-28 18:45:01 -07:00
return projects ;
}
2021-06-06 17:09:53 -07:00
2022-11-22 16:22:48 -08:00
private async _collectFiles ( projects : FullProjectInternal [ ] , commandLineFileFilters : TestFileFilter [ ] ) : Promise < { filesByProject : Map < FullProjectInternal , string [ ] > ; setupFiles : Set < string > } > {
2022-11-01 23:44:30 -07:00
const extensions = [ '.js' , '.ts' , '.mjs' , '.tsx' , '.jsx' ] ;
const testFileExtension = ( file : string ) = > extensions . includes ( path . extname ( file ) ) ;
const filesByProject = new Map < FullProjectInternal , string [ ] > ( ) ;
const setupFiles = new Set < string > ( ) ;
const fileToProjectName = new Map < string , string > ( ) ;
2022-11-22 16:22:48 -08:00
const commandLineFileMatcher = commandLineFileFilters . length ? createFileMatcherFromFilters ( commandLineFileFilters ) : ( ) = > true ;
2021-06-06 17:09:53 -07:00
for ( const project of projects ) {
2022-05-26 14:39:51 -07:00
const allFiles = await collectFiles ( project . testDir , project . _respectGitIgnore ) ;
2022-12-06 14:55:22 -08:00
const setupMatch = createFileMatcher ( project . _setupMatch ) ;
2022-04-29 15:05:08 -08:00
const testMatch = createFileMatcher ( project . testMatch ) ;
const testIgnore = createFileMatcher ( project . testIgnore ) ;
2022-11-01 23:44:30 -07:00
const testFiles = allFiles . filter ( file = > {
if ( ! testFileExtension ( file ) )
return false ;
const isSetup = setupMatch ( file ) ;
2022-11-18 11:35:29 -08:00
const isTest = ! testIgnore ( file ) && testMatch ( file ) && commandLineFileMatcher ( file ) ;
2022-11-01 23:44:30 -07:00
if ( ! isTest && ! isSetup )
return false ;
if ( isSetup && isTest )
2022-12-13 22:48:38 -08:00
throw new Error ( ` File " ${ file } " matches both '_setup' and 'testMatch' filters in project " ${ project . name } " ` ) ;
2022-11-01 23:44:30 -07:00
if ( fileToProjectName . has ( file ) ) {
if ( isSetup ) {
if ( ! setupFiles . has ( file ) )
2022-12-13 22:48:38 -08:00
throw new Error ( ` File " ${ file } " matches '_setup' filter in project " ${ project . name } " and 'testMatch' filter in project " ${ fileToProjectName . get ( file ) } " ` ) ;
2022-11-01 23:44:30 -07:00
} else if ( setupFiles . has ( file ) ) {
2022-12-13 22:48:38 -08:00
throw new Error ( ` File " ${ file } " matches '_setup' filter in project " ${ fileToProjectName . get ( file ) } " and 'testMatch' filter in project " ${ project . name } " ` ) ;
2022-11-01 23:44:30 -07:00
}
}
fileToProjectName . set ( file , project . name ) ;
if ( isSetup )
setupFiles . add ( file ) ;
return true ;
} ) ;
filesByProject . set ( project , testFiles ) ;
2021-06-06 17:09:53 -07:00
}
2022-11-18 11:35:29 -08:00
2022-11-22 16:22:48 -08:00
return { filesByProject , setupFiles } ;
2022-01-21 19:11:22 -08:00
}
2021-06-06 17:09:53 -07:00
2022-11-01 23:44:30 -07:00
private async _collectTestGroups ( options : RunOptions , fatalErrors : TestError [ ] ) : Promise < { rootSuite : Suite , projectSetupGroups : TestGroup [ ] , testGroups : TestGroup [ ] } > {
2022-01-21 19:11:22 -08:00
const config = this . _loader . fullConfig ( ) ;
2022-10-10 17:56:18 -07:00
const projects = this . _collectProjects ( options . projectFilter ) ;
2022-11-22 16:22:48 -08:00
const { filesByProject , setupFiles } = await this . _collectFiles ( projects , options . testFileFilters ) ;
2022-12-05 18:15:01 -08:00
let result = await this . _createFilteredRootSuite ( options , filesByProject , new Set ( ) , ! ! setupFiles . size , setupFiles ) ;
2022-11-22 16:22:48 -08:00
if ( setupFiles . size ) {
const allTests = result . rootSuite . allTests ( ) ;
2022-12-06 08:59:28 -08:00
const tests = allTests . filter ( test = > ! test . _isProjectSetup ) ;
2022-11-22 16:22:48 -08:00
// If >0 tests match and
// - none of the setup files match the filter then we run all setup files,
// - if the filter also matches some of the setup tests, we'll run only
// that maching subset of setup tests.
if ( tests . length > 0 && tests . length === allTests . length )
2022-12-05 18:15:01 -08:00
result = await this . _createFilteredRootSuite ( options , filesByProject , setupFiles , false , setupFiles ) ;
2022-11-22 16:22:48 -08:00
}
fatalErrors . push ( . . . result . fatalErrors ) ;
const { rootSuite } = result ;
2022-10-31 14:04:24 -07:00
2022-11-22 16:22:48 -08:00
const allTestGroups = createTestGroups ( rootSuite . suites , config . workers ) ;
const projectSetupGroups = [ ] ;
const testGroups = [ ] ;
for ( const group of allTestGroups ) {
2022-12-06 08:59:28 -08:00
if ( group . isProjectSetup )
2022-11-22 16:22:48 -08:00
projectSetupGroups . push ( group ) ;
else
testGroups . push ( group ) ;
}
return { rootSuite , projectSetupGroups , testGroups } ;
}
2022-12-05 18:15:01 -08:00
private async _createFilteredRootSuite ( options : RunOptions , filesByProject : Map < FullProjectInternal , string [ ] > , doNotFilterFiles : Set < string > , shouldCloneTests : boolean , setupFiles : Set < string > ) : Promise < { rootSuite : Suite , fatalErrors : TestError [ ] } > {
2022-11-22 16:22:48 -08:00
const config = this . _loader . fullConfig ( ) ;
const fatalErrors : TestError [ ] = [ ] ;
2022-10-31 14:04:24 -07:00
const allTestFiles = new Set < string > ( ) ;
for ( const files of filesByProject . values ( ) )
files . forEach ( file = > allTestFiles . add ( file ) ) ;
// Add all tests.
const preprocessRoot = new Suite ( '' , 'root' ) ;
for ( const file of allTestFiles ) {
2022-12-05 18:15:01 -08:00
const fileSuite = await this . _loader . loadTestFile ( file , 'runner' , setupFiles . has ( file ) ) ;
2022-10-31 14:04:24 -07:00
if ( fileSuite . _loadError )
fatalErrors . push ( fileSuite . _loadError ) ;
2022-11-22 16:22:48 -08:00
// We have to clone only if there maybe subsequent calls of this method.
preprocessRoot . _addSuite ( shouldCloneTests ? fileSuite . _deepClone ( ) : fileSuite ) ;
2022-10-31 14:04:24 -07:00
}
2022-01-16 08:47:09 -08:00
2022-10-31 14:04:24 -07:00
// Complain about duplicate titles.
2022-12-21 09:36:59 -08:00
fatalErrors . push ( . . . createDuplicateTitlesErrors ( config , preprocessRoot ) ) ;
2022-01-31 17:09:04 -08:00
2022-10-31 14:04:24 -07:00
// Filter tests to respect line/column filter.
2022-11-22 16:22:48 -08:00
filterByFocusedLine ( preprocessRoot , options . testFileFilters , doNotFilterFiles ) ;
2021-06-06 17:09:53 -07:00
2022-10-31 14:04:24 -07:00
// Complain about only.
if ( config . forbidOnly ) {
const onlyTestsAndSuites = preprocessRoot . _getOnlyItems ( ) ;
if ( onlyTestsAndSuites . length > 0 )
2022-12-21 09:36:59 -08:00
fatalErrors . push ( . . . createForbidOnlyErrors ( config , onlyTestsAndSuites ) ) ;
2022-10-31 14:04:24 -07:00
}
2022-01-31 17:09:04 -08:00
2022-10-31 14:04:24 -07:00
// Filter only.
2022-11-22 16:22:48 -08:00
if ( ! options . listOnly )
filterOnly ( preprocessRoot , doNotFilterFiles ) ;
2022-10-31 14:04:24 -07:00
// Generate projects.
const fileSuites = new Map < string , Suite > ( ) ;
for ( const fileSuite of preprocessRoot . suites )
fileSuites . set ( fileSuite . _requireFile , fileSuite ) ;
const rootSuite = new Suite ( '' , 'root' ) ;
for ( const [ project , files ] of filesByProject ) {
const grepMatcher = createTitleMatcher ( project . grep ) ;
const grepInvertMatcher = project . grepInvert ? createTitleMatcher ( project . grepInvert ) : null ;
2022-11-21 09:23:28 -08:00
const titleMatcher = ( test : TestCase ) = > {
2022-11-22 16:22:48 -08:00
if ( doNotFilterFiles . has ( test . _requireFile ) )
return true ;
2022-11-21 09:23:28 -08:00
const grepTitle = test . titlePath ( ) . join ( ' ' ) ;
if ( grepInvertMatcher ? . ( grepTitle ) )
return false ;
return grepMatcher ( grepTitle ) && options . testTitleMatcher ( grepTitle ) ;
} ;
2022-10-31 14:04:24 -07:00
const projectSuite = new Suite ( project . name , 'project' ) ;
projectSuite . _projectConfig = project ;
if ( project . _fullyParallel )
projectSuite . _parallelMode = 'parallel' ;
rootSuite . _addSuite ( projectSuite ) ;
for ( const file of files ) {
const fileSuite = fileSuites . get ( file ) ;
if ( ! fileSuite )
continue ;
for ( let repeatEachIndex = 0 ; repeatEachIndex < project . repeatEach ; repeatEachIndex ++ ) {
2022-11-22 16:22:48 -08:00
const builtSuite = this . _loader . buildFileSuiteForProject ( project , fileSuite , repeatEachIndex , titleMatcher ) ;
2022-10-31 14:04:24 -07:00
if ( builtSuite )
projectSuite . _addSuite ( builtSuite ) ;
2021-06-06 17:09:53 -07:00
}
}
2022-01-31 17:09:04 -08:00
}
2022-11-22 16:22:48 -08:00
return { rootSuite , fatalErrors } ;
2022-09-30 09:12:06 -07:00
}
2022-11-01 23:44:30 -07:00
private _filterForCurrentShard ( rootSuite : Suite , projectSetupGroups : TestGroup [ ] , testGroups : TestGroup [ ] ) {
2022-10-12 14:34:22 -07:00
const shard = this . _loader . fullConfig ( ) . shard ;
if ( ! shard )
return ;
// Each shard includes:
2022-11-01 23:44:30 -07:00
// - its portion of the regular tests
// - project setup tests for the projects that have regular tests in this shard
2022-10-12 14:34:22 -07:00
let shardableTotal = 0 ;
2022-11-01 23:44:30 -07:00
for ( const group of testGroups )
shardableTotal += group . tests . length ;
2022-10-12 14:34:22 -07:00
const shardTests = new Set < TestCase > ( ) ;
// Each shard gets some tests.
const shardSize = Math . floor ( shardableTotal / shard . total ) ;
// First few shards get one more test each.
const extraOne = shardableTotal - shardSize * shard . total ;
const currentShard = shard . current - 1 ; // Make it zero-based for calculations.
const from = shardSize * currentShard + Math . min ( extraOne , currentShard ) ;
const to = from + shardSize + ( currentShard < extraOne ? 1 : 0 ) ;
let current = 0 ;
2022-11-01 23:44:30 -07:00
const shardProjects = new Set < string > ( ) ;
2022-10-31 14:04:24 -07:00
const shardTestGroups = [ ] ;
for ( const group of testGroups ) {
2022-11-01 23:44:30 -07:00
// Any test group goes to the shard that contains the first test of this group.
// So, this shard gets any group that starts at [from; to)
if ( current >= from && current < to ) {
shardProjects . add ( group . projectId ) ;
2022-10-31 14:04:24 -07:00
shardTestGroups . push ( group ) ;
for ( const test of group . tests )
shardTests . add ( test ) ;
2022-10-12 14:34:22 -07:00
}
2022-11-01 23:44:30 -07:00
current += group . tests . length ;
2022-10-12 14:34:22 -07:00
}
2022-10-31 14:04:24 -07:00
testGroups . length = 0 ;
testGroups . push ( . . . shardTestGroups ) ;
2022-10-12 14:34:22 -07:00
2022-11-01 23:44:30 -07:00
const shardSetupGroups = [ ] ;
for ( const group of projectSetupGroups ) {
if ( ! shardProjects . has ( group . projectId ) )
continue ;
shardSetupGroups . push ( group ) ;
for ( const test of group . tests )
shardTests . add ( test ) ;
}
projectSetupGroups . length = 0 ;
projectSetupGroups . push ( . . . shardSetupGroups ) ;
2022-11-29 16:02:11 -08:00
if ( ! shardTests . size ) {
// Filtering with "only semantics" does not work when we have zero tests - it leaves all the tests.
// We need an empty suite in this case.
rootSuite . _entries = [ ] ;
rootSuite . suites = [ ] ;
rootSuite . tests = [ ] ;
} else {
filterSuiteWithOnlySemantics ( rootSuite , ( ) = > false , test = > shardTests . has ( test ) ) ;
}
2022-10-12 14:34:22 -07:00
}
2022-09-30 09:12:06 -07:00
private async _run ( options : RunOptions ) : Promise < FullResult > {
const config = this . _loader . fullConfig ( ) ;
const fatalErrors : TestError [ ] = [ ] ;
// Each entry is an array of test groups that can be run concurrently. All
// test groups from the previos entries must finish before entry starts.
2022-11-01 23:44:30 -07:00
const { rootSuite , projectSetupGroups , testGroups } = await this . _collectTestGroups ( options , fatalErrors ) ;
2021-06-06 17:09:53 -07:00
2022-09-30 09:12:06 -07:00
// Fail when no tests.
2022-10-12 14:34:22 -07:00
if ( ! rootSuite . allTests ( ) . length && ! options . passWithNoTests )
2022-01-31 17:09:04 -08:00
fatalErrors . push ( createNoTestsError ( ) ) ;
2022-11-01 23:44:30 -07:00
this . _filterForCurrentShard ( rootSuite , projectSetupGroups , testGroups ) ;
2022-09-23 20:01:27 -07:00
2022-11-01 23:44:30 -07:00
config . _maxConcurrentTestGroups = Math . max ( projectSetupGroups . length , testGroups . length ) ;
2021-07-27 11:04:38 -07:00
2022-09-30 09:12:06 -07:00
// Report begin
2022-01-31 17:09:04 -08:00
this . _reporter . onBegin ? . ( config , rootSuite ) ;
2022-09-30 09:12:06 -07:00
// Bail out on errors prior to running global setup.
2022-01-31 17:09:04 -08:00
if ( fatalErrors . length ) {
for ( const error of fatalErrors )
this . _reporter . onError ? . ( error ) ;
return { status : 'failed' } ;
}
2022-09-30 09:12:06 -07:00
// Bail out if list mode only, don't do any work.
2022-08-04 08:09:54 -07:00
if ( options . listOnly )
2022-01-31 17:09:04 -08:00
return { status : 'passed' } ;
2022-09-30 09:12:06 -07:00
// Remove output directores.
2022-08-04 08:09:54 -07:00
if ( ! this . _removeOutputDirs ( options ) )
2022-02-22 12:50:26 -08:00
return { status : 'failed' } ;
2022-02-07 10:41:56 -08:00
2022-09-30 09:12:06 -07:00
// Run Global setup.
2022-01-31 17:09:04 -08:00
const result : FullResult = { status : 'passed' } ;
2022-09-23 20:01:27 -07:00
const globalTearDown = await this . _performGlobalSetup ( config , rootSuite , result ) ;
2022-06-12 12:06:00 -08:00
if ( result . status !== 'passed' )
return result ;
2022-01-31 17:09:04 -08:00
2022-09-14 09:16:41 -07:00
if ( config . _ignoreSnapshots ) {
this . _reporter . onStdOut ? . ( colors . dim ( [
'NOTE: running with "ignoreSnapshots" option. All of the following asserts are silently ignored:' ,
'- expect().toMatchSnapshot()' ,
'- expect().toHaveScreenshot()' ,
'' ,
] . join ( '\n' ) ) ) ;
}
2022-09-30 09:12:06 -07:00
// Run tests.
2022-01-31 17:09:04 -08:00
try {
2022-11-01 23:44:30 -07:00
let dispatchResult = await this . _dispatchToWorkers ( projectSetupGroups ) ;
if ( dispatchResult === 'success' ) {
const failedSetupProjectIds = new Set < string > ( ) ;
for ( const testGroup of projectSetupGroups ) {
if ( testGroup . tests . some ( test = > ! test . ok ( ) ) )
failedSetupProjectIds . add ( testGroup . projectId ) ;
2022-09-23 20:01:27 -07:00
}
2022-11-01 23:44:30 -07:00
const testGroupsToRun = this . _skipTestsFromFailedProjects ( testGroups , failedSetupProjectIds ) ;
dispatchResult = await this . _dispatchToWorkers ( testGroupsToRun ) ;
2021-06-06 17:09:53 -07:00
}
2022-11-01 23:44:30 -07:00
if ( dispatchResult === 'signal' ) {
2022-09-23 20:01:27 -07:00
result . status = 'interrupted' ;
} else {
2022-11-01 23:44:30 -07:00
const failed = dispatchResult === 'workererror' || rootSuite . allTests ( ) . some ( test = > ! test . ok ( ) ) ;
2022-01-31 17:09:04 -08:00
result . status = failed ? 'failed' : 'passed' ;
2021-06-29 10:55:46 -07:00
}
2022-01-31 17:09:04 -08:00
} catch ( e ) {
this . _reporter . onError ? . ( serializeError ( e ) ) ;
return { status : 'failed' } ;
2021-06-06 17:09:53 -07:00
} finally {
2022-02-07 10:41:56 -08:00
await globalTearDown ? . ( ) ;
}
return result ;
}
2022-11-01 23:44:30 -07:00
private async _dispatchToWorkers ( stageGroups : TestGroup [ ] ) : Promise < 'success' | 'signal' | 'workererror' > {
const dispatcher = new Dispatcher ( this . _loader , [ . . . stageGroups ] , this . _reporter ) ;
const sigintWatcher = new SigIntWatcher ( ) ;
await Promise . race ( [ dispatcher . run ( ) , sigintWatcher . promise ( ) ] ) ;
if ( ! sigintWatcher . hadSignal ( ) ) {
// We know for sure there was no Ctrl+C, so we remove custom SIGINT handler
// as soon as we can.
sigintWatcher . disarm ( ) ;
}
await dispatcher . stop ( ) ;
if ( sigintWatcher . hadSignal ( ) )
return 'signal' ;
if ( dispatcher . hasWorkerErrors ( ) )
return 'workererror' ;
return 'success' ;
}
private _skipTestsFromFailedProjects ( testGroups : TestGroup [ ] , failedProjects : Set < string > ) : TestGroup [ ] {
const result = [ ] ;
2022-10-18 17:18:45 -07:00
for ( const group of testGroups ) {
2022-11-01 23:44:30 -07:00
if ( failedProjects . has ( group . projectId ) ) {
2022-10-18 17:18:45 -07:00
for ( const test of group . tests ) {
const result = test . _appendTestResult ( ) ;
this . _reporter . onTestBegin ? . ( test , result ) ;
result . status = 'skipped' ;
this . _reporter . onTestEnd ? . ( test , result ) ;
}
2022-11-01 23:44:30 -07:00
} else {
result . push ( group ) ;
2022-10-18 17:18:45 -07:00
}
}
2022-11-01 23:44:30 -07:00
return result ;
2022-10-18 17:18:45 -07:00
}
2022-08-04 08:09:54 -07:00
private async _removeOutputDirs ( options : RunOptions ) : Promise < boolean > {
const config = this . _loader . fullConfig ( ) ;
const outputDirs = new Set < string > ( ) ;
for ( const p of config . projects ) {
if ( ! options . projectFilter || options . projectFilter . includes ( p . name ) )
outputDirs . add ( p . outputDir ) ;
}
try {
await Promise . all ( Array . from ( outputDirs ) . map ( outputDir = > removeFolderAsync ( outputDir ) . catch ( async ( error : any ) = > {
if ( ( error as any ) . code === 'EBUSY' ) {
// We failed to remove folder, might be due to the whole folder being mounted inside a container:
// https://github.com/microsoft/playwright/issues/12106
// Do a best-effort to remove all files inside of it instead.
const entries = await readDirAsync ( outputDir ) . catch ( e = > [ ] ) ;
await Promise . all ( entries . map ( entry = > removeFolderAsync ( path . join ( outputDir , entry ) ) ) ) ;
} else {
throw error ;
}
} ) ) ) ;
} catch ( e ) {
this . _reporter . onError ? . ( serializeError ( e ) ) ;
return false ;
}
return true ;
}
2022-09-23 20:01:27 -07:00
private async _performGlobalSetup ( config : FullConfigInternal , rootSuite : Suite , result : FullResult ) : Promise < ( ( ) = > Promise < void > ) | undefined > {
2022-08-05 15:24:30 -07:00
let globalSetupResult : any = undefined ;
2022-08-01 09:01:23 -07:00
2022-06-12 12:06:00 -08:00
const pluginsThatWereSetUp : TestRunnerPlugin [ ] = [ ] ;
const sigintWatcher = new SigIntWatcher ( ) ;
2022-01-31 17:09:04 -08:00
2022-02-07 10:41:56 -08:00
const tearDown = async ( ) = > {
2022-08-05 15:24:30 -07:00
await this . _runAndReportError ( async ( ) = > {
if ( globalSetupResult && typeof globalSetupResult === 'function' )
await globalSetupResult ( this . _loader . fullConfig ( ) ) ;
} , result ) ;
2022-01-31 17:09:04 -08:00
2022-08-05 15:24:30 -07:00
await this . _runAndReportError ( async ( ) = > {
if ( globalSetupResult && config . globalTeardown )
await ( await this . _loader . loadGlobalHook ( config . globalTeardown ) ) ( this . _loader . fullConfig ( ) ) ;
} , result ) ;
2022-01-31 17:09:04 -08:00
2022-06-12 12:06:00 -08:00
for ( const plugin of pluginsThatWereSetUp . reverse ( ) ) {
2022-04-25 09:40:58 -08:00
await this . _runAndReportError ( async ( ) = > {
2022-05-05 09:14:00 -08:00
await plugin . teardown ? . ( ) ;
2022-04-25 09:40:58 -08:00
} , result ) ;
}
2022-02-07 10:41:56 -08:00
} ;
2022-06-12 12:06:00 -08:00
// Legacy webServer support.
2022-09-13 17:05:37 -07:00
this . _plugins . push ( . . . webServerPluginsForConfig ( config ) ) ;
// Docker support.
this . _plugins . push ( dockerPlugin ) ;
2022-05-03 13:25:56 -08:00
2022-06-12 12:06:00 -08:00
await this . _runAndReportError ( async ( ) = > {
2022-04-25 09:40:58 -08:00
// First run the plugins, if plugin is a web server we want it to run before the
// config's global setup.
2022-06-12 12:06:00 -08:00
for ( const plugin of this . _plugins ) {
await Promise . race ( [
2022-09-13 17:05:37 -07:00
plugin . setup ? . ( config , config . _configDir , rootSuite , this . _reporter ) ,
2022-06-12 12:06:00 -08:00
sigintWatcher . promise ( ) ,
] ) ;
if ( sigintWatcher . hadSignal ( ) )
break ;
pluginsThatWereSetUp . push ( plugin ) ;
}
2022-04-25 09:40:58 -08:00
2022-08-05 15:24:30 -07:00
// Then do global setup.
if ( ! sigintWatcher . hadSignal ( ) ) {
if ( config . globalSetup ) {
const hook = await this . _loader . loadGlobalHook ( config . globalSetup ) ;
await Promise . race ( [
Promise . resolve ( ) . then ( ( ) = > hook ( this . _loader . fullConfig ( ) ) ) . then ( ( r : any ) = > globalSetupResult = r || '<noop>' ) ,
sigintWatcher . promise ( ) ,
] ) ;
} else {
// Make sure we run the teardown.
globalSetupResult = '<noop>' ;
2022-07-20 12:41:35 -07:00
}
2022-06-12 12:06:00 -08:00
}
2022-02-07 10:41:56 -08:00
} , result ) ;
2022-06-12 12:06:00 -08:00
sigintWatcher . disarm ( ) ;
if ( result . status !== 'passed' || sigintWatcher . hadSignal ( ) ) {
2022-02-10 12:44:42 -08:00
await tearDown ( ) ;
2022-06-12 12:06:00 -08:00
result . status = sigintWatcher . hadSignal ( ) ? 'interrupted' : 'failed' ;
2022-02-07 10:41:56 -08:00
return ;
2022-01-31 17:09:04 -08:00
}
2022-02-07 10:41:56 -08:00
return tearDown ;
2022-01-31 17:09:04 -08:00
}
2022-02-07 10:41:56 -08:00
private async _runAndReportError ( callback : ( ) = > Promise < void > , result : FullResult ) {
2022-01-31 17:09:04 -08:00
try {
await callback ( ) ;
} catch ( e ) {
2022-02-07 10:41:56 -08:00
result . status = 'failed' ;
2022-01-31 17:09:04 -08:00
this . _reporter . onError ? . ( serializeError ( e ) ) ;
2021-06-06 17:09:53 -07:00
}
}
}
2022-11-21 16:33:23 -08:00
function filterOnly ( suite : Suite , doNotFilterFiles : Set < string > ) {
2022-11-22 16:22:48 -08:00
if ( ! suite . _getOnlyItems ( ) . length )
return ;
const suiteFilter = ( suite : Suite ) = > suite . _only || doNotFilterFiles . has ( suite . _requireFile ) ;
const testFilter = ( test : TestCase ) = > test . _only || doNotFilterFiles . has ( test . _requireFile ) ;
2022-01-16 08:47:09 -08:00
return filterSuiteWithOnlySemantics ( suite , suiteFilter , testFilter ) ;
2021-06-24 10:02:34 +02:00
}
2022-11-22 16:22:48 -08:00
function createFileMatcherFromFilter ( filter : TestFileFilter ) {
const fileMatcher = createFileMatcher ( filter . re || filter . exact || '' ) ;
return ( testFileName : string , testLine : number , testColumn : number ) = >
fileMatcher ( testFileName ) && ( filter . line === testLine || filter . line === null ) && ( filter . column === testColumn || filter . column === null ) ;
}
2022-11-21 16:33:23 -08:00
function filterByFocusedLine ( suite : Suite , focusedTestFileLines : TestFileFilter [ ] , doNotFilterFiles : Set < string > ) {
2022-11-22 16:22:48 -08:00
if ( ! focusedTestFileLines . length )
2022-01-16 08:47:09 -08:00
return ;
2022-11-22 16:22:48 -08:00
const matchers = focusedTestFileLines . map ( createFileMatcherFromFilter ) ;
const testFileLineMatches = ( testFileName : string , testLine : number , testColumn : number ) = > matchers . some ( m = > m ( testFileName , testLine , testColumn ) ) ;
const suiteFilter = ( suite : Suite ) = > doNotFilterFiles . has ( suite . _requireFile ) || ! ! suite . location && testFileLineMatches ( suite . location . file , suite . location . line , suite . location . column ) ;
2022-11-21 16:33:23 -08:00
const testFilter = ( test : TestCase ) = > doNotFilterFiles . has ( test . _requireFile ) || testFileLineMatches ( test . location . file , test . location . line , test . location . column ) ;
2021-07-15 22:02:10 -07:00
return filterSuite ( suite , suiteFilter , testFilter ) ;
2021-06-24 10:02:34 +02:00
}
2022-01-16 08:47:09 -08:00
function filterSuiteWithOnlySemantics ( suite : Suite , suiteFilter : ( suites : Suite ) = > boolean , testFilter : ( test : TestCase ) = > boolean ) {
const onlySuites = suite . suites . filter ( child = > filterSuiteWithOnlySemantics ( child , suiteFilter , testFilter ) || suiteFilter ( child ) ) ;
2021-07-15 22:02:10 -07:00
const onlyTests = suite . tests . filter ( testFilter ) ;
2021-06-06 17:09:53 -07:00
const onlyEntries = new Set ( [ . . . onlySuites , . . . onlyTests ] ) ;
if ( onlyEntries . size ) {
suite . suites = onlySuites ;
2021-07-15 22:02:10 -07:00
suite . tests = onlyTests ;
2021-06-06 17:09:53 -07:00
suite . _entries = suite . _entries . filter ( e = > onlyEntries . has ( e ) ) ; // Preserve the order.
return true ;
}
return false ;
}
2022-01-16 08:47:09 -08:00
function filterSuite ( suite : Suite , suiteFilter : ( suites : Suite ) = > boolean , testFilter : ( test : TestCase ) = > boolean ) {
for ( const child of suite . suites ) {
if ( ! suiteFilter ( child ) )
filterSuite ( child , suiteFilter , testFilter ) ;
}
suite . tests = suite . tests . filter ( testFilter ) ;
const entries = new Set ( [ . . . suite . suites , . . . suite . tests ] ) ;
suite . _entries = suite . _entries . filter ( e = > entries . has ( e ) ) ; // Preserve the order.
}
2022-05-26 14:39:51 -07:00
async function collectFiles ( testDir : string , respectGitIgnore : boolean ) : Promise < string [ ] > {
2022-02-08 15:27:05 -08:00
if ( ! fs . existsSync ( testDir ) )
return [ ] ;
if ( ! fs . statSync ( testDir ) . isDirectory ( ) )
return [ ] ;
2021-06-06 17:09:53 -07:00
type Rule = {
dir : string ;
negate : boolean ;
match : ( s : string , partial? : boolean ) = > boolean
} ;
type IgnoreStatus = 'ignored' | 'included' | 'ignored-but-recurse' ;
const checkIgnores = ( entryPath : string , rules : Rule [ ] , isDirectory : boolean , parentStatus : IgnoreStatus ) = > {
let status = parentStatus ;
for ( const rule of rules ) {
const ruleIncludes = rule . negate ;
if ( ( status === 'included' ) === ruleIncludes )
continue ;
const relative = path . relative ( rule . dir , entryPath ) ;
if ( rule . match ( '/' + relative ) || rule . match ( relative ) ) {
// Matches "/dir/file" or "dir/file"
status = ruleIncludes ? 'included' : 'ignored' ;
} else if ( isDirectory && ( rule . match ( '/' + relative + '/' ) || rule . match ( relative + '/' ) ) ) {
// Matches "/dir/subdir/" or "dir/subdir/" for directories.
status = ruleIncludes ? 'included' : 'ignored' ;
} else if ( isDirectory && ruleIncludes && ( rule . match ( '/' + relative , true ) || rule . match ( relative , true ) ) ) {
// Matches "/dir/donotskip/" when "/dir" is excluded, but "!/dir/donotskip/file" is included.
status = 'ignored-but-recurse' ;
}
}
return status ;
} ;
const files : string [ ] = [ ] ;
const visit = async ( dir : string , rules : Rule [ ] , status : IgnoreStatus ) = > {
const entries = await readDirAsync ( dir , { withFileTypes : true } ) ;
entries . sort ( ( a , b ) = > a . name . localeCompare ( b . name ) ) ;
2022-05-26 14:39:51 -07:00
if ( respectGitIgnore ) {
const gitignore = entries . find ( e = > e . isFile ( ) && e . name === '.gitignore' ) ;
if ( gitignore ) {
const content = await readFileAsync ( path . join ( dir , gitignore . name ) , 'utf8' ) ;
const newRules : Rule [ ] = content . split ( /\r?\n/ ) . map ( s = > {
s = s . trim ( ) ;
if ( ! s )
return ;
// Use flipNegate, because we handle negation ourselves.
const rule = new minimatch . Minimatch ( s , { matchBase : true , dot : true , flipNegate : true } ) as any ;
if ( rule . comment )
return ;
rule . dir = dir ;
return rule ;
} ) . filter ( rule = > ! ! rule ) ;
rules = [ . . . rules , . . . newRules ] ;
}
2021-06-06 17:09:53 -07:00
}
for ( const entry of entries ) {
2022-05-26 14:39:51 -07:00
if ( entry . name === '.' || entry . name === '..' )
continue ;
if ( entry . isFile ( ) && entry . name === '.gitignore' )
2021-06-06 17:09:53 -07:00
continue ;
if ( entry . isDirectory ( ) && entry . name === 'node_modules' )
continue ;
const entryPath = path . join ( dir , entry . name ) ;
const entryStatus = checkIgnores ( entryPath , rules , entry . isDirectory ( ) , status ) ;
if ( entry . isDirectory ( ) && entryStatus !== 'ignored' )
await visit ( entryPath , rules , entryStatus ) ;
else if ( entry . isFile ( ) && entryStatus === 'included' )
files . push ( entryPath ) ;
}
} ;
await visit ( testDir , [ ] , 'included' ) ;
return files ;
}
2021-06-28 22:13:35 +02:00
2021-07-19 14:54:18 -07:00
function buildItemLocation ( rootDir : string , testOrSuite : Suite | TestCase ) {
2021-07-18 17:40:59 -07:00
if ( ! testOrSuite . location )
return '' ;
2021-07-16 12:40:33 -07:00
return ` ${ path . relative ( rootDir , testOrSuite . location . file ) } : ${ testOrSuite . location . line } ` ;
2021-06-28 22:13:35 +02:00
}
2021-07-16 22:34:55 -07:00
2022-09-23 20:01:27 -07:00
function createTestGroups ( projectSuites : Suite [ ] , workers : number ) : TestGroup [ ] {
2021-08-04 21:11:02 -07:00
// This function groups tests that can be run together.
// Tests cannot be run together when:
// - They belong to different projects - requires different workers.
// - They have a different repeatEachIndex - requires different workers.
// - They have a different set of worker fixtures in the pool - requires different workers.
// - They have a different requireFile - reuses the worker, but runs each requireFile separately.
2021-09-02 15:42:07 -07:00
// - They belong to a parallel suite.
// Using the map "workerHash -> requireFile -> group" makes us preserve the natural order
// of worker hashes and require files for the simple cases.
2022-03-28 16:10:32 -07:00
const groups = new Map < string , Map < string , {
// Tests that must be run in order are in the same group.
general : TestGroup ,
2022-07-26 13:38:25 -07:00
// There are 3 kinds of parallel tests:
// - Tests belonging to parallel suites, without beforeAll/afterAll hooks.
// These can be run independently, they are put into their own group, key === test.
// - Tests belonging to parallel suites, with beforeAll/afterAll hooks.
// These should share the worker as much as possible, put into single parallelWithHooks group.
// We'll divide them into equally-sized groups later.
// - Tests belonging to serial suites inside parallel suites.
// These should run as a serial group, each group is independent, key === serial suite.
parallel : Map < Suite | TestCase , TestGroup > ,
2022-03-28 16:10:32 -07:00
parallelWithHooks : TestGroup ,
} >> ( ) ;
2021-09-02 15:42:07 -07:00
const createGroup = ( test : TestCase ) : TestGroup = > {
return {
workerHash : test._workerHash ,
requireFile : test._requireFile ,
2022-01-03 17:29:54 -08:00
repeatEachIndex : test.repeatEachIndex ,
2022-07-27 20:17:19 -07:00
projectId : test._projectId ,
2021-09-02 15:42:07 -07:00
tests : [ ] ,
2022-08-04 08:09:54 -07:00
watchMode : false ,
2022-12-05 18:15:01 -08:00
isProjectSetup : test._isProjectSetup ,
2021-09-02 15:42:07 -07:00
} ;
} ;
2021-08-04 21:11:02 -07:00
2022-09-23 20:01:27 -07:00
for ( const projectSuite of projectSuites ) {
2021-07-27 11:04:38 -07:00
for ( const test of projectSuite . allTests ( ) ) {
2021-09-02 15:42:07 -07:00
let withWorkerHash = groups . get ( test . _workerHash ) ;
if ( ! withWorkerHash ) {
withWorkerHash = new Map ( ) ;
groups . set ( test . _workerHash , withWorkerHash ) ;
2021-07-29 18:27:47 -07:00
}
2021-09-02 15:42:07 -07:00
let withRequireFile = withWorkerHash . get ( test . _requireFile ) ;
if ( ! withRequireFile ) {
withRequireFile = {
general : createGroup ( test ) ,
2022-07-26 13:38:25 -07:00
parallel : new Map ( ) ,
2022-03-28 16:10:32 -07:00
parallelWithHooks : createGroup ( test ) ,
2021-09-02 15:42:07 -07:00
} ;
withWorkerHash . set ( test . _requireFile , withRequireFile ) ;
2021-07-29 18:27:47 -07:00
}
2022-07-26 13:38:25 -07:00
// Note that a parallel suite cannot be inside a serial suite. This is enforced in TestType.
2021-09-02 15:42:07 -07:00
let insideParallel = false ;
2022-07-26 13:38:25 -07:00
let outerMostSerialSuite : Suite | undefined ;
2022-03-28 16:10:32 -07:00
let hasAllHooks = false ;
for ( let parent : Suite | undefined = test . parent ; parent ; parent = parent . parent ) {
2022-07-26 13:38:25 -07:00
if ( parent . _parallelMode === 'serial' )
outerMostSerialSuite = parent ;
insideParallel = insideParallel || parent . _parallelMode === 'parallel' ;
2022-03-28 16:10:32 -07:00
hasAllHooks = hasAllHooks || parent . _hooks . some ( hook = > hook . type === 'beforeAll' || hook . type === 'afterAll' ) ;
}
2021-09-02 15:42:07 -07:00
if ( insideParallel ) {
2022-07-26 13:38:25 -07:00
if ( hasAllHooks && ! outerMostSerialSuite ) {
2022-03-28 16:10:32 -07:00
withRequireFile . parallelWithHooks . tests . push ( test ) ;
} else {
2022-07-26 13:38:25 -07:00
const key = outerMostSerialSuite || test ;
let group = withRequireFile . parallel . get ( key ) ;
if ( ! group ) {
group = createGroup ( test ) ;
withRequireFile . parallel . set ( key , group ) ;
}
2022-03-28 16:10:32 -07:00
group . tests . push ( test ) ;
}
2021-09-02 15:42:07 -07:00
} else {
withRequireFile . general . tests . push ( test ) ;
2021-07-27 11:04:38 -07:00
}
}
}
2021-07-29 18:27:47 -07:00
2021-09-02 15:42:07 -07:00
const result : TestGroup [ ] = [ ] ;
for ( const withWorkerHash of groups . values ( ) ) {
for ( const withRequireFile of withWorkerHash . values ( ) ) {
2022-07-26 13:38:25 -07:00
// Tests without parallel mode should run serially as a single group.
2021-09-02 15:42:07 -07:00
if ( withRequireFile . general . tests . length )
result . push ( withRequireFile . general ) ;
2022-03-28 16:10:32 -07:00
2022-07-26 13:38:25 -07:00
// Parallel test groups without beforeAll/afterAll can be run independently.
result . push ( . . . withRequireFile . parallel . values ( ) ) ;
// Tests with beforeAll/afterAll should try to share workers as much as possible.
2022-03-28 16:10:32 -07:00
const parallelWithHooksGroupSize = Math . ceil ( withRequireFile . parallelWithHooks . tests . length / workers ) ;
let lastGroup : TestGroup | undefined ;
for ( const test of withRequireFile . parallelWithHooks . tests ) {
if ( ! lastGroup || lastGroup . tests . length >= parallelWithHooksGroupSize ) {
lastGroup = createGroup ( test ) ;
result . push ( lastGroup ) ;
}
lastGroup . tests . push ( test ) ;
}
2021-09-02 15:42:07 -07:00
}
}
return result ;
2021-07-27 11:04:38 -07:00
}
2021-07-16 22:34:55 -07:00
class ListModeReporter implements Reporter {
2022-03-28 15:53:42 -07:00
private config ! : FullConfigInternal ;
2022-03-24 07:33:33 -07:00
2022-03-28 15:53:42 -07:00
onBegin ( config : FullConfigInternal , suite : Suite ) : void {
2022-03-24 07:33:33 -07:00
this . config = config ;
2021-11-11 16:48:08 -08:00
// eslint-disable-next-line no-console
2021-07-16 22:34:55 -07:00
console . log ( ` Listing tests: ` ) ;
const tests = suite . allTests ( ) ;
const files = new Set < string > ( ) ;
for ( const test of tests ) {
// root, project, file, ...describes, test
const [ , projectName , , . . . titles ] = test . titlePath ( ) ;
const location = ` ${ path . relative ( config . rootDir , test . location . file ) } : ${ test . location . line } : ${ test . location . column } ` ;
const projectTitle = projectName ? ` [ ${ projectName } ] › ` : '' ;
2021-11-11 16:48:08 -08:00
// eslint-disable-next-line no-console
2021-07-16 22:34:55 -07:00
console . log ( ` ${ projectTitle } ${ location } › ${ titles . join ( ' ' ) } ` ) ;
files . add ( test . location . file ) ;
}
2021-11-11 16:48:08 -08:00
// eslint-disable-next-line no-console
2021-07-16 22:34:55 -07:00
console . log ( ` Total: ${ tests . length } ${ tests . length === 1 ? 'test' : 'tests' } in ${ files . size } ${ files . size === 1 ? 'file' : 'files' } ` ) ;
}
2022-03-24 07:33:33 -07:00
onError ( error : TestError ) {
// eslint-disable-next-line no-console
console . error ( '\n' + formatError ( this . config , error , false ) . message ) ;
}
2021-07-16 22:34:55 -07:00
}
2021-07-20 15:03:01 -05:00
2022-12-21 09:36:59 -08:00
function createForbidOnlyErrors ( config : FullConfigInternal , onlyTestsAndSuites : ( TestCase | Suite ) [ ] ) : TestError [ ] {
const errors : TestError [ ] = [ ] ;
2021-11-11 16:48:08 -08:00
for ( const testOrSuite of onlyTestsAndSuites ) {
// Skip root and file.
const title = testOrSuite . titlePath ( ) . slice ( 2 ) . join ( ' ' ) ;
2022-12-21 09:36:59 -08:00
const error : TestError = {
message : ` Error: focused item found in the --forbid-only mode: " ${ title } " ` ,
location : testOrSuite.location ! ,
} ;
errors . push ( error ) ;
2021-11-11 16:48:08 -08:00
}
2022-12-21 09:36:59 -08:00
return errors ;
2021-11-11 16:48:08 -08:00
}
2022-12-21 09:36:59 -08:00
function createDuplicateTitlesErrors ( config : FullConfigInternal , rootSuite : Suite ) : TestError [ ] {
const errors : TestError [ ] = [ ] ;
2022-05-11 11:53:16 +01:00
for ( const fileSuite of rootSuite . suites ) {
2022-12-21 09:36:59 -08:00
const testsByFullTitle = new Map < string , TestCase > ( ) ;
2022-05-11 11:53:16 +01:00
for ( const test of fileSuite . allTests ( ) ) {
2022-07-27 20:17:19 -07:00
const fullTitle = test . titlePath ( ) . slice ( 2 ) . join ( '\x1e' ) ;
2022-12-21 09:36:59 -08:00
const existingTest = testsByFullTitle . get ( fullTitle ) ;
if ( existingTest ) {
const error : TestError = {
message : ` Error: duplicate test title " ${ fullTitle } ", first declared in ${ buildItemLocation ( config . rootDir , existingTest ) } ` ,
location : test.location ,
} ;
errors . push ( error ) ;
2022-05-11 11:53:16 +01:00
}
2022-12-21 09:36:59 -08:00
testsByFullTitle . set ( fullTitle , test ) ;
2022-05-11 11:53:16 +01:00
}
}
2022-12-21 09:36:59 -08:00
return errors ;
2021-11-11 16:48:08 -08:00
}
function createNoTestsError ( ) : TestError {
return createStacklessError ( ` ================= \ n no tests found. \ n================= ` ) ;
}
2022-12-21 09:36:59 -08:00
function createStacklessError ( message : string , location? : TestError [ 'location' ] ) : TestError {
return { message , location } ;
2021-11-11 16:48:08 -08:00
}
2022-11-08 12:05:00 -08:00
function sanitizeConfigForJSON ( object : any , visited : Set < any > ) : any {
const type = typeof object ;
if ( type === 'function' || type === 'symbol' )
return undefined ;
if ( ! object || type !== 'object' )
return object ;
if ( object instanceof RegExp )
return String ( object ) ;
if ( object instanceof Date )
return object . toISOString ( ) ;
if ( visited . has ( object ) )
return undefined ;
visited . add ( object ) ;
if ( Array . isArray ( object ) )
return object . map ( a = > sanitizeConfigForJSON ( a , visited ) ) ;
const result : any = { } ;
const keys = Object . keys ( object ) . slice ( 0 , 100 ) ;
for ( const key of keys ) {
if ( key . startsWith ( '_' ) )
continue ;
result [ key ] = sanitizeConfigForJSON ( object [ key ] , visited ) ;
}
return result ;
}
2021-10-14 10:17:35 -08:00
export const builtInReporters = [ 'list' , 'line' , 'dot' , 'json' , 'junit' , 'null' , 'github' , 'html' ] as const ;
2021-07-20 15:03:01 -05:00
export type BuiltInReporter = typeof builtInReporters [ number ] ;