import { EventEmitter } from 'events' ; import { Writable } from 'stream' ; import { System } from '../../src/helpers/system' ; import { Plugin , mapFancyTestAsyncPlugin } from './FancyTestAsync' ; import { debug } from './test' ; type SimulatedResponse = { stdin : string ; } | { signal : 'SIGINT' ; } | { filename : string ; contents : Buffer | null ; } ; interface PromptTest { ( value ) : boolean ; } /**
 * An iterable queue that iterates once over values pushed into it.
 */ class IterableAsyncQueue < T > { open : boolean = true ; items : T [ ] = [ ] ; push ( ... items ) { this . items . push ( ... items ) ; } close ( ) { this . open = false ; } async * [ Symbol . asyncIterator ] ( ) { while ( ( await new Promise ( resolve => { this . push = ( ... items ) => { this . items . push ( ... items ) ; resolve ( true ) ; } ; this . close = ( ) => { this . open = false ; resolve ( true ) ; } ; } ) ) && this . open ) { while ( this . items . length > 0 ) { yield this . items . shift ( ) ; } } } } function spyStreamWrites ( stream ) { const dataIterable = new IterableAsyncQueue < string > ( ) ; const write = stream . write ; stream . write = function ( this , message , ... args ) : boolean { dataIterable . push ( ArrayBuffer . isView ( message ) ? new TextDecoder ( ) . decode ( message ) : message ) ; return write . apply ( this , [ message , ... args ] as any ) ; } ; const end = stream . end ; stream . end = function ( this , ... args ) : ReturnType < typeof end > { if ( typeof args [ 0 ] === 'string' ) { dataIterable . push ( args [ 0 ] ) ; } else if ( Buffer . isBuffer ( args [ 0 ] ) ) { dataIterable . push ( args [ 0 ] . toString ( ) ) ; } else if ( ArrayBuffer . isView ( args [ 0 ] ) ) { dataIterable . push ( new TextDecoder ( ) . decode ( args [ 0 ] ) ) ; } return end . apply ( this , args as Parameters < typeof end > ) ; } ; const uninstall = ( ) => { dataIterable . close ( ) ; stream . write = write ; stream . end = end ; } ; return { dataIterable , uninstall , } ; } param dataIterable param predicate async function waitForAsync ( dataIterable , predicate ) { let data = '' ; for await ( const chunk of dataIterable ) { data += chunk ; if ( predicate ( data ) ) { break ; } } } param emitter returns function spyNewListeners ( realEmitter ) { const methodNames = [ 'on' , 'once' , 'addListener' ] as const ; const savedState = { } as { [ key ] : ( ... args : any [ ] ) => any ; } ; const fakeEmitter = new EventEmitter ( ) ; for ( const methodName of methodNames ) { const realMethod = ( savedState [ methodName ] = realEmitter [ methodName ] ) ; realEmitter [ methodName ] = function ( this , event , listener ) { fakeEmitter [ methodName ] ( event , listener ) ; return realMethod . call ( this , event , listener ) ; } ; } return { fakeEmitter , uninstall ( ) { for ( const methodName of methodNames ) { realEmitter [ methodName ] = savedState [ methodName ] ; } } , } ; } param prompt param response function answerThread ( prompt , response ) { return async ( ctx ) => { debug ( 'binding answerer for: %s' , prompt ) ; let realEmitter ; let simulate ; if ( 'stdin' in response ) { realEmitter = process . stdin ; simulate = ( emitter ) => { emitter . emit ( 'data' , response . stdin ) ; emitter . emit ( 'data' , '\n' ) ; } ; } else if ( 'signal' in response ) { realEmitter = process ; simulate = ( emitter ) => { emitter . emit ( response . signal ) ; } ; } else { realEmitter = new EventEmitter ( ) ; simulate = ( ) => { const { contents , filename } = response ; if ( ! contents ) { ctx . system . fs . unlinkAsync ( filename ) ; } else { ctx . system . fs . writeFileAsync ( filename , contents ) ; } } ; } const { fakeEmitter , uninstall } = spyNewListeners ( realEmitter ) ; const errSpy = spyStreamWrites ( process . stderr ) ; const outSpy = spyStreamWrites ( process . stdout ) ; await Promise . race ( [ waitForAsync ( errSpy . dataIterable , prompt ) , waitForAsync ( outSpy . dataIterable , prompt ) , ] ) ; uninstall ( ) ; errSpy . uninstall ( ) ; outSpy . uninstall ( ) ; simulate ( fakeEmitter ) ; } ; } /**
 * Return a FancyTest Plugin that calls a function to do work and test the code
 * in parallel with other FancyTest Plugins.
 */ function testPluginFrom ( threadAsync ) { let threadPromise ; return mapFancyTestAsyncPlugin ( { run ( ctx ) { threadPromise = threadAsync ( ctx ) ; } , async finallyAsync ( ctx ) { try { await threadPromise ; } catch ( err ) { if ( ! ctx . error ) { ctx . error = err ; } } } , } ) ; } function castPromptTest ( prompt ) { if ( typeof prompt === 'string' ) { return data => data . includes ( prompt ) ; } return data => prompt . test ( data ) ; } param prompt param response export function answer ( prompt , response ) { debug ( 'will answer: %s' , prompt ) ; return testPluginFrom ( answerThread ( castPromptTest ( prompt ) , response ) ) ; }