import * as Fs from 'fs' ; import * as Path from 'path' ; import { Glob } from 'glob' ; import { dialog } from 'electron' ; import { v4 as uuid } from 'uuid' ; import { findIndex , omit } from 'lodash' ; import { ApplicationWindow } from '@main/application-window' ; import { Frameworks } from '@lib/frameworks' ; import { ProjectEventEmitter } from '@lib/frameworks/emitter' ; import { FrameworkStatus , parseFrameworkStatus } from '@lib/frameworks/status' ; import { ProgressLedger } from '@lib/frameworks/progress' ; import { FrameworkFactory } from '@lib/frameworks/factory' ; import { FrameworkOptions , IFramework } from '@lib/frameworks/framework' ; /**
 * Options to instantiate a Project with.
 */ export type RepositoryOptions = { id ? : string ; name ? : string ; path : string ; expanded ? : boolean ; frameworks ? : Array < FrameworkOptions > ; status ? : FrameworkStatus ; } ; /**
 * Standardised repository information (e.g. used for scanning
 * for test frameworks).
 */ export type ParsedRepository = { path : string ; files : Array < string > ; } ; export interface IRepository extends ProjectEventEmitter { frameworks : Array < IFramework > ; status : FrameworkStatus ; selected : boolean ; scanning : boolean ; getId ( ) : string ; getDisplayName ( ) : string ; start ( ) : void ; refresh ( ) : void ; stop ( ) : Promise < any > ; reset ( ) : Promise < any > ; save ( ) : void ; scan ( ) : Promise < Array < FrameworkOptions > > ; expand ( ) : void ; collapse ( ) : void ; isRunning ( ) : boolean ; isRefreshing ( ) : boolean ; isBusy ( ) : boolean ; empty ( ) : boolean ; count ( ) : number ; isExpanded ( ) : boolean ; render ( ) : RepositoryOptions ; persist ( ) : RepositoryOptions ; addFramework ( options ) : Promise < IFramework > ; removeFramework ( id ) : void ; getFrameworkById ( id ) : IFramework | undefined ; getPath ( ) : string ; exists ( ) : Promise < boolean > ; locate ( window ) : Promise < void > ; getProgressLedger ( ) : ProgressLedger ; resetProgressLedger ( ) : void ; emitFrameworksToRenderer ( ) : void ; } export class Repository extends ProjectEventEmitter implements IRepository { public frameworks : Array < IFramework > = [ ] ; public status : FrameworkStatus = 'loading' ; public selected = false ; public scanning = false ; protected id : string ; protected path : string ; protected name : string ; protected expanded : boolean ; protected parsed = false ; protected ready = false ; protected initialFrameworkCount = 0 ; protected initialFrameworkReady = 0 ; protected progressLedger : ProgressLedger = { run : 0 , total : 0 } ; constructor ( window , options ) { super ( window ) ; this . id = options . id || uuid ( ) ; this . path = options . path ; this . name = options . name || Path . basename ( this . path ) || 'untitled' ; this . expanded = typeof options . expanded === 'undefined' ? true : options . expanded ; this . initialFrameworkCount = ( options . frameworks || [ ] ) . length ; this . exists ( ) ; this . loadFrameworks ( options . frameworks || [ ] ) ; } /**
     * Run all of this repository's test frameworks.
     */ public start ( ) { this . frameworks . forEach ( ( framework ) => { framework . start ( ) ; } ) ; } /**
     * Refresh all of this repository's test frameworks.
     */ public refresh ( ) { this . frameworks . forEach ( ( framework ) => { framework . refresh ( ) ; } ) ; } /**
     * Stop any test framework in this repository that might be running.
     */ public stop ( ) { return Promise . all ( this . frameworks . map ( ( framework ) : Promise < void > => { return new Promise ( async ( resolve , reject ) => { await framework . stop ( ) ; resolve ( ) ; } ) ; } ) ) ; } /**
     * Reset this repository's state.
     */ public async reset ( ) { return Promise . all ( this . frameworks . map ( ( framework ) => { return framework . reset ( ) ; } ) ) ; } /**
     * Whether this repository is running.
     */ public isRunning ( ) { return this . frameworks . some ( ( framework ) => framework . isRunning ( ) ) ; } /**
     * Whether this repository is refreshing.
     */ public isRefreshing ( ) { return this . frameworks . some ( ( framework ) => framework . isRefreshing ( ) ) ; } /**
     * Whether this repository is busy.
     */ public isBusy ( ) { return this . frameworks . some ( ( framework ) => framework . isBusy ( ) ) ; } /**
     * How many frameworks the repository currently has.
     */ public count ( ) { return this . frameworks . length ; } /**
     * Whether this repository has any frameworks.
     */ public empty ( ) { return this . count ( ) === 0 ; } /**
     * Whether this repository is expanded.
     */ public isExpanded ( ) { return this . expanded ; } /**
     * Prepares the repository for sending out to renderer process.
     */ public render ( ) { return { id : this . id , name : this . name , path : this . path , status : this . status , expanded : this . expanded } ; } /**
     * Prepares the repository for persistence.
     */ public persist ( ) { return omit ( { ... this . render ( ) , frameworks : this . frameworks . map ( framework => framework . persist ( ) ) } , 'status' ) ; } /**
     * Save this repository in the persistent store.
     */ public save ( ) { this . emit ( 'change' ) ; } /**
     * Scan the repository folder for testing frameworks.
     */ public async scan ( ) { this . scanning = true ; const glob = new Glob ( '*' , { cwd : this . path , dot : true , sync : true } ) ; return new Promise ( async ( resolve , reject ) => { const frameworks = await Promise . all ( Frameworks . map ( framework => { return framework . spawnForDirectory ( { path : this . path , files : glob . found } ) ; } ) ) ; resolve ( frameworks . filter ( options => ! ! options ) . map ( options => { return < FrameworkOptions > { ... options , scanStatus : 'pending' } ; } ) ) ; this . scanning = false ; } ) ; } /**
     * Get this repository's id.
     */ public getId ( ) { return this . id ; } /**
     * Get this repository's display name.
     */ public getDisplayName ( ) { return this . name ; } /**
     * Expand this repository.
     */ public expand ( ) { this . expanded = true ; this . emit ( 'change' , this ) ; } /**
     * Collapse this repository.
     */ public collapse ( ) { this . expanded = false ; this . emit ( 'change' , this ) ; } /**
     * A function to run when a child framework changes its status.
     */ protected statusListener ( ) { this . updateStatus ( ) ; } /**
     * A function to run when a child framework changes its state (i.e. runs, stops, etc).
     */ protected stateListener ( ) { this . emit ( 'state' ) ; } /**
     * A function to run when a child framework changes.
     */ protected changeListener ( ) { this . save ( ) ; } /**
     * A function to run when a child framework errors out.
     */ protected errorListener ( ) { this . exists ( ) ; } /**
     * A function to run when a child framework starts measuring progress.
     */ protected measuringListener ( frameworkLedger ) { this . progressLedger . total += frameworkLedger . total ; } /**
     * A function to run when a child framework progresses.
     */ protected progressListener ( ) { this . progress ( ) ; } /**
     * Prepare the repository for parsed state.
     */ protected onParsed ( ) { this . parsed = true ; if ( ! this . initialFrameworkCount ) { this . onReady ( ) ; } this . emit ( 'parsed' , this ) ; } /**
     * Prepare the repository for ready state.
     */ protected onReady ( ) { if ( this . ready ) { return ; } this . ready = true ; if ( ! this . initialFrameworkCount ) { this . updateStatus ( ) ; } this . emit ( 'ready' , this ) ; } /**
     * Listener for when a child framework is ready.
     */ protected onFrameworkReady ( ) { this . initialFrameworkReady ++ ; if ( this . initialFrameworkReady >= this . initialFrameworkCount ) { this . onReady ( ) ; } } param to protected updateStatus ( to ? ) { if ( this . status === 'missing' ) { return ; } if ( typeof to === 'undefined' ) { to = parseFrameworkStatus ( this . frameworks . map ( framework => framework . status ) ) ; } const from = this . status ; if ( to !== from ) { this . status = to ; this . emit ( 'status' , to , from ) ; this . emitToRenderer ( `${ this . id }:status:sidebar` , to , from ) ; } } param frameworks protected async loadFrameworks ( frameworks ) { return new Promise ( ( resolve , reject ) => { setTimeout ( ( ) => { Promise . all ( frameworks . map ( ( framework ) => { return this . addFramework ( framework ) ; } ) ) . then ( ( ) => { this . onParsed ( ) ; resolve ( ) ; } ) ; } ) ; } ) ; } param options public async addFramework ( options ) { return new Promise ( ( resolve , reject ) => { const framework = FrameworkFactory . make ( this . window , { ... options , ... { repositoryPath : this . path } } ) ; framework . on ( 'ready' , this . onFrameworkReady . bind ( this ) ) . on ( 'status' , this . statusListener . bind ( this ) ) . on ( 'state' , this . stateListener . bind ( this ) ) . on ( 'change' , this . changeListener . bind ( this ) ) . on ( 'error' , this . errorListener . bind ( this ) ) . on ( 'measuring' , this . measuringListener . bind ( this ) ) . on ( 'progress' , this . progressListener . bind ( this ) ) ; this . frameworks . push ( framework ) ; this . updateStatus ( ) ; resolve ( framework ) ; } ) ; } param id public removeFramework ( id ) { const index = findIndex ( this . frameworks , framework => framework . getId ( ) === id ) ; if ( index > - 1 ) { this . frameworks [ index ] . removeAllListeners ( ) ; this . frameworks . splice ( index , 1 ) ; this . updateStatus ( ) ; this . save ( ) ; } } param id public getFrameworkById ( id ) { const index = findIndex ( this . frameworks , framework => framework . getId ( ) === id ) ; if ( index > - 1 ) { return this . frameworks [ index ] ; } return undefined ; } /**
    * Get this repository's path.
    */ public getPath ( ) { return this . path ; } /**
     * Update the repository's path.
     */ protected async updatePath ( path ) { this . path = path ; await Promise . all ( this . frameworks . map ( ( framework ) => { return framework . updateOptions ( { ... framework . persist ( ) , ... { repositoryPath : path } } ) ; } ) ) ; await this . exists ( ) ; this . save ( ) ; } /**
     * Whether the repository exists in the filesystem
     */ public async exists ( ) { return new Promise ( ( resolve , reject ) => { Fs . access ( this . path , Fs . constants . R_OK , error => { this . status = 'loading' ; this . updateStatus ( error ? 'missing' : undefined ) ; resolve ( ! error ) ; } ) ; } ) ; } /**
     * Locate this repository, if missing.
     */ public async locate ( window ) { const { filePaths } = await dialog . showOpenDialog ( window , { properties : [ 'openDirectory' , 'multiSelections' ] } ) ; if ( ! filePaths || ! filePaths . length ) { return ; } this . updatePath ( filePaths [ 0 ] ) ; } /**
     * Progress the ledger by one unit.
     */ protected progress ( ) { this . progressLedger . run ++ ; this . emit ( 'progress' ) ; } /**
     * Return the framework's progress ledger.
     */ public getProgressLedger ( ) { return this . progressLedger ; } /**
     * Reset the framework's progress ledger.
     */ public resetProgressLedger ( ) { this . progressLedger = { run : 0 , total : 0 } ; this . frameworks . forEach ( ( framework ) => { framework . resetProgressLedger ( ) ; } ) ; } /**
     * Send repository's frameworks to the renderer process.
     */ public emitFrameworksToRenderer ( ) { this . emitToRenderer ( `${ this . id }:frameworks` , this . frameworks . map ( framework => framework . render ( ) ) ) ; } }