import * as Path from 'path' ; import * as Fs from 'fs-extra' ; import { chunk , debounce , find , findIndex , get , omit , orderBy , trim , trimStart } from 'lodash' ; import { v4 as uuid } from 'uuid' ; import * as fuzzy from 'fuzzysearch' ; import { ApplicationWindow } from '@main/application-window' ; import { ProcessId , IProcess } from '@lib/process/process' ; import { ProcessFactory } from '@lib/process/factory' ; import { queue } from '@lib/process/queue' ; import { ProjectEventEmitter } from '@lib/frameworks/emitter' ; import { IRepository , ParsedRepository } from '@lib/frameworks/repository' ; import { Suite , ISuite , ISuiteResult } from '@lib/frameworks/suite' ; import { FrameworkStatus , Status , StatusLedger , StatusMap , parseStatus } from '@lib/frameworks/status' ; import { ProgressLedger } from '@lib/frameworks/progress' ; import { FrameworkSort , sortDirection } from '@lib/frameworks/sort' ; import { FrameworkValidator } from '@lib/frameworks/validator' ; import { SSHOptions } from '@lib/process/ssh' ; import pool from '@lib/process/pool' ; /**
 * Framework object with repository context
 */ export type FrameworkWithContext = { repository : IRepository ; framework : IFramework ; } ; /**
 * Possible outcomes for a framework reload process
 */ export type FrameworkReloadOutcome = 'success' | 'killed' | 'empty' ; /**
 * A list of test suites.
 */ export type SuiteList = { suites : Array < ISuite > ; } ; /**
 * A list of possible framework filters.
 */ export type FrameworkFilter = 'keyword' | 'status' | 'group' ; /**
 * Options to instantiate a Framework with.
 */ export type FrameworkOptions = { id ? : string ; name : string ; type : string ; command : string ; runner ? : string ; path : string ; repositoryPath ? : string ; runsInRemote ? : boolean ; remotePath ? : string ; sshHost ? : string ; sshUser ? : string | null ; sshPort ? : number | null ; sshIdentity ? : string | null ; active ? : boolean ; suites ? : Array < ISuiteResult > ; scanStatus ? : 'pending' | 'removed' ; proprietary : any ; sort ? : FrameworkSort ; selected ? : number ; canToggleTests ? : boolean ; status ? : FrameworkStatus ; } ; /**
 * An object to declare default framework options.
 */ export type FrameworkDefaults = { all : FrameworkOptions ; darwin ? : object ; win32 ? : object ; linux ? : object ; } ; /**
 * The Framework interface.
 */ export interface IFramework extends ProjectEventEmitter { name : string ; type : string ; path : string ; repositoryPath : string ; fullPath : string ; runsInRemote : boolean ; remotePath : string | null ; status : FrameworkStatus ; readonly canToggleTests : boolean ; getId ( ) : string ; getDisplayName ( ) : string ; getRemotePath ( ) : string ; getFullRemotePath ( ) : string ; start ( ) : void ; refresh ( ) : void ; stop ( ) : Promise < any > ; reset ( ) : Promise < any > ; isRunning ( ) : boolean ; isRefreshing ( ) : boolean ; isBusy ( ) : boolean ; empty ( ) : boolean ; count ( ) : number ; render ( ) : FrameworkOptions ; persist ( ) : FrameworkOptions ; save ( ) : void ; updateOptions ( options ) : Promise < void > ; setActive ( active ) : void ; isActive ( ) : boolean ; isSelective ( ) : boolean ; getAllSuites ( ) : Array < ISuite > ; getSuites ( ) : Array < ISuite > ; getSuiteById ( id ) : ISuite | undefined ; getSelected ( ) : SuiteList ; emitSuitesToRenderer ( ) : void ; setFilter ( filter , value ) : void ; getFilter ( filter ) : Array < string > | string | null ; hasFilters ( ) : boolean ; resetFilters ( ) : void ; getLedger ( ) : StatusLedger ; getStatusMap ( ) : StatusMap ; getNuggetStatus ( id ) : Status ; setNuggetStatus ( id , to , from , updateLedger ) : void ; getProgressLedger ( ) : ProgressLedger ; resetProgressLedger ( ) : void ; } /**
 * The Framework class represents a testing framework (e.g. Jest, PHPUnit)
 * and contains a set of test suites (files).
 */ export abstract class Framework extends ProjectEventEmitter implements IFramework { public name ! : string ; public type ! : string ; public path ! : string ; public repositoryPath ! : string ; public fullPath ! : string ; public remotePath ! : string ; public runsInRemote ! : boolean ; public status : FrameworkStatus = 'loading' ; public readonly canToggleTests : boolean = false ; protected id ! : string ; protected command ! : string ; protected sshHost ! : string ; protected sshUser ! : string | null ; protected sshPort ! : number | null ; protected sshIdentity ! : string | null ; protected runner ! : string | null ; protected process ? : ProcessId ; protected running : Array < Promise < void > > = [ ] ; protected killed = false ; protected queue : { [ index ] : Function ; } = { } ; protected ready = false ; protected suites : Array < ISuite > = [ ] ; protected selective = false ; protected selected : SuiteList = { suites : [ ] } ; protected maxSelective = 200 ; protected initialSuiteCount = 0 ; protected initialSuiteReady = 0 ; protected hasSuites = false ; protected proprietary : any = { } ; protected active = false ; protected filters : { [ key in FrameworkFilter ] : Array < string > | string | null ; } = { keyword : null , status : null , group : null } ; protected sort ! : FrameworkSort ; protected ledger : StatusLedger = { queued : 0 , running : 0 , passed : 0 , failed : 0 , incomplete : 0 , skipped : 0 , warning : 0 , partial : 0 , empty : 0 , idle : 0 , error : 0 } ; protected statuses : { [ key ] : Status ; } = { } ; protected progressLedger : ProgressLedger = { run : 0 , total : 0 } ; protected emitLedgerToRenderer : _ . DebouncedFunc < ( ) => Promise < void > > ; static readonly defaults ? : FrameworkDefaults ; static readonly sortDefault : FrameworkSort = 'name' ; constructor ( window , options ) { super ( window ) ; this . emitLedgerToRenderer = debounce ( ( ) => { this . emitToRenderer ( `${ this . id }:ledger` , this . ledger , this . statuses ) ; } , 30 ) ; this . build ( options ) . then ( ( ) => { this . loadSuites ( options . suites || [ ] ) ; } ) ; } param options protected async build ( options ) { this . id = options . id || uuid ( ) ; this . name = options . name ; this . type = options . type ; this . command = options . command . trim ( ) ; this . path = trimStart ( options . path , '/' ) ; this . repositoryPath = options . repositoryPath || '' ; this . fullPath = this . path ? Path . join ( this . repositoryPath , this . path ) : this . repositoryPath ; this . runsInRemote = options . runsInRemote || false ; this . remotePath = options . remotePath || '' ; this . sshHost = options . sshHost || '' ; this . sshUser = options . sshUser || null ; this . sshPort = options . sshPort || null ; this . sshIdentity = options . sshIdentity || null ; this . runner = options . runner || '' ; this . proprietary = options . proprietary || { ... ( this . constructor as typeof Framework ) . getDefaults ( ) . proprietary } ; this . active = options . active || false ; this . sort = options . sort || ( this . constructor as typeof Framework ) . sortDefault ; this . initialSuiteCount = ( options . suites || [ ] ) . length ; this . hasSuites = this . initialSuiteCount > 0 ; } /**
     * Prepare this framework for running.
     */ protected async assemble ( ) { this . emit ( 'state' ) ; this . emit ( 'assembled' ) ; log . debug ( `Assembled ${ this . name }` ) ; } /**
     * Prepare this framework for running.
     */ protected injectPath ( ) { return Path . join ( this . repositoryPath , '.lode' , this . type ) ; } /**
     * Clean-up after running a process for this framework.
     */ protected async disassemble ( ) { if ( this . runsInRemote ) { try { await Fs . remove ( this . injectPath ( ) ) ; const files = await Fs . readdir ( Path . join ( this . repositoryPath , '.lode' ) ) ; if ( ! files . length ) { await Fs . remove ( Path . join ( this . repositoryPath , '.lode' ) ) ; } } catch ( error ) { } } this . emit ( 'state' ) ; this . emit ( 'disassembled' ) ; log . debug ( `Disassembled ${ this . name }` ) ; } /**
     * The command arguments for running this framework.
     */ protected abstract runArgs ( ) ; param suites param selectTests protected abstract runSelectiveArgs ( suites , selectTests ) ; /**
     * Reload this framework's suites and tests.
     */ protected abstract reload ( ) ; param repository public static async spawnForDirectory ( repository ) { return false ; } param options public static hydrate ( options ? ) { options = options || { } ; const defaults = { ... { name : '' , type : '' , command : '' , path : '' , runsInRemote : false , proprietary : { } } , ... this . getDefaults ( ) } ; return { ... defaults , ... options } ; } public static getDefaults ( ) { return { ... { name : '' , type : '' , command : '' , path : '' , runsInRemote : false , proprietary : { } } , ... ( get ( this . defaults , 'all' , { } ) ) , ... ( get ( this . defaults , process . platform , { } ) ) } ; } /**
     * Prepares the framework for sending out to renderer process.
     */ public render ( ) { return { id : this . id , name : this . name , type : this . type , command : this . command , path : this . path , runsInRemote : this . runsInRemote , remotePath : this . remotePath , sshHost : this . sshHost , sshUser : this . sshUser , sshPort : this . sshPort , sshIdentity : this . sshIdentity , active : this . active , status : this . status , proprietary : this . proprietary , sort : this . getSort ( ) , selected : this . getSelected ( ) . suites . length , canToggleTests : this . canToggleTests } ; } /**
     * Prepares the framework for persistence.
     */ public persist ( ) { return omit ( { ... this . render ( ) , suites : this . suites . map ( ( suite ) => suite . persist ( ) ) } , 'status' , 'selective' ) ; } /**
     * Save this framework in the persistent store.
     */ public save ( ) { this . emit ( 'save' ) ; } param options public async updateOptions ( options ) { const initChanged = options . command !== this . command || this . runsInRemote !== options . runsInRemote || this . sshHost !== options . sshHost || this . repositoryPath !== options . repositoryPath ; if ( ! options . runsInRemote ) { options . sshHost = '' ; options . sshUser = null ; options . sshPort = null ; options . sshIdentity = null ; } await this . build ( { ... options , ... { id : options . id || this . id || uuid ( ) } } ) ; if ( initChanged ) { this . reset ( ) ; this . resetSuites ( ) ; this . refresh ( ) ; } this . emit ( 'change' , this ) ; } /**
     * Get this framework's id.
     */ public getId ( ) { return this . id ; } /**
     * Get this framework's display name.
     */ public getDisplayName ( ) { return this . name ; } /**
     * Get this framework's remote path, if any, forcibly prefixing
     * it with a slash as they are used as root paths.
     */ public getRemotePath ( ) { return this . remotePath ? '/' + trimStart ( this . remotePath , '/' ) : '' ; } /**
     * Get this framework's full remote path, if any, including the tests path.
     */ public getFullRemotePath ( ) { if ( ! this . getRemotePath ( ) ) { return '' ; } return Path . join ( this . getRemotePath ( ) , this . path ) ; } param to protected updateStatus ( to ? ) { if ( typeof to === 'undefined' ) { to = parseStatus ( this . suites . map ( suite => suite . getStatus ( ) ) ) ; } const from = this . status ; if ( to !== from ) { this . status = to ; this . emit ( 'status' , to , from ) ; this . emitToRenderer ( `${ this . id }:status:sidebar` , to , from ) ; this . emitToRenderer ( `${ this . id }:status:list` , to , from ) ; } } /**
     * Run this framework's test suites, either fully or selectively.
     */ protected async handleRun ( ) { await this . assemble ( ) ; if ( this . selective || this . hasFilters ( ) ) { return this . runSelective ( ) . then ( ( ) => { this . disassemble ( ) ; } ) . catch ( error => { this . onError ( error ) ; } ) ; } return this . run ( ) . then ( ( ) => { this . disassemble ( ) ; } ) . catch ( error => { this . onError ( error ) ; } ) ; } /**
     * Stop any processes that apply to this framework. This can include
     * running, refreshing or cancelling any queued jobs.
     */ public async stop ( ) { return new Promise < void > ( ( resolve , reject ) => { this . queue = { } ; this . killed = true ; if ( ! this . process ) { resolve ( ) ; return ; } const running = pool . findProcess ( this . process ! ) ; if ( ! running ) { resolve ( ) ; return ; } running ! . on ( 'killed' , ( ) => { resolve ( ) ; } ) . stop ( ) ; } ) . then ( ( ) => { this . killed = false ; this . idleQueued ( ) ; this . updateStatus ( ) ; this . emit ( 'change' , this ) ; log . debug ( `Stopping ${ this . name }` ) ; } ) . catch ( error => { this . killed = false ; this . onError ( error ) ; } ) ; } /**
     * Reset this framework's state.
     */ public async reset ( ) { this . resetFilters ( ) ; } /**
     * Whether this framework is running.
     */ public isRunning ( ) { return this . status === 'running' ; } /**
     * Whether this framework is refreshing.
     */ public isRefreshing ( ) { return this . status === 'refreshing' ; } /**
     * Whether this framework is busy.
     */ public isBusy ( ) { return this . isRunning ( ) || this . isRefreshing ( ) || this . status === 'queued' ; } /**
     * How many suites the framework currently has.
     */ public count ( ) { return this . suites . length ; } /**
     * Whether this framework has any suites.
     */ public empty ( ) { return this . count ( ) === 0 ; } /**
     * Run all suites inside this framework.
     */ protected async run ( ) { this . rebuildStatusMap ( 'queued' ) ; this . rebuildLedger ( ) ; return new Promise ( ( resolve , reject ) => { this . updateStatus ( 'running' ) ; this . reload ( ) . then ( ( outcome ) => { if ( outcome === 'killed' || this . killed ) { log . debug ( `Process was killed while reloading before run.` ) ; resolve ( ) ; return ; } this . rebuildStatusMap ( 'queued' ) ; this . afterRefresh ( ) ; this . report ( this . runArgs ( ) ) . then ( ( outcome ) => { if ( outcome !== 'killed' ) { this . afterRun ( ) ; } resolve ( ) ; } ) . catch ( error => { reject ( error ) ; } ) ; } ) . catch ( error => { reject ( error ) ; } ) ; } ) ; } /**
     * Run this framework selectively (i.e. only run suites that have been
     * selected by the user).
     */ protected async runSelective ( ) { const suites = this . selective ? this . selected . suites : this . getSuites ( ) ; if ( ! this . selective && suites . length === this . suites . length ) { return this . run ( ) ; } suites . forEach ( suite => { suite . getNuggetIds ( this . selective ) . forEach ( id => { this . statuses [ id ] = 'queued' ; } ) ; } ) ; this . rebuildLedger ( ) ; return new Promise ( ( resolve , reject ) => { this . updateStatus ( 'running' ) ; chunk ( this . sortSuites ( suites ) , this . maxSelective ) . reduce ( ( step , chunk ) => { return step . then ( ( outcome ) => { if ( outcome === 'success' ) { return this . report ( this . runSelectiveArgs ( chunk , this . selective ) ) ; } return '' ; } ) ; } , Promise . resolve ( 'success' ) ) . then ( ( outcome ) => { if ( outcome !== 'killed' ) { this . afterRun ( ) ; } resolve ( ) ; } ) . catch ( error => { reject ( error ) ; } ) ; } ) ; } /**
     * Refresh the list of suites inside this framework.
     */ protected async handleRefresh ( ) { await this . assemble ( ) ; this . updateStatus ( 'refreshing' ) ; this . suites . forEach ( suite => { suite . setFresh ( false ) ; } ) ; return this . reload ( ) . then ( outcome => { if ( outcome !== 'killed' ) { this . afterRefresh ( ) ; this . updateStatus ( ) ; this . emit ( 'change' , this ) ; } this . disassemble ( ) ; } ) . catch ( error => { this . onError ( error ) ; } ) ; } param args protected async report ( args ) { return new Promise ( ( resolve , reject ) => { this . spawn ( args ) . on ( 'report' , ( { report } ) => { this . progress ( ) ; try { if ( typeof report === 'string' ) { throw Error ( 'Report was malformed.' ) ; } this . running . push ( this . debriefSuite ( report ) ) ; } catch ( error ) { this . emit ( 'error' , error ) ; this . emitToRenderer ( `${ this . id }:error` , error . toString ( ) , this . troubleshoot ( error ) ) ; log . error ( 'Failed to debrief suite results.' , error ) ; } } ) . on ( 'success' , ( ) => { Promise . all ( this . running ) . then ( ( ) => { resolve ( 'success' ) ; } ) ; } ) . on ( 'killed' , ( ) => { resolve ( 'killed' ) ; } ) . on ( 'error' , error => { reject ( error ) ; } ) ; } ) ; } /**
     * A function that runs after a framework has been refreshed.
     */ protected afterRefresh ( ) { this . cleanStaleSuites ( ) ; this . suites . filter ( suite => suite . expanded ) . forEach ( suite => { suite . emitTestsToRenderer ( ) ; suite . tests . filter ( test => test . expanded ) . forEach ( test => { test . emitTestsToRenderer ( ) ; } ) ; } ) ; } /**
     * A function that runs after a framework has been run, either fully or
     * selectively.
     */ protected afterRun ( ) { if ( this . selective ) { this . selected . suites . forEach ( suite => { suite . getNuggetIds ( true ) . forEach ( id => { if ( [ 'queued' , 'running' ] . indexOf ( this . statuses [ id ] ) > - 1 ) { this . statuses [ id ] = 'error' ; } } ) ; } ) ; } else { let nuggets = [ ] ; this . suites = this . suites . filter ( ( suite ) => { if ( suite . getStatus ( ) === 'queued' ) { this . onSuiteRemove ( suite ) ; return false ; } nuggets = nuggets . concat ( suite . getNuggetIds ( false ) ) ; return true ; } ) ; Object . keys ( this . statuses ) . filter ( id => ! nuggets . includes ( id ) ) . forEach ( id => { delete this . statuses [ id ] ; } ) ; this . emitSuitesToRenderer ( ) ; } this . rebuildLedger ( ) ; this . updateStatus ( ) ; this . emit ( 'change' , this ) ; } /**
     * Clean currently loaded suites that are not marked as "fresh".
     */ protected cleanStaleSuites ( ) { let nuggets = [ ] ; this . suites = this . suites . filter ( ( suite ) => { if ( ! suite . isFresh ( ) ) { this . onSuiteRemove ( suite ) ; return false ; } nuggets = nuggets . concat ( suite . getNuggetIds ( false ) ) ; return true ; } ) ; Object . keys ( this . statuses ) . filter ( id => ! nuggets . includes ( id ) ) . forEach ( id => { delete this . statuses [ id ] ; } ) ; this . emitSuitesToRenderer ( ) ; this . rebuildLedger ( ) ; } param suite protected onSuiteRemove ( suite ) { suite . removeAllListeners ( ) ; this . updateSelected ( suite ) ; } param error protected onError ( error ) { this . idleQueued ( ) ; this . updateStatus ( 'error' ) ; this . emit ( 'error' , error ) ; this . emitToRenderer ( `${ this . id }:error` , error . toString ( ) , this . troubleshoot ( error ) ) ; this . emit ( 'change' , this ) ; this . disassemble ( ) ; } /**
     * Reset all previously queued suites.
     */ protected idleQueued ( ) { const suites = this . selective ? this . selected . suites : this . suites ; suites . forEach ( suite => { const nuggets = suite . getNuggetIds ( this . selective ) ; nuggets . shift ( ) ; nuggets . forEach ( id => { if ( [ 'queued' , 'running' ] . indexOf ( this . getNuggetStatus ( id ) ) > - 1 ) { this . statuses [ id ] = 'idle' ; } } ) ; this . statuses [ suite . getId ( ) ] = parseStatus ( nuggets . map ( id => this . getNuggetStatus ( id ) ) ) ; } ) ; this . rebuildLedger ( ) ; } param args protected spawn ( args ) { const process = ProcessFactory . make ( { command : this . command , args , path : this . repositoryPath , forceRunner : this . runner , ssh : ! ! this . sshHost , sshOptions : < SSHOptions > { host : this . sshHost , user : this . sshUser , port : this . sshPort , identity : this . sshIdentity , path : this . getRemotePath ( ) } } ) ; this . process = process . getId ( ) ; return process ; } param partial protected hydrateSuiteResult ( partial ) { return partial ; } /**
     * The class of suite we use for this framework.
     */ protected suiteClass ( ) { return Suite ; } param result protected newSuite ( result ) { const SuiteClass = this . suiteClass ( ) ; return new SuiteClass ( this , result ) ; } param file protected findSuite ( file ) { return find ( this . suites , { file } ) ; } param result param rebuild protected async makeSuite ( result , rebuild = false ) { return new Promise ( async ( resolve , reject ) => { let suite = this . findSuite ( result . file ) ; if ( ! suite ) { suite = this . newSuite ( result ) ; suite . on ( 'selected' , this . updateSelected . bind ( this ) ) ; this . suites . push ( suite ) ; } else if ( rebuild ) { await suite . rebuildTests ( result ) ; } suite . setFresh ( true ) ; this . onSuiteReady ( ) ; resolve ( suite ) ; } ) ; } /**
     * Clear the framework's suites.
     */ protected resetSuites ( ) { this . suites = [ ] ; this . selected = { suites : [ ] } ; this . selective = false ; this . rebuildStatusMap ( ) ; this . rebuildLedger ( ) ; } /**
     * Prepare the framework for ready state.
     */ protected onReady ( ) { if ( this . ready ) { return ; } this . ready = true ; this . updateStatus ( ) ; this . emit ( 'ready' , this ) ; } /**
     * Listener for when a child suite is ready.
     */ protected onSuiteReady ( ) { this . initialSuiteReady ++ ; } param suites protected async loadSuites ( suites ) { return new Promise ( ( resolve , reject ) => { Promise . all ( suites . map ( ( result ) => { return this . makeSuite ( this . hydrateSuiteResult ( result ) , true ) ; } ) ) . then ( ( ) => { this . rebuildStatusMap ( ) ; this . rebuildLedger ( ) ; this . onReady ( ) ; resolve ( ) ; } ) ; } ) ; } param file protected fileInPath ( file ) { return file . startsWith ( this . runsInRemote ? this . getRemotePath ( ) : this . fullPath ) ; } param path param file protected pathInFile ( path , file ) { return trim ( path , '/' ) . split ( '/' ) . reduce ( ( found , directory ) => { if ( file . startsWith ( found + directory + '/' ) ) { found += directory + '/' ; return found ; } return '' ; } , '' ) ; } param suite protected updateSelected ( suite ) { const index = findIndex ( this . selected . suites , selected => selected . getId ( ) === suite . getId ( ) ) ; if ( suite . selected && index === - 1 ) { this . selected . suites . push ( suite ) ; } else if ( index > - 1 ) { this . selected . suites . splice ( index , 1 ) ; } this . selective = this . selected . suites . length > 0 ; this . emitToRenderer ( `${ this . id }:selective` , this . selected . suites . length ) ; } param to param from protected updateLedger ( to , from ) { if ( to ) { this . ledger [ to ] ! ++ ; } this . ledger [ from ] ! -- ; this . emitLedgerToRenderer ( ) ; } /**
     * Rebuild the status ledger
     */ protected rebuildLedger ( ) { for ( const key of Object . keys ( this . ledger ) ) { this . ledger [ < Status > key ] = 0 ; } this . suites . forEach ( suite => { this . ledger [ suite . getStatus ( ) ] ++ ; } ) ; this . emitLedgerToRenderer ( ) ; } /**
     * Return the framework's status ledger.
     */ public getLedger ( ) { return this . ledger ; } /**
     * Rebuild the status map.
     */ protected rebuildStatusMap ( status = 'idle' ) { this . statuses = { } ; this . suites . forEach ( ( suite ) => { suite . getNuggetIds ( false ) . forEach ( id => { this . statuses [ id ] = status ; } ) ; } ) ; } /**
     * Return the framework's status map.
     */ public getStatusMap ( ) { return this . statuses ; } /**
     * Return the status of a nugget from this framework.
     */ public getNuggetStatus ( id ) { return get ( this . statuses , id , 'idle' ) ; } /**
     * Set the status of a nugget from this framework.
     */ public setNuggetStatus ( id , to , from , updateLedger ) { this . statuses [ id ] = to ; if ( updateLedger ) { this . updateLedger ( to , from ) ; return ; } this . emitLedgerToRenderer ( ) ; } /**
     * Progress the ledger by one unit.
     */ protected progress ( ) { this . progressLedger . run ++ ; this . emit ( 'progress' ) ; } param suites protected measureProgressForSuites ( suites ) { this . updateProgressLedger ( 0 , this . calculateProgressTotalForSuites ( suites ) ) ; this . emit ( 'measuring' , this . progressLedger ) ; } param suites protected calculateProgressTotalForSuites ( suites ) { return suites . length ; } param run param total protected updateProgressLedger ( run , total ? ) { this . progressLedger . run = run ; if ( typeof total !== 'undefined' ) { this . progressLedger . total = total ; } } /**
     * Return the framework's progress ledger.
     */ public getProgressLedger ( ) { return this . progressLedger ; } /**
     * Reset the framework's progress ledger.
     */ public resetProgressLedger ( ) { this . updateProgressLedger ( 0 , 0 ) ; } param partial protected async debriefSuite ( partial ) { return new Promise ( ( resolve , reject ) => { const result = this . hydrateSuiteResult ( partial ) ; this . makeSuite ( result ) . then ( ( suite ) => { suite . debrief ( result , this . shouldCleanup ( suite ) ) . then ( ( ) => { resolve ( ) ; } ) ; } ) ; } ) ; } param suite protected shouldCleanup ( suite ) { if ( ! this . selective ) { return true ; } return suite . selected && ! suite . partial ; } /**
     * Queue a run job with a unique id. This let's us cancel the job
     * if it's not yet executed simply by clearing the internal queue object.
     */ public start ( ) { if ( this . isBusy ( ) || Object . keys ( this . queue ) . length > 0 ) { return ; } this . updateStatus ( 'queued' ) ; const id = uuid ( ) ; this . queue [ id ] = ( ) => this . handleRun ( ) ; queue . add ( ( ) => this . handleQueued ( id ) ) ; this . measureProgressForSuites ( this . selective ? this . selected . suites : ( this . hasFilters ( ) ? this . getSuites ( ) : this . suites ) ) ; } /**
     * Queue a `refresh` job with a unique id.
     * See @start for more info.
     */ public refresh ( ) { if ( this . isBusy ( ) || Object . keys ( this . queue ) . length > 0 ) { return ; } this . updateStatus ( 'queued' ) ; const id = uuid ( ) ; this . queue [ id ] = ( ) => this . handleRefresh ( ) ; queue . add ( ( ) => this . handleQueued ( id ) ) ; } param id protected handleQueued ( id ) { if ( typeof this . queue [ id ] === 'undefined' ) { log . debug ( `Queued job with id ${ id } was cancelled before execution.` ) ; return ( ) => Promise . resolve ( ) ; } log . debug ( `Running queued job with id ${ id }.` ) ; const job = this . queue [ id ] ; delete this . queue [ id ] ; return job ( ) ; } param active public setActive ( active ) { this . active = active ; this . emit ( 'change' , this ) ; } /**
     * Get the active state of a framework.
     */ public isActive ( ) { return this . active ; } /**
     * Whether the framework has any selected suites.
     */ public isSelective ( ) { return this . selective ; } /**
     * Get all the framework's suites in active sort order.
     */ public getAllSuites ( ) { return this . sortSuites ( this . suites ) ; } /**
     * Get the framework's suites, considering active filters
     * and sort order.
     */ public getSuites ( ) { if ( ! this . hasFilters ( ) ) { return this . getAllSuites ( ) ; } const exact = this . filters . keyword && ( this . filters . keyword as string ) . match ( /^[\'\"].+[\'\"]$/g ) ; const keyword = this . getFilterKeyword ( ) ; return this . sortSuites ( this . suites . filter ( ( suite ) => { let match = true ; if ( keyword ) { if ( exact ) { match = suite . getFilePath ( ) . toUpperCase ( ) . indexOf ( keyword ) > - 1 ; } else { match = fuzzy ( keyword , suite . getDisplayName ( ) . toUpperCase ( ) ) ; } } if ( this . filters . status ) { match = match && ! ( this . filters . status . indexOf ( suite . getStatus ( ) ) === - 1 && ( this . filters . status . indexOf ( 'selected' ) === - 1 || ! suite . selected ) && [ 'queued' , 'running' ] . indexOf ( suite . getStatus ( ) ) === - 1 ) ; } if ( ! match ) { if ( suite . selected ) { suite . toggleSelected ( false , true ) ; } return false ; } return true ; } ) ) ; } param id public getSuiteById ( id ) { const index = findIndex ( this . suites , suite => suite . getId ( ) === id ) ; if ( index > - 1 ) { return this . suites [ index ] ; } return undefined ; } /**
     * Get the framework's selected suites.
     */ public getSelected ( ) { return this . selected ; } /**
     * Send current suites to renderer process.
     */ public emitSuitesToRenderer ( ) { this . emitToRenderer ( `${ this . id }:refreshed` , this . getSuites ( ) . map ( ( suite ) => suite . render ( false ) ) , this . count ( ) ) ; } /**
     * Set a filter for this framework.
     */ public setFilter ( filter , value ) { this . filters [ filter ] = Array . isArray ( value ) ? ( value . length ? value : null ) : value ; this . emitSuitesToRenderer ( ) ; } /**
     * Get the value of a framework filter.
     */ public getFilter ( filter ) { return this . filters [ filter ] ; } /**
     * Whether the framework has any filters currently active.
     */ public hasFilters ( ) { return Object . values ( this . filters ) . some ( value => ! ! value ) ; } /**
     * Reset all filters for this framework.
     */ public resetFilters ( ) { this . filters = { keyword : null , status : null , group : null } ; this . emitSuitesToRenderer ( ) ; } /**
     * Get the relevant part of the current filter keyword (i.e. minus the
     * path sections not shown in the interface), and processed in a way that
     * that be processes by the filtering routine.
     */ protected getFilterKeyword ( ) { if ( ! this . filters . keyword ) { return null ; } let keyword = trim ( ( this . filters . keyword as string ) , `"'/\\` ) ; const remoteEmbeddedPath = this . pathInFile ( this . getFullRemotePath ( ) , keyword ) ; const embeddedPath = this . pathInFile ( this . fullPath , keyword ) ; if ( this . runsInRemote && remoteEmbeddedPath ) { keyword = Path . relative ( remoteEmbeddedPath , keyword ) ; } else if ( embeddedPath ) { keyword = Path . relative ( embeddedPath , keyword ) ; } return keyword . toUpperCase ( ) ; } /**
     * Get the current sort option for this framework.
     */ protected getSort ( ) { return ( this . constructor as typeof Framework ) . sortDefault ; } param suites protected sortSuites ( suites ) { return orderBy ( suites , ( suite ) => this . sortProperty ( suite , this . sort ) , sortDirection ( this . sort , false ) ) ; } param suite param sort protected sortProperty ( suite , sort ? ) { switch ( sort ) { case 'framework' : return suite . getRunningOrder ( ) ; case 'name' : return suite . getDisplayName ( ) ; default : return null ; } } /**
     * Validate framework specific options. This is meant to be overridden
     * by specific framework implementations (e.g. PHPUnit).
     */ public static validate ( validator , options ) { } /**
     * Provide setup instructions for using Lode with a testing framework.
     */ public static instructions ( ) { return '' ; } param error protected troubleshoot ( error ) { if ( error instanceof Error ) { error = error . toString ( ) ; } if ( error . includes ( 'EACCES' ) ) { return 'When running commands that connect into a remote machine instance, make sure all arguments needed to connect to the machine and running the test framework after doing so are present in the command itself. Using a batch script or storing SSH connection information in a file might be helpful or even required.' ; } if ( error === '' && this . runsInRemote ) { return 'Is the remote machine that runs the tests running?' ; } return '' ; } }