Completed
Push — dev ( 249398...dac991 )
by Fike
39s
created

  D

Complexity

Conditions 9
Paths 1

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 1
dl 0
loc 28
rs 4.909
c 0
b 0
f 0
nop 0
1
/* eslint-env mocha */
2
/* eslint-disable no-unused-expressions */
3
4
var Sinon = require('sinon')
5
var Chai = require('chai')
6
var expect = Chai.expect
7
8
Chai.use(require('chai-as-promised'))
9
10
var SDK = require('@ama-team/voxengine-sdk')
11
var Future = SDK.Concurrent.Future
12
13
var StateMachine = require('../../../../lib/Execution').StateMachine
14
var Status = require('../../../../lib/Schema').OperationStatus
15
var Errors = require('../../../../lib/Error')
16
var Executor = require('../../../../lib/Execution/Executor').Executor
17
18
describe('Integration', function () {
19
  describe('/Execution', function () {
20
    describe('/StateMachine.js', function () {
21
      describe('.StateMachine', function () {
22
        var context
23
        var executor
24
25
        var entrypointState
26
        var terminalState
27
        var redirectingState
28
        var erroneousState
29
        var timingOutState
30
        var infiniteState
31
32
        var scenario
33
34
        var errorHandler
35
36
        var handlerFactory = function (handler, timeout, name) {
37
          return {
38
            name: name || 'stub',
39
            timeout: timeout || null,
40
            handler: Sinon.spy(handler || function () {})
41
          }
42
        }
43
44
        var stateFactory = function (id, handler, terminal, timeout) {
45
          var state = {
46
            id: id,
47
            entrypoint: false,
48
            terminal: !!terminal
49
          }
50
          var handlers = ['transition', 'onTransitionTimeout', 'abort', 'onAbortTimeout']
51
          handlers.forEach(function (name) {
52
            state[name] = handlerFactory(handler, timeout, name)
53
          })
54
          return state
55
        }
56
57
        var scenarioFactory = function () {
58
          entrypointState.entrypoint = true
59
          var states = [
60
            entrypointState,
61
            terminalState,
62
            redirectingState,
63
            erroneousState,
64
            timingOutState,
65
            infiniteState
66
          ]
67
          scenario = {}
68
          states.forEach(function (state) {
69
            scenario[state.id] = state
70
          })
71
          return {
72
            states: scenario,
73
            onError: errorHandler
74
          }
75
        }
76
77
        beforeEach(function () {
78
          context = {}
79
          executor = new Executor(context)
80
          terminalState = stateFactory('terminal', function () {}, true)
81
          entrypointState = stateFactory('entrypoint', function () {
82
            return {trigger: {id: 'terminal'}}
83
          })
84
          redirectingState = stateFactory('redirect', function (_, hints) {
85
            return {trigger: hints.trigger}
86
          })
87
          erroneousState = stateFactory('error', function () {
88
            throw new Error()
89
          })
90
          timingOutState = stateFactory('timeout', function () {
91
            return new Promise(function () {})
92
          }, false, 0)
93
          infiniteState = stateFactory('infinite', function () {
94
            return new Promise(function () {})
95
          })
96
          errorHandler = handlerFactory(function () {}, null, 'onError')
97
          scenario = scenarioFactory()
98
        })
99
100
        var factory = function (scenario) {
101
          return new StateMachine(executor, scenario)
102
        }
103
104
        var autoFactory = function () {
105
          return factory(scenarioFactory())
106
        }
107
108
        describe('< new', function () {
109
          it('fails if entrypoint state is not provided', function () {
110
            var lambda = function () {
111
              var scenario = {
112
                states: {infinite: infiniteState},
113
                onError: function () {}
114
              }
115
              return new StateMachine(executor, scenario)
116
            }
117
            expect(lambda).to.throw(Errors.ScenarioError)
118
          })
119
        })
120
121
        describe('#run()', function () {
122
          it('ends if terminal state is reached', function () {
123
            entrypointState.terminal = true
124
            var machine = autoFactory()
125
            return machine
126
              .run()
127
              .then(function () {
128
                var entrypoint = entrypointState.transition.handler
129
                expect(entrypoint.callCount).to.eq(1)
130
              })
131
          })
132
133
          it('triggers error handler in case error has been thrown', function () {
134
            var error = {error: 'imma mighty errro woooooo'}
135
            entrypointState.transition.handler = Sinon.spy(function () {
136
              throw error
137
            })
138
            var hints = {x: 12}
139
            var machine = autoFactory()
140
            return machine
141
              .run(hints)
142
              .then(function (result) {
143
                expect(result.status).to.eq(Status.Failed)
144
                expect(result.value).to.eq(error)
145
                expect(errorHandler.handler.callCount).to.eq(1)
146
                var args = errorHandler.handler.getCall(0).args
147
                expect(args[0]).to.eq(error)
148
                expect(args[1]).to.eq(null)
149
                expect(args[2]).to.eq(entrypointState.id)
150
                expect(args[3]).to.eq(hints)
151
              })
152
          })
153
154
          it('tolerates error handler failure', function () {
155
            var error = {x: 'fake error'}
156
            entrypointState.transition.handler = Sinon.spy(function () {
157
              throw error
158
            })
159
            errorHandler.handler = Sinon.spy(function () {
160
              throw new Error()
161
            })
162
            var machine = autoFactory()
163
            return machine
164
              .run()
165
              .then(function (result) {
166
                expect(result.status).to.eq(Status.Failed)
167
                expect(result.value).to.eq(error)
168
                expect(errorHandler.handler.callCount).to.eq(1)
169
              })
170
          })
171
172
          it('allows error handler to rescue the situation', function () {
173
            errorHandler.handler = Sinon.spy(function () {
174
              return {trigger: {id: 'terminal'}}
175
            })
176
            entrypointState.transition.handler = Sinon.spy(function () {
177
              throw new Error()
178
            })
179
            var machine = autoFactory()
180
            return machine
181
              .run()
182
              .then(function (result) {
183
                expect(result.status).to.eq(Status.Finished)
184
                expect(errorHandler.handler.callCount).to.eq(1)
185
              })
186
          })
187
188
          it('triggers next state if `.trigger.id` is present in return value', function () {
189
            var hints = {x: 12}
190
            entrypointState.transition.handler = function () {
191
              return {trigger: {id: 'terminal', hints: hints}}
192
            }
193
            terminalState.transition.handler = Sinon.spy(function () {})
194
            var machine = autoFactory()
195
            return machine
196
              .run()
197
              .then(function (result) {
198
                expect(result.status).to.eq(Status.Finished)
199
                var handler = terminalState.transition.handler
200
                expect(handler.callCount).to.eq(1)
201
                expect(handler.getCall(0).args[0]).to.eq(entrypointState.id)
202
                expect(handler.getCall(0).args[1]).to.deep.eq(hints)
203
              })
204
          })
205
206
          it('calls `.trigger.hints` in result if it is a function', function () {
207
            var hints = {x: 12}
208
            var wrapper = Sinon.spy(function () { return hints })
209
            entrypointState.transition.handler = function () {}
210
            entrypointState.triggers = {id: 'terminal', hints: wrapper}
211
            var machine = autoFactory()
212
            return machine
213
              .run()
214
              .then(function (result) {
215
                expect(result.status).to.eq(Status.Finished)
216
                var handler = terminalState.transition.handler
217
                expect(handler.callCount).to.eq(1)
218
                expect(handler.getCall(0).args[0]).to.eq(entrypointState.id)
219
                expect(handler.getCall(0).args[1]).to.eq(hints)
220
                expect(wrapper.callCount).to.eq(1)
221
              })
222
          })
223
224
          it('triggers next state if `.triggers` property is set', function () {
225
            var hints = {x: 12}
226
            entrypointState.transition.handler = function () {}
227
            entrypointState.triggers = {id: 'terminal', hints: hints}
228
            var machine = autoFactory()
229
            return machine
230
              .run()
231
              .then(function (result) {
232
                expect(result.status).to.eq(Status.Finished)
233
                var handler = terminalState.transition.handler
234
                expect(handler.callCount).to.eq(1)
235
                expect(handler.getCall(0).args[0]).to.eq(entrypointState.id)
236
                expect(handler.getCall(0).args[1]).to.eq(hints)
237
              })
238
          })
239
240
          it('calls `.triggers.hints` state property if it is a function', function () {
241
            var hints = {x: 12}
242
            var wrapper = Sinon.spy(function () { return hints })
243
            entrypointState.transition.handler = function () {}
244
            entrypointState.triggers = {id: 'terminal', hints: wrapper}
245
            var machine = autoFactory()
246
            return machine
247
              .run()
248
              .then(function (result) {
249
                expect(result.status).to.eq(Status.Finished)
250
                var handler = terminalState.transition.handler
251
                expect(handler.callCount).to.eq(1)
252
                expect(handler.getCall(0).args[0]).to.eq(entrypointState.id)
253
                expect(handler.getCall(0).args[1]).to.eq(hints)
254
                expect(wrapper.callCount).to.eq(1)
255
              })
256
          })
257
258
          it('reads `.transitionedTo` result property', function () {
259
            entrypointState.transition.handler = function () {
260
              return {transitionedTo: 'terminal'}
261
            }
262
            var machine = autoFactory()
263
            return machine
264
              .run()
265
              .then(function (result) {
266
                expect(result.status).to.eq(Status.Finished)
267
                expect(machine.getState()).to.eq(terminalState)
268
              })
269
          })
270
271
          it('terminates with error if `.transitionedTo` specifies nonexistent state', function () {
272
            entrypointState.transition.handler = function () {
273
              return {transitionedTo: 'nonexistent'}
274
            }
275
            var machine = autoFactory()
276
            return machine
277
              .run()
278
              .then(function (result) {
279
                expect(result.status).to.eq(Status.Failed)
280
                expect(result.value).to.be.instanceOf(Errors.ScenarioError)
281
              })
282
          })
283
284
          it('terminates with Tripped status if internal error has been ecnountered', function () {
285
            var error = new Errors.InternalError()
286
            entrypointState.transition.handler = function () {
287
              throw error
288
            }
289
            var machine = autoFactory()
290
            return machine
291
              .run()
292
              .then(function (result) {
293
                expect(result.status).to.eq(Status.Tripped)
294
                expect(result.value).to.eq(error)
295
              })
296
          })
297
        })
298
299
        describe('#transitionTo', function () {
300
          it('throws if missing state is specified', function () {
301
            var machine = autoFactory()
302
            var lambda = function () {
303
              machine.transitionTo('missing')
304
            }
305
            expect(lambda).to.throw(Errors.ScenarioError)
306
          })
307
308
          it('throws if machine has terminated', function () {
309
            entrypointState.terminal = true
310
            var machine = autoFactory()
311
            return machine
312
              .run()
313
              .then(function () {
314
                var lambda = function () {
315
                  machine.transitionTo('terminal')
316
                }
317
                expect(lambda).to.throw(Errors.ScenarioError)
318
              })
319
          })
320
321
          it('aborts running transition and ignores aborted transition errors', function () {
322
            var barrier = new Future()
323
            entrypointState = stateFactory('entrypoint', function () {
324
              return barrier.then(function () {
325
                throw new Error()
326
              })
327
            })
328
            var machine = autoFactory()
329
            var promise = machine.run()
330
            machine.transitionTo('terminal')
331
            barrier.resolve()
332
            return promise
333
              .then(function (result) {
334
                expect(result.status).to.eq(Status.Finished)
335
                expect(terminalState.transition.handler.callCount).to.eq(1)
336
                expect(entrypointState.abort.handler.callCount).to.eq(1)
337
              })
338
          })
339
340
          it('can be used to reanimate idle machine', function () {
341
            entrypointState.transition.handler = function () {}
342
            var machine = autoFactory()
343
            return machine
344
              .transitionTo('entrypoint')
345
              .then(function () {
346
                expect(machine.getStatus()).to.eq(StateMachine.Stage.Idle)
347
                machine.transitionTo('terminal')
348
                return machine.getTermination()
349
              })
350
              .then(function (result) {
351
                expect(result.status).to.eq(Status.Finished)
352
              })
353
          })
354
355
          it('tolerates string trigger', function () {
356
            entrypointState.transition.handler = Sinon.spy(function () {
357
              // not `trigger.id = ?` as expected
358
              return {trigger: 'terminal'}
359
            })
360
            var machine = autoFactory()
361
            return machine
362
              .run()
363
              .then(function (result) {
364
                expect(result.status).to.eq(Status.Finished)
365
                expect(entrypointState.transition.handler.callCount).to.eq(1)
366
                expect(terminalState.transition.handler.callCount).to.eq(1)
367
              })
368
          })
369
        })
370
371
        describe('#terminate', function () {
372
          it('aborts running transition', function () {
373
            entrypointState.transition.handler = function () {
374
              return new Promise(function () {})
375
            }
376
            entrypointState.abort.handler = Sinon.spy(function () {})
377
            var machine = autoFactory()
378
            machine.run()
379
            return machine
380
              .terminate()
381
              .then(function (result) {
382
                expect(result.status).to.eq(Status.Aborted)
383
                expect(entrypointState.abort.handler.callCount).to.eq(1)
384
              })
385
          })
386
387
          it('throws if called on inactive machine', function () {
388
            var machine = autoFactory()
389
            return machine
390
              .run()
391
              .then(function () {
392
                var lambda = function () {
393
                  machine.terminate()
394
                }
395
                expect(lambda).to.throw(Errors.InternalError)
396
              })
397
          })
398
        })
399
400
        describe('#getTransition()', function () {
401
          it('returns running transition', function () {
402
            entrypointState.transition.handler = function () {
403
              return new Promise(function () {})
404
            }
405
            var machine = autoFactory()
406
            var hints = {x: 12}
407
            expect(machine.getTransition()).to.be.null
0 ignored issues
show
introduced by
The result of the property access to expect(machine.getTransition()).to.be.null is not used.
Loading history...
408
            machine.run(hints)
409
            var transition = machine.getTransition()
410
            expect(transition.getOrigin()).to.eq(null)
411
            expect(transition.getTarget()).to.eq(entrypointState)
412
            expect(transition.getHints()).to.eq(hints)
413
          })
414
415
          it('returns null if transition has finished', function () {
416
            var machine = autoFactory()
417
            return machine
418
              .run()
419
              .then(function () {
420
                expect(machine.getTransition()).to.be.null
0 ignored issues
show
introduced by
The result of the property access to expect(machine.getTransition()).to.be.null is not used.
Loading history...
421
              })
422
          })
423
        })
424
425
        describe('#getTransitions()', function () {
426
          it('returns running and aborting transitions', function () {
427
            var barrier = new Future()
428
            var handler = function () {
429
              return barrier
430
            }
431
            entrypointState.transition.handler = handler
432
            entrypointState.abort.handler = handler
433
            terminalState.transition.handler = handler
434
            var machine = autoFactory()
435
            machine.run()
436
            machine.transitionTo('terminal')
437
            var transitions = machine.getTransitions()
438
            expect(transitions).to.have.lengthOf(2)
439
440
            expect(transitions[0].getOrigin()).to.be.null
0 ignored issues
show
introduced by
The result of the property access to expect(transitions.0.getOrigin()).to.be.null is not used.
Loading history...
441
            expect(transitions[0].getTarget()).to.eq(entrypointState)
442
443
            expect(transitions[1].getOrigin()).to.be.null
0 ignored issues
show
introduced by
The result of the property access to expect(transitions.1.getOrigin()).to.be.null is not used.
Loading history...
444
            expect(transitions[1].getTarget()).to.eq(terminalState)
445
          })
446
        })
447
448
        describe('#getHistory()', function () {
449
          it('stores up to 100 last history entries', function () {
450
            this.timeout(2000)
451
            var counter = 0
452
            entrypointState.transition.handler = function () {
453
              if (counter++ < 50) {
454
                return {trigger: {id: 'entrypoint'}}
455
              }
456
              return {trigger: {id: 'terminal'}}
457
            }
458
            var machine = autoFactory()
459
            return machine
460
              .run()
461
              .then(function () {
462
                expect(machine.getHistory()).to.have.lengthOf(100)
463
              })
464
          })
465
        })
466
      })
467
    })
468
  })
469
})
470