microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
test/resources/processExecution/simulator.ts
201lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. |
| 3 | |
| 4 | import assert = require("assert"); |
| 5 | import * as child_process from "child_process"; |
| 6 | |
| 7 | import { ISpawnResult, ChildProcess } from "../../../src/common/node/childProcess"; |
| 8 | import { PromiseUtil } from "../../../src/common/node/promise"; |
| 9 | |
| 10 | import { IStdOutEvent, IStdErrEvent, IErrorEvent, IExitEvent, ICustomEvent } from "./recording"; |
| 11 | import * as recording from "./recording"; |
| 12 | import * as simulators from "../simulators/childProcess"; |
| 13 | |
| 14 | export type IEventArguments = recording.IEventArguments; |
| 15 | export type Recording = recording.Recording; |
| 16 | |
| 17 | export interface ISimulationResult { |
| 18 | simulatedProcess: child_process.ChildProcess; |
| 19 | simulationEnded: Promise<void> | void; |
| 20 | } |
| 21 | |
| 22 | /* The side effects definition has rule to identify when an event with side effects happened in the simulation, |
| 23 | and the callback that must be called for the simulator to simulate that side-effect during the tests. |
| 24 | e.g.: When the 'projectWasCreated' event happens, we call a callback to actually create the project */ |
| 25 | export interface ISideEffectsDefinition { |
| 26 | beforeStart: () => Promise<void>; |
| 27 | outputBased: IOutputBasedSideEffectDefinition[]; |
| 28 | beforeSuccess: (stdout: string, stderr: string) => Promise<void>; |
| 29 | } |
| 30 | |
| 31 | type IOutputBasedSideEffectDefinition = |
| 32 | | IOutputSingleEventBasedSideEffectDefinition |
| 33 | | IWholeOutputBasedSideEffectDefinition; |
| 34 | |
| 35 | // Side effects based on analyzing each stdout event individually |
| 36 | export interface IOutputSingleEventBasedSideEffectDefinition { |
| 37 | eventPattern: RegExp; |
| 38 | action: () => Promise<void>; |
| 39 | } |
| 40 | |
| 41 | // Side effects based on analyzing the whole stdout of the recording |
| 42 | export interface IWholeOutputBasedSideEffectDefinition { |
| 43 | wholeOutputPattern: RegExp; |
| 44 | action: () => Promise<void>; |
| 45 | } |
| 46 | |
| 47 | /* We use this class to replay the events that we captured from a real execution of a process, to get |
| 48 | the best possible simulation of that processes for our tests */ |
| 49 | export class Simulator { |
| 50 | private process = new simulators.ChildProcess(); // Fake child process where we'll simulate the events that are recorded |
| 51 | |
| 52 | private wholeOutputBasedDefinitions: IWholeOutputBasedSideEffectDefinition[]; |
| 53 | private outputEventBasedDefinitions: IOutputSingleEventBasedSideEffectDefinition[]; |
| 54 | |
| 55 | private allSimulatedEvents: IEventArguments[] = []; |
| 56 | |
| 57 | private allStdout = ""; // All the stdout the recordings have generated so far |
| 58 | private allStderr = ""; // All the stderr the recordings have generated so far |
| 59 | |
| 60 | constructor(private sideEffectsDefinition: ISideEffectsDefinition) { |
| 61 | // We extract the whole output rules and the single event output rules into two different lists. |
| 62 | this.outputEventBasedDefinitions = <IOutputSingleEventBasedSideEffectDefinition[]>( |
| 63 | this.sideEffectsDefinition.outputBased.filter( |
| 64 | definition => !this.isWholeOutputDefinition(definition), |
| 65 | ) |
| 66 | ); |
| 67 | this.wholeOutputBasedDefinitions = <IWholeOutputBasedSideEffectDefinition[]>( |
| 68 | this.sideEffectsDefinition.outputBased.filter(definition => |
| 69 | this.isWholeOutputDefinition(definition), |
| 70 | ) |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | /* Given that we use ChildProcess for spawning processes, we create this spawn method with a |
| 75 | similar result, so it'll be easier for simulated/fake classes to behave similar to the real |
| 76 | ChildProcess class when spawning a simulated process */ |
| 77 | public spawn(): ISpawnResult { |
| 78 | const fakeChildProcessModule = <typeof child_process>(<any>{ |
| 79 | spawn: () => { |
| 80 | return this.process; |
| 81 | }, |
| 82 | }); |
| 83 | |
| 84 | /* We call spawn to fill the ISpawnResult object appropiatedly. The command |
| 85 | and the arguments don't affect that object, so we just pass an empty command and parameters */ |
| 86 | return new ChildProcess({ childProcess: fakeChildProcessModule }).spawn("", []); |
| 87 | } |
| 88 | |
| 89 | public simulate(simRecording: Recording): Promise<void> { |
| 90 | assert(simRecording, "recording shouldn't be null"); |
| 91 | return this.sideEffectsDefinition.beforeStart().then(() => { |
| 92 | return this.simulateAllEvents(simRecording.events); |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | public simulateAllEvents(events: IEventArguments[]): Promise<void> { |
| 97 | return PromiseUtil.reduce(events, (event: IEventArguments) => |
| 98 | this.simulateSingleEvent(event), |
| 99 | ); |
| 100 | } |
| 101 | |
| 102 | public getAllSimulatedEvents(): IEventArguments[] { |
| 103 | return this.allSimulatedEvents; |
| 104 | } |
| 105 | |
| 106 | private isWholeOutputDefinition(definition: IOutputBasedSideEffectDefinition): boolean { |
| 107 | return definition.hasOwnProperty("wholeOutputPattern"); |
| 108 | } |
| 109 | |
| 110 | private simulateOutputSideEffects(data: string, previousOutputLength: number): Promise<void> { |
| 111 | /* We store the applicable side effects with the index where they were applicable, so we execute the |
| 112 | ones that were detected earlier in the recording first */ |
| 113 | const applicableSideEffectDefinitions: { |
| 114 | index: number; |
| 115 | definition: IOutputBasedSideEffectDefinition; |
| 116 | }[] = []; |
| 117 | |
| 118 | this.outputEventBasedDefinitions.forEach(definition => { |
| 119 | const match = data.match(definition.eventPattern); |
| 120 | if (match && match.index !== undefined) { |
| 121 | applicableSideEffectDefinitions.push({ |
| 122 | index: previousOutputLength + match.index, // Index relative to the whole output |
| 123 | definition: definition, |
| 124 | }); |
| 125 | } |
| 126 | }); |
| 127 | |
| 128 | /* We add the elements that match the whole output to applicableSideEffectDefinitions, and we remove them |
| 129 | from future iterations of wholeOutputBasedDefinitions so they won't be matched again. */ |
| 130 | this.wholeOutputBasedDefinitions = this.wholeOutputBasedDefinitions.filter(definition => { |
| 131 | const match = this.allStdout.match(definition.wholeOutputPattern); |
| 132 | if (match && match.index !== undefined) { |
| 133 | applicableSideEffectDefinitions.push({ |
| 134 | index: match.index, |
| 135 | definition: definition, |
| 136 | }); |
| 137 | return false; // We've just matched the output. Remove it from future iterations of wholeOutputBasedDefinitions |
| 138 | } |
| 139 | |
| 140 | return true; // We didn't match yet, keep it for future iterations of wholeOutputBasedDefinitions |
| 141 | }); |
| 142 | |
| 143 | // Sort by index, so the action matching the earlier text gets executed first |
| 144 | applicableSideEffectDefinitions.sort((a, b) => a.index - b.index); |
| 145 | |
| 146 | return PromiseUtil.reduce(applicableSideEffectDefinitions, definition => |
| 147 | definition.definition.action(), |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | private simulateSingleEvent(event: IEventArguments): Promise<void> { |
| 152 | /* TODO: Implement proper timing logic based on return Q.delay(event.at).then(() => { |
| 153 | using sinon fake timers to simulate time passing by */ |
| 154 | return new Promise(resolve => { |
| 155 | this.allSimulatedEvents.push(event); |
| 156 | const key = Object.keys(event).find(eventKey => eventKey !== "after"); // At the moment we are only using a single key/parameter per event |
| 157 | switch (key) { |
| 158 | case "stdout": { |
| 159 | const data = (<IStdOutEvent>event).stdout.data; |
| 160 | const previousOutputLength = this.allStdout.length; |
| 161 | this.allStdout += data; |
| 162 | this.simulateOutputSideEffects(data, previousOutputLength).then(() => { |
| 163 | this.process.stdout.emit("data", Buffer.from(data)); |
| 164 | }); |
| 165 | break; |
| 166 | } |
| 167 | case "stderr": { |
| 168 | const data = (<IStdErrEvent>event).stderr.data; |
| 169 | this.allStderr += data; |
| 170 | this.process.stderr.emit("data", Buffer.from(data)); |
| 171 | break; |
| 172 | } |
| 173 | case "error": |
| 174 | this.process.emit("error", (<IErrorEvent>event).error.error); |
| 175 | break; |
| 176 | case "exit": |
| 177 | const code = (<IExitEvent>event).exit.code; |
| 178 | |
| 179 | let beforeFinishing = Promise.resolve(); |
| 180 | if (code === 0) { |
| 181 | beforeFinishing = Promise.resolve( |
| 182 | this.sideEffectsDefinition.beforeSuccess( |
| 183 | this.allStdout, |
| 184 | this.allStderr, |
| 185 | ), |
| 186 | ); |
| 187 | } |
| 188 | |
| 189 | beforeFinishing.then(() => { |
| 190 | this.process.emit("exit", code); |
| 191 | }); |
| 192 | break; |
| 193 | case "custom": |
| 194 | return (<ICustomEvent>event).custom.lambda(); |
| 195 | default: |
| 196 | throw new Error(`Unknown event to simulate: ${key} from:\n\t${event}`); |
| 197 | } |
| 198 | return resolve(); |
| 199 | }); |
| 200 | } |
| 201 | } |
| 202 | |