microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
test/resources/processExecution/simulator.ts
183lines · modeblame
efebb488digeff10 years ago | 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 | | |
3c172a05Artem Egorov8 years ago | 8 | import {ISpawnResult, ChildProcess} from "../../../src/common/node/childProcess"; |
| 9 | import {PromiseUtil} from "../../../src/common/node/promise"; | |
efebb488digeff10 years ago | 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 | | |
6e7f90d8Daniel Lebu10 years ago | 77 | /* We call spawn to fill the ISpawnResult object appropiatedly. The command |
efebb488digeff10 years ago | 78 | and the arguments don't affect that object, so we just pass an empty command and parameters */ |
6e7f90d8Daniel Lebu10 years ago | 79 | return new ChildProcess({ childProcess: fakeChildProcessModule }).spawn("", []); |
efebb488digeff10 years ago | 80 | } |
| 81 | | |
27710197Vladimir Kotikov8 years ago | 82 | public simulate(simRecording: Recording): Q.Promise<void> { |
| 83 | assert(simRecording, "recording shouldn't be null"); | |
efebb488digeff10 years ago | 84 | return this.sideEffectsDefinition.beforeStart().then(() => { |
27710197Vladimir Kotikov8 years ago | 85 | return this.simulateAllEvents(simRecording.events); |
efebb488digeff10 years ago | 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; | |
27710197Vladimir Kotikov8 years ago | 95 | } |
efebb488digeff10 years ago | 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); | |
5c8365a6Artem Egorov8 years ago | 108 | if (match && match.index !== undefined) { |
efebb488digeff10 years ago | 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); | |
5c8365a6Artem Egorov8 years ago | 120 | if (match && match.index !== undefined) { |
efebb488digeff10 years ago | 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 | } |