1import { ChildProcess } from 'child_process'; 2import { PassThrough, Readable, Writable } from 'stream'; 3import { StringDecoder } from 'string_decoder'; 4import readStream from 'stream-to-string'; 5 6function readChunks(input: Readable): Readable { 7 let output = new PassThrough({ objectMode: true }); 8 let decoder = new StringDecoder('utf8'); 9 input.on('data', data => { 10 output.write(decoder.write(data)); 11 }); 12 input.on('close', () => { 13 output.write(decoder.end()); 14 output.destroy(); 15 }); 16 return output; 17} 18 19function splitLines(s: string): string[] { 20 return s.split(/([^\n]*\r?\n)/).filter(x => x); 21} 22 23function isCompleteLine(s: string): boolean { 24 return s.endsWith('\n'); 25} 26 27class LinesBuffer { 28 29 // INVARIANT: (this.buffer.length > 0) && 30 // !isCompleteLine(this.buffer[this.buffer.length - 1]) 31 // In other words, the last line in the buffer is always incomplete. 32 private buffer: string[]; 33 34 constructor() { 35 this.buffer = [""]; 36 } 37 38 add(lines: string[]) { 39 if (isCompleteLine(lines[lines.length - 1])) { 40 lines.push(""); 41 } 42 this.buffer[this.buffer.length - 1] += lines.shift(); 43 this.buffer = this.buffer.concat(lines); 44 } 45 46 find(p: (s: string) => boolean): string[] | null { 47 let index = this.buffer.findIndex(p); 48 if (index === -1) { 49 return null; 50 } 51 let extracted = this.buffer.splice(0, index + 1); 52 if (this.buffer.length === 0) { 53 this.buffer.push(""); 54 } 55 return extracted; 56 } 57} 58 59async function* run(script: Record<string, string>, stdin: Writable, stdout: Readable) { 60 let lines = new LinesBuffer(); 61 62 let keys = Object.keys(script); 63 let i = 0; 64 for await (let chunk of readChunks(stdout)) { 65 lines.add(splitLines(chunk)); 66 let found = lines.find(line => line.startsWith(keys[i])); 67 if (found) { 68 stdin.write(script[keys[i]] + "\n"); 69 yield found; 70 i++; 71 if (i >= keys.length) { 72 break; 73 } 74 } 75 } 76} 77 78function exit(child: ChildProcess): Promise<number | null> { 79 let resolve: (code: number | null) => void; 80 let result: Promise<number | null> = new Promise(res => { resolve = res; }); 81 child.on('exit', code => { 82 resolve(code); 83 }); 84 return result; 85} 86 87export default async function expect(child: ChildProcess, script: Record<string, string>): Promise<void> { 88 let output: string[][] = []; 89 for await (let lines of run(script, child.stdin!, child.stdout!)) { 90 output.push(lines); 91 } 92 let stderr = await readStream(child.stderr!); 93 let code = await exit(child); 94 switch (code) { 95 case null: 96 throw new Error("child process interrupted"); 97 case 0: 98 return; 99 default: 100 console.log("stderr: " + stderr.trim()); 101 console.log("stdout: " + JSON.stringify(output)); 102 throw new Error("child process exited with code " + code); 103 } 104} 105