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