pushdown 0.1.0.pre.20210714190141 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,504 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ require 'pushdown/spec_helpers'
7
+
8
+
9
+ RSpec.describe( Pushdown::SpecHelpers ) do
10
+
11
+ include Pushdown::SpecHelpers
12
+
13
+ #
14
+ # Expectation-failure Matchers (stolen from rspec-expectations)
15
+ # See the README for licensing information.
16
+ #
17
+
18
+ def fail
19
+ raise_error( RSpec::Expectations::ExpectationNotMetError )
20
+ end
21
+
22
+ def fail_with( message )
23
+ raise_error( RSpec::Expectations::ExpectationNotMetError, message )
24
+ end
25
+
26
+ def fail_matching( message )
27
+ if String === message
28
+ regexp = /#{Regexp.escape(message)}/
29
+ else
30
+ regexp = message
31
+ end
32
+ raise_error( RSpec::Expectations::ExpectationNotMetError, regexp )
33
+ end
34
+
35
+ def fail_including( *messages )
36
+ raise_error do |err|
37
+ expect( err ).to be_a( RSpec::Expectations::ExpectationNotMetError )
38
+ expect( err.message ).to include( *messages )
39
+ end
40
+ end
41
+
42
+ def dedent( string )
43
+ return string.gsub( /^\t+/, '' ).chomp
44
+ end
45
+
46
+
47
+ let( :state_class ) do
48
+ subclass = Class.new( Pushdown::State )
49
+ end
50
+
51
+ let( :seeking_state_class ) do
52
+ subclass = Class.new( Pushdown::State )
53
+ subclass.singleton_class.attr_accessor( :name )
54
+ subclass.name = 'Acme::State::Seeking'
55
+ return subclass
56
+ end
57
+
58
+
59
+
60
+ describe "transition matcher" do
61
+
62
+ it "passes if a Pushdown::Transition is returned" do
63
+ state_class.attr_accessor( :seeking_state_class )
64
+ state_class.define_method( :update ) do |*|
65
+ return Pushdown::Transition.create( :push, :change, self.seeking_state_class )
66
+ end
67
+
68
+ state = state_class.new
69
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
70
+
71
+ expect {
72
+ expect( state ).to transition
73
+ }.to_not raise_error
74
+ end
75
+
76
+
77
+ it "passes if a Symbol that maps to a declared transition is returned" do
78
+ state_class.transition_push( :change, :other )
79
+ state_class.define_method( :update ) do |*|
80
+ return :change
81
+ end
82
+
83
+ state = state_class.new
84
+
85
+ expect {
86
+ expect( state ).to transition
87
+ }.to_not raise_error
88
+ end
89
+
90
+
91
+ it "fails if a Symbol that does not map to a declared transition is returned" do
92
+ state_class.transition_push( :change, :other )
93
+ state_class.define_method( :update ) do |*|
94
+ return :something_else
95
+ end
96
+
97
+ state = state_class.new
98
+
99
+ expect {
100
+ expect( state ).to transition
101
+ }.to fail_matching( /unmapped symbol/i )
102
+ end
103
+
104
+
105
+ it "fails if something other than a Transition or Symbol is returned" do
106
+ state_class.transition_push( :change, :other )
107
+
108
+ state = state_class.new
109
+
110
+ expect {
111
+ expect( state ).to transition
112
+ }.to fail_matching( /expected to transition.*it did not/i )
113
+ end
114
+
115
+ end
116
+
117
+
118
+ describe "transition.via mutator" do
119
+
120
+ it "passes if the state returns a Symbol that maps to the specified kind of transition" do
121
+ state_class.transition_push( :seek, :seeking )
122
+ state_class.define_method( :update ) do |*|
123
+ return :seek
124
+ end
125
+
126
+ state = state_class.new
127
+
128
+ expect {
129
+ expect( state ).to transition.via( :push )
130
+ }.to_not raise_error
131
+ end
132
+
133
+
134
+ it "passes if the state returns a Pushdown::Transition of the correct type" do
135
+ state_class.attr_accessor( :seeking_state_class )
136
+ state_class.define_method( :update ) do |*|
137
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
138
+ end
139
+
140
+ state = state_class.new
141
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
142
+
143
+ expect {
144
+ expect( state ).to transition.via( :push )
145
+ }.to_not raise_error
146
+ end
147
+
148
+
149
+ it "fails with a detailed failure message if the state doesn't transition" do
150
+ state_class.transition_push( :seek, :seeking )
151
+
152
+ state = state_class.new
153
+
154
+ expect {
155
+ expect( state ).to transition.via( :push )
156
+ }.to fail_matching( /expected to transition via push.*returned: nil/i )
157
+ end
158
+
159
+
160
+ it "fails if the state returns a different kind of Pushdown::Transition" do
161
+ state_class.define_method( :update ) do |*|
162
+ return Pushdown::Transition.create( :pop, :restart )
163
+ end
164
+
165
+ state = state_class.new
166
+
167
+ expect {
168
+ expect( state ).to transition.via( :push )
169
+ }.to fail_matching( /transition via push.*pop/i )
170
+ end
171
+
172
+
173
+ it "fails if the state terutns a Symbol that maps to the wrong kind of transition" do
174
+ state_class.transition_pop( :seek )
175
+ state_class.define_method( :update ) do |*|
176
+ return :seek
177
+ end
178
+
179
+ state = state_class.new
180
+
181
+ expect {
182
+ expect( state ).to transition.via( :push )
183
+ }.to fail_matching( /transition via push.*it returned a pop/i )
184
+ end
185
+
186
+ end
187
+
188
+
189
+ describe "transition.to mutator" do
190
+
191
+ it "passes if the state returns a Symbol that maps to a transition to the specified state" do
192
+ state_class.transition_push( :seek, :seeking )
193
+ state_class.define_method( :update ) do |*|
194
+ return :seek
195
+ end
196
+
197
+ state = state_class.new
198
+
199
+ expect {
200
+ expect( state ).to transition.to( :seeking )
201
+ }.to_not raise_error
202
+ end
203
+
204
+
205
+ it "passes if the state returns a Pushdown::Transition with the correct target state" do
206
+ state_class.attr_accessor( :seeking_state_class )
207
+ state_class.define_method( :update ) do |*|
208
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
209
+ end
210
+
211
+ state = state_class.new
212
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
213
+
214
+ expect {
215
+ expect( state ).to transition.to( :seeking )
216
+ }.to_not raise_error
217
+ end
218
+
219
+
220
+ it "fails if the state returns a Symbol that maps to a transition to a different state" do
221
+ state_class.transition_push( :seek, :seeking )
222
+ state_class.define_method( :update ) do |*|
223
+ return :seek
224
+ end
225
+
226
+ state = state_class.new
227
+
228
+ expect {
229
+ expect( state ).to transition.to( :broadcasting )
230
+ }.to fail_matching( /broadcasting.*seeking/i )
231
+ end
232
+
233
+
234
+ it "fails with a detailed failure message if the state doesn't transition" do
235
+ state_class.transition_push( :seek, :seeking )
236
+
237
+ state = state_class.new
238
+
239
+ expect {
240
+ expect( state ).to transition.to( :seeking )
241
+ }.to fail_matching( /to seeking.*returned: nil/i )
242
+ end
243
+
244
+
245
+ it "fails if a Pushdown::Transition with a different target state is returned" do
246
+ state_class.attr_accessor( :seeking_state_class )
247
+ state_class.define_method( :update ) do |*|
248
+ return Pushdown::Transition.create( :push, :seek, self.seeking_state_class )
249
+ end
250
+
251
+ state = state_class.new
252
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
253
+
254
+ expect {
255
+ expect( state ).to transition.to( :other )
256
+ }.to fail_matching( /other.*seeking/i )
257
+ end
258
+
259
+ end
260
+
261
+
262
+ describe "composed matcher" do
263
+
264
+ it "passes if both .to and .via are specified and match" do
265
+ state_class.transition_push( :seek, :seeking )
266
+ state_class.define_method( :update ) do |*|
267
+ return :seek
268
+ end
269
+
270
+ state = state_class.new
271
+
272
+ expect {
273
+ expect( state ).to transition.via( :push ).to( :seeking )
274
+ }.to_not raise_error
275
+ end
276
+
277
+
278
+ it "fails if both .to and .via are specified and .to doesn't match" do
279
+ state_class.transition_push( :seek, :broadcasting )
280
+ state_class.define_method( :update ) do |*|
281
+ return :seek
282
+ end
283
+
284
+ state = state_class.new
285
+
286
+ expect {
287
+ expect( state ).to transition.via( :push ).to( :seeking )
288
+ }.to fail_matching( /seeking.*broadcasting/i )
289
+ end
290
+
291
+
292
+ it "fails if both .to and .via are specified and .via doesn't match" do
293
+ state_class.transition_push( :seek, :broadcasting )
294
+ state_class.define_method( :update ) do |*|
295
+ return :seek
296
+ end
297
+
298
+ state = state_class.new
299
+
300
+ expect {
301
+ expect( state ).to transition.via( :switch ).to( :broadcasting )
302
+ }.to fail_matching( /switch.*push/i )
303
+ end
304
+
305
+
306
+ it "fails if both .to and .via are specified and neither match" do
307
+ state_class.transition_push( :seek, :broadcasting )
308
+ state_class.define_method( :update ) do |*|
309
+ return :seek
310
+ end
311
+
312
+ state = state_class.new
313
+
314
+ expect {
315
+ expect( state ).to transition.via( :switch ).to( :seeking )
316
+ }.to fail_matching( /switch.*seeking.*push.*broadcasting/i )
317
+ end
318
+
319
+ end
320
+
321
+
322
+ describe "operation mutators" do
323
+
324
+ it "allows the #update operation to be explicitly specified" do
325
+ state_class.transition_push( :change, :other )
326
+ state_class.define_method( :update ) do |*|
327
+ return :change
328
+ end
329
+ state_class.define_method( :on_event ) do |*|
330
+ return nil
331
+ end
332
+
333
+ state = state_class.new
334
+
335
+ expect {
336
+ expect( state ).to transition.on_update
337
+ }.to_not raise_error
338
+ end
339
+
340
+
341
+ it "allows the #on_event operation to be explicitly specified" do
342
+ state_class.transition_push( :change, :other )
343
+ state_class.define_method( :on_event ) do |*|
344
+ return :change
345
+ end
346
+
347
+ state = state_class.new
348
+
349
+ expect {
350
+ expect( state ).to transition.on_an_event( :foo )
351
+ }.to_not raise_error
352
+ end
353
+
354
+
355
+ it "supports giving additional arguments to pass the #on_event operation" do
356
+ state_class.transition_push( :change, :other )
357
+
358
+ state = state_class.new
359
+ expect( state ).to receive( :on_event ).with( :foo, 1, "another arg" ).
360
+ and_return( :change )
361
+
362
+ expect {
363
+ expect( state ).to transition.on_an_event( :foo, 1, "another arg" )
364
+ }.to_not raise_error
365
+ end
366
+
367
+
368
+ it "adds the callback to the description if on_update is specified" do
369
+ state_class.transition_push( :change, :other )
370
+
371
+ state = state_class.new
372
+
373
+ expect {
374
+ expect( state ).to transition.on_update
375
+ }.to fail_matching( /transition when #update is called/i )
376
+ end
377
+
378
+
379
+ it "adds the callback to the description if on_an_event is specified" do
380
+ state_class.transition_push( :change, :other )
381
+
382
+ state = state_class.new
383
+
384
+ expect {
385
+ expect( state ).to transition.on_an_event( :foo )
386
+ }.to fail_matching( /transition when #on_event is called with :foo/i )
387
+ end
388
+
389
+
390
+ it "handles multiple callback arguments on failures" do
391
+ state_class.transition_push( :change, :other )
392
+
393
+ state = state_class.new
394
+ allow( state ).to receive( :on_event ).with( :foo, 18, "nebraska" ).
395
+ and_return( nil )
396
+
397
+ expect {
398
+ expect( state ).to transition.on_an_event( :foo, 18, "nebraska" )
399
+ }.to fail_matching( /#on_event is called with :foo, 18, "nebraska"/i )
400
+ end
401
+
402
+ end
403
+
404
+ describe "negated matcher" do
405
+
406
+ it "succeeds if a Symbol that does not map to a declared transition is returned" do
407
+ state_class.transition_push( :change, :other )
408
+ state_class.define_method( :update ) do |*|
409
+ return :something_else
410
+ end
411
+
412
+ state = state_class.new
413
+
414
+ expect {
415
+ expect( state ).not_to transition
416
+ }.to_not raise_error
417
+ end
418
+
419
+
420
+ it "succeeds if something other than a Transition or Symbol is returned" do
421
+ state_class.transition_push( :change, :other )
422
+
423
+ state = state_class.new
424
+
425
+ expect {
426
+ expect( state ).not_to transition
427
+ }.to_not raise_error
428
+ end
429
+
430
+
431
+ it "succeeds if a specific transition is given, but a different one is returned" do
432
+ state_class.transition_push( :change, :other )
433
+ state_class.define_method( :update ) do |*|
434
+ return :change
435
+ end
436
+
437
+ state = state_class.new
438
+
439
+ expect {
440
+ expect( state ).not_to transition.via( :switch )
441
+ }.to_not raise_error
442
+ end
443
+
444
+
445
+ it "succeeds if a target state is given, but a different one is returned" do
446
+ state_class.transition_push( :change, :broadcasting )
447
+ state_class.define_method( :update ) do |*|
448
+ return :change
449
+ end
450
+
451
+ state = state_class.new
452
+
453
+ expect {
454
+ expect( state ).not_to transition.via( :push ).to( :seeking )
455
+ }.to_not raise_error
456
+ end
457
+
458
+
459
+ it "fails if a Pushdown::Transition is returned" do
460
+ state_class.attr_accessor( :seeking_state_class )
461
+ state_class.define_method( :update ) do |*|
462
+ return Pushdown::Transition.create( :push, :change, self.seeking_state_class )
463
+ end
464
+
465
+ state = state_class.new
466
+ state.seeking_state_class = seeking_state_class # inject the "seeking" state class
467
+
468
+ expect {
469
+ expect( state ).not_to transition
470
+ }.to fail_matching( /not to transition, but it did/i )
471
+ end
472
+
473
+
474
+ it "fails if a Symbol that maps to a declared transition is returned" do
475
+ state_class.transition_push( :change, :other )
476
+ state_class.define_method( :update ) do |*|
477
+ return :change
478
+ end
479
+
480
+ state = state_class.new
481
+
482
+ expect {
483
+ expect( state ).not_to transition
484
+ }.to fail_matching( /not to transition, but it did/i )
485
+ end
486
+
487
+
488
+ it "fails if a type and state are specified and they describe the returned transition" do
489
+ state_class.transition_push( :seek, :seeking )
490
+ state_class.define_method( :update ) do |*|
491
+ return :seek
492
+ end
493
+
494
+ state = state_class.new
495
+
496
+ expect {
497
+ expect( state ).not_to transition.via( :push ).to( :seeking )
498
+ }.to fail_matching( /not.*via push to seeking, but it did/i )
499
+ end
500
+
501
+ end
502
+
503
+
504
+ end
@@ -0,0 +1,154 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ # Let autoloads decide the order
7
+ require 'pushdown'
8
+
9
+
10
+ RSpec.describe( Pushdown::State ) do
11
+
12
+ let( :state_data ) { {} }
13
+ let( :subclass ) do
14
+ Class.new( described_class )
15
+ end
16
+
17
+ let( :starting_state_class ) do
18
+ Class.new( subclass )
19
+ end
20
+
21
+ let( :automaton_class ) do
22
+ extended_class = Class.new
23
+ extended_class.extend( Pushdown::Automaton )
24
+ extended_class.const_set( :Starting, starting_state_class )
25
+ extended_class.pushdown_state( :state, initial_state: :starting )
26
+
27
+ return extended_class
28
+ end
29
+
30
+
31
+ it "is an abstract class" do
32
+ expect { described_class.new }.to raise_error( NoMethodError, /\bnew\b/ )
33
+ end
34
+
35
+
36
+ it "knows what the name of its type is" do
37
+ starting_state_class.singleton_class.attr_accessor :name
38
+ starting_state_class.name = 'Acme::State::Starting'
39
+
40
+ state = starting_state_class.new
41
+
42
+ expect( state.type_name ).to eq( :starting )
43
+ end
44
+
45
+
46
+ it "handles anonymous classes for #type_name" do
47
+ transition = starting_state_class.new
48
+
49
+ expect( transition.type_name ).to eq( :anonymous )
50
+ end
51
+
52
+
53
+
54
+ describe "transition callbacks" do
55
+
56
+ it "has a default (no-op) callback for when it is added to the stack" do
57
+ instance = subclass.new
58
+ expect( instance.on_start ).to be_nil
59
+ end
60
+
61
+
62
+ it "has a default (no-op) callback for when it is removed from the stack" do
63
+ instance = subclass.new
64
+ expect( instance.on_stop ).to be_nil
65
+ end
66
+
67
+
68
+ it "has a default (no-op) callback for when it is pushed down on the stack" do
69
+ instance = subclass.new
70
+ expect( instance.on_pause ).to be_nil
71
+ end
72
+
73
+
74
+ it "has a default (no-op) callback for when the stack is popped and it becomes current again" do
75
+ instance = subclass.new
76
+ expect( instance.on_resume ).to be_nil
77
+ end
78
+
79
+ end
80
+
81
+
82
+ describe "event handlers" do
83
+
84
+ it "has a default (no-op) event callback" do
85
+ instance = subclass.new
86
+ expect( instance.on_event(:an_event) ).to be_nil
87
+ end
88
+
89
+ end
90
+
91
+
92
+ describe "interval event handlers" do
93
+
94
+ it "has a default (no-op) interval callback for when it is current" do
95
+ instance = subclass.new
96
+ expect( instance.update ).to be_nil
97
+ end
98
+
99
+
100
+ it "has a default (no-op) interval callback for when it is on the stack" do
101
+ instance = subclass.new
102
+ expect( instance.shadow_update ).to be_nil
103
+ end
104
+
105
+ end
106
+
107
+
108
+
109
+ describe "transition declaration" do
110
+
111
+ it "can declare a push transition" do
112
+ subclass.transition_push( :start, :starting )
113
+ expect( subclass.transitions[:start] ).to eq([ :push, :starting ])
114
+ end
115
+
116
+
117
+ it "can declare a pop transition" do
118
+ subclass.transition_pop( :undo )
119
+ expect( subclass.transitions[:undo] ).to eq([ :pop ])
120
+ end
121
+
122
+ end
123
+
124
+
125
+ describe "transition creation" do
126
+
127
+ it "can create a transition it has declared" do
128
+ subclass.transition_push( :start, :starting )
129
+ instance = subclass.new
130
+
131
+ automaton = automaton_class.new
132
+
133
+ result = instance.transition( :start, automaton, :state )
134
+ expect( result ).to be_a( Pushdown::Transition::Push )
135
+ expect( result.name ).to eq( :start )
136
+ expect( result.data ).to be_nil
137
+ end
138
+
139
+
140
+ it "can create a transition it has declared that doesn't take a state class" do
141
+ subclass.transition_pop( :quit )
142
+ instance = subclass.new
143
+
144
+ automaton = automaton_class.new
145
+
146
+ result = instance.transition( :quit, automaton, :state )
147
+ expect( result ).to be_a( Pushdown::Transition::Pop )
148
+ expect( result.name ).to eq( :quit )
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+
@@ -0,0 +1,65 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../spec_helper'
5
+
6
+ require 'pushdown/transition/pop'
7
+ require 'pushdown/state'
8
+
9
+
10
+ RSpec.describe( Pushdown::Transition::Pop ) do
11
+
12
+ let( :state_class ) do
13
+ Class.new( Pushdown::State )
14
+ end
15
+
16
+ let( :state_a ) { state_class.new }
17
+ let( :state_b ) { state_class.new }
18
+ let( :state_c ) { state_class.new }
19
+
20
+ let( :stack ) do
21
+ return [ state_a, state_b, state_c ]
22
+ end
23
+
24
+
25
+ it "pops the last state from the stack when applied" do
26
+ stack = [ state_a, state_b, state_c ]
27
+ transition = described_class.new( :pop_test )
28
+
29
+ new_stack = transition.apply( stack )
30
+
31
+ expect( new_stack ).to eq([ state_a, state_b ])
32
+ end
33
+
34
+
35
+ it "passes state data through the transition callbacks" do
36
+ transition = described_class.new( :pop_test )
37
+
38
+ expect( state_c ).to receive( :on_stop ).with( no_args ).once.ordered
39
+ expect( state_b ).to receive( :on_resume ).with( no_args ).once.ordered
40
+
41
+ transition.apply( stack )
42
+ end
43
+
44
+
45
+ it "errors if applied to a stack that has only one state" do
46
+ stack = [ state_a ]
47
+ transition = described_class.new( :pop_test )
48
+
49
+ expect {
50
+ transition.apply( stack )
51
+ }.to raise_error( Pushdown::TransitionError, /can't pop/i )
52
+ end
53
+
54
+
55
+ it "errors if applied to an empty stack" do
56
+ stack = []
57
+ transition = described_class.new( :pop_test )
58
+
59
+ expect {
60
+ transition.apply( stack )
61
+ }.to raise_error( Pushdown::TransitionError, /can't pop/i )
62
+ end
63
+
64
+ end
65
+