event_state 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +137 -0
- data/lib/event_state/machine.rb +467 -0
- data/lib/event_state/object_machine.rb +54 -0
- data/lib/event_state/state.rb +68 -0
- data/lib/event_state/version.rb +10 -0
- data/lib/event_state.rb +78 -0
- data/test/event_state/event_state_test.rb +359 -0
- metadata +87 -0
data/README.rdoc
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
= event_state
|
2
|
+
|
3
|
+
* http://github.com/jdleesmiller/event_state
|
4
|
+
|
5
|
+
== SYNOPSIS
|
6
|
+
|
7
|
+
A small embedded DSL for implementing stateful protocols in EventMachine using
|
8
|
+
finite state machines. The protocol is specified in terms of _states_ and
|
9
|
+
_messages_. The processing happens in the states, and the messages that can be
|
10
|
+
sent or received from each state are declared by name using the DSL.
|
11
|
+
|
12
|
+
Here's everyone's favorite example: an echo server. It starts in the
|
13
|
+
+:listening+ state, in which it can receive a +Noise+ message. It then
|
14
|
+
transitions to the +:speaking+ state. After a short delay (+EM.add_timer+), it
|
15
|
+
sends the noise back to the client, which causes it to transition back to the
|
16
|
+
listening state.
|
17
|
+
|
18
|
+
class MessageEchoServer < EventState::ObjectMachine
|
19
|
+
Noise = Struct.new(:content)
|
20
|
+
|
21
|
+
protocol do
|
22
|
+
state :listening do
|
23
|
+
on_recv Noise, :speaking
|
24
|
+
end
|
25
|
+
|
26
|
+
state :speaking do
|
27
|
+
on_send Noise, :listening
|
28
|
+
|
29
|
+
on_enter do |noise|
|
30
|
+
EM.add_timer 0.5 do
|
31
|
+
send_message Noise.new(noise.content)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
In a picture (generated from the code above using
|
39
|
+
{EventState::Machine.print_state_machine_dot}):
|
40
|
+
|
41
|
+
http://github.com/jdleesmiller/event_state/raw/master/assets/echo.png
|
42
|
+
|
43
|
+
Here the start state is indicated by a double circle, a blue arrow is a message
|
44
|
+
that can be received, and a red arrow is a message that can be sent.
|
45
|
+
|
46
|
+
The {EventState::ObjectMachine} base class extends {EventState::Machine}, which
|
47
|
+
in turn extends <tt>EventMachine::Connection</tt>. +ObjectMachine+ handles
|
48
|
+
serializing and deserializing the ruby objects using
|
49
|
+
<tt>EventMachine::ObjectProtocol</tt>, and (by default) it uses the class of the
|
50
|
+
message object as the message name. In this example, the message name is
|
51
|
+
+Noise+. +Machine+ provides the state machine DSL and the primitives for
|
52
|
+
handling arbitrary kinds of messages.
|
53
|
+
|
54
|
+
Here is the corresponding client and a demo showing how to run it:
|
55
|
+
|
56
|
+
class MessageEchoClient < EventState::ObjectMachine
|
57
|
+
Noise = MessageEchoServer::Noise
|
58
|
+
|
59
|
+
def initialize noises
|
60
|
+
super
|
61
|
+
@noises = noises
|
62
|
+
end
|
63
|
+
|
64
|
+
protocol do
|
65
|
+
state :speaking do
|
66
|
+
on_send Noise, :listening
|
67
|
+
|
68
|
+
on_enter do
|
69
|
+
if @noises.empty?
|
70
|
+
EM.stop
|
71
|
+
else
|
72
|
+
send_message MessageEchoServer::Noise.new(@noises.shift)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
state :listening do
|
78
|
+
on_recv Noise, :speaking
|
79
|
+
|
80
|
+
on_enter do |noise|
|
81
|
+
puts "heard: #{noise.content}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.demo
|
87
|
+
EM.run do
|
88
|
+
EM.start_server('localhost', 14159, MessageEchoServer)
|
89
|
+
EM.connect('localhost', 14159, MessageEchoClient, %w(foo bar baz))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Output:
|
95
|
+
heard: foo
|
96
|
+
heard: bar
|
97
|
+
heard: baz
|
98
|
+
|
99
|
+
== INSTALLATION
|
100
|
+
|
101
|
+
Not yet released; will eventually be installable with:
|
102
|
+
|
103
|
+
gem install event_state
|
104
|
+
|
105
|
+
== RELATED PROJECTS
|
106
|
+
|
107
|
+
This library was inspired by http://slagyr.github.com/statemachine which
|
108
|
+
provides a nice DSL for defining state machines but doesn't integrate directly
|
109
|
+
with EventMachine. See also the http://www.complang.org/ragel state machine
|
110
|
+
compiler and Zed Shaw's http://zedshaw.com/essays/ragel_state_charts.html blog
|
111
|
+
post about it.
|
112
|
+
|
113
|
+
== LICENSE
|
114
|
+
|
115
|
+
(The MIT License)
|
116
|
+
|
117
|
+
Copyright (c) 2011 John Lees-Miller
|
118
|
+
|
119
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
120
|
+
a copy of this software and associated documentation files (the
|
121
|
+
'Software'), to deal in the Software without restriction, including
|
122
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
123
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
124
|
+
permit persons to whom the Software is furnished to do so, subject to
|
125
|
+
the following conditions:
|
126
|
+
|
127
|
+
The above copyright notice and this permission notice shall be
|
128
|
+
included in all copies or substantial portions of the Software.
|
129
|
+
|
130
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
131
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
132
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
133
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
134
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
135
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
136
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
137
|
+
|
@@ -0,0 +1,467 @@
|
|
1
|
+
module EventState
|
2
|
+
#
|
3
|
+
# Base class for state machines driven by +EventMachine+.
|
4
|
+
#
|
5
|
+
# This class provides a domain-specific language (DSL) for declaring the
|
6
|
+
# structure of the machine. See the {file:README} for examples and the general
|
7
|
+
# idea of how this works.
|
8
|
+
#
|
9
|
+
# If you are sending ruby objects as messages, see {ObjectMachine}; it handles
|
10
|
+
# serialization (using EventMachine's +ObjectProtocol+) and names messages
|
11
|
+
# according to their classes (but you can override this).
|
12
|
+
#
|
13
|
+
# If you have some other kind of messages, then you should subclass this class
|
14
|
+
# directly. Two methods are required:
|
15
|
+
# 1. Override EventMachine's +receive_data+ method to call
|
16
|
+
# {#transition_on_recv} with the received message.
|
17
|
+
# 2. Override EventMachine's +send_data+ method to call {#transition_on_send}
|
18
|
+
# with the message to be sent.
|
19
|
+
# Note that {#transition_on_recv} and {#transition_on_send} take a message
|
20
|
+
# _name_ as well as a message. You have to define the mapping from messages
|
21
|
+
# to message names so that the message names correspond with the transitions
|
22
|
+
# declared using the DSL ({on_send} and {on_recv} in particular).
|
23
|
+
#
|
24
|
+
class Machine < EventMachine::Connection
|
25
|
+
class << self
|
26
|
+
#
|
27
|
+
# Declare the protocol; pass a block to declare the {state}s.
|
28
|
+
#
|
29
|
+
# When the block terminates, this method declares any 'implicit' states
|
30
|
+
# that have been referenced by {on_send} or {on_recv} but that have not
|
31
|
+
# been declared with {state}. It also does some basic sanity checking.
|
32
|
+
#
|
33
|
+
# There can be multiple protocol blocks declared for one class; it is
|
34
|
+
# equivalent to moving all of the definitions to the same block.
|
35
|
+
#
|
36
|
+
# @yield [] declare the {state}s in the protocol
|
37
|
+
#
|
38
|
+
# @return [nil]
|
39
|
+
#
|
40
|
+
def protocol
|
41
|
+
raise "cannot nest protocol blocks" if defined?(@protocol) && @protocol
|
42
|
+
|
43
|
+
@start_state = nil unless defined?(@start_state)
|
44
|
+
@states = {} unless defined?(@states)
|
45
|
+
|
46
|
+
@protocol = true
|
47
|
+
@state = nil
|
48
|
+
yield
|
49
|
+
@protocol = false
|
50
|
+
|
51
|
+
# add implicitly defined states to @states to avoid having to check for
|
52
|
+
# nil states while the machine is running
|
53
|
+
explicit_states = Set[*@states.keys]
|
54
|
+
all_states = Set[*@states.values.map {|state|
|
55
|
+
state.on_sends.values + state.on_recvs.values}.flatten]
|
56
|
+
implicit_states = all_states - explicit_states
|
57
|
+
implicit_states.each do |state_name|
|
58
|
+
@states[state_name] = State.new(state_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Exchange sends for receives and receives for sends in the +base+
|
64
|
+
# protocol, and clear all of the {on_enter} and {on_exit} handlers. It
|
65
|
+
# often happens that a server and a client follow protocols with the same
|
66
|
+
# states, but with sends and receives interchanged. This method is
|
67
|
+
# intended to help with this case. You can, for example, reverse a server
|
68
|
+
# and use the passed block to define new {on_enter} and {on_exit} handlers
|
69
|
+
# appropriate for the client.
|
70
|
+
#
|
71
|
+
# The start state is determined by the first state declared in the given
|
72
|
+
# block (not by the protocol being reversed).
|
73
|
+
#
|
74
|
+
# @param [Class] base
|
75
|
+
#
|
76
|
+
# @yield [] define {state}s new {on_enter} and {on_exit} handlers
|
77
|
+
#
|
78
|
+
# @return [nil]
|
79
|
+
#
|
80
|
+
def reverse_protocol base, &block
|
81
|
+
raise "cannot mirror if already have a protocol" if defined?(@protocol)
|
82
|
+
|
83
|
+
@states = Hash[base.states.map {|state_name, state|
|
84
|
+
new_state = EventState::State.new(state_name)
|
85
|
+
new_state.on_recvs = state.on_sends.dup
|
86
|
+
new_state.on_sends = state.on_recvs.dup
|
87
|
+
[state_name, new_state]
|
88
|
+
}]
|
89
|
+
|
90
|
+
protocol(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Declare a state; pass a block to configure the state using {on_enter},
|
95
|
+
# {on_send} and so on.
|
96
|
+
#
|
97
|
+
# By default, the machine's start state is the first state declared using
|
98
|
+
# this method.
|
99
|
+
#
|
100
|
+
# @yield [] configure the state
|
101
|
+
#
|
102
|
+
# @return [nil]
|
103
|
+
#
|
104
|
+
def state state_name
|
105
|
+
raise "must be called from within a protocol block" unless @protocol
|
106
|
+
raise "cannot nest calls to state" if @state
|
107
|
+
|
108
|
+
# create new state or edit exiting state
|
109
|
+
@state = @states[state_name] || State.new(state_name)
|
110
|
+
|
111
|
+
# configure this state
|
112
|
+
yield
|
113
|
+
|
114
|
+
# need to know the start state
|
115
|
+
@start_state = @state unless @start_state
|
116
|
+
|
117
|
+
# index by name for easy lookup
|
118
|
+
@states[@state.name] = @state
|
119
|
+
|
120
|
+
# ensure that on_enter etc. aren't callable outside of a state block
|
121
|
+
@state = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Register a block to be called after the machine enters the current
|
126
|
+
# {state}.
|
127
|
+
#
|
128
|
+
# The machine changes state in response to a message being sent or
|
129
|
+
# received, and you can register an {on_enter} handler that is specific
|
130
|
+
# to the message that caused the change. Or you can render a catch-all
|
131
|
+
# block that will be called if no more specific handler was found (see
|
132
|
+
# example below).
|
133
|
+
#
|
134
|
+
# If a catch-all +on_enter+ block is given for the {start_state}, it is
|
135
|
+
# called from EventMachine's +post_init+ method. In this case (and only
|
136
|
+
# this case), the message passed to the block is +nil+.
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# state :foo do
|
140
|
+
# on_enter :my_message do |message|
|
141
|
+
# # got here due to a :my_message message
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# on_enter do
|
145
|
+
# # got here some other way
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# @param [Array<Symbol>] message_names zero or more
|
150
|
+
#
|
151
|
+
# @yield [message]
|
152
|
+
#
|
153
|
+
# @yieldparam [Message, nil] message nil iff this is the start state and
|
154
|
+
# the machine has just started up (called from +post_init+)
|
155
|
+
#
|
156
|
+
# @return [nil]
|
157
|
+
#
|
158
|
+
def on_enter *message_names, &block
|
159
|
+
raise "on_enter must be called from a state block" unless @state
|
160
|
+
if message_names == []
|
161
|
+
raise "on_enter block already given" if @state.default_on_enter
|
162
|
+
@state.default_on_enter = block
|
163
|
+
else
|
164
|
+
save_state_handlers('on_enter', @state.on_enters, message_names,block)
|
165
|
+
end
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Register a block to be called after the machine exits the current
|
171
|
+
# {state}. See {on_enter} for more information.
|
172
|
+
#
|
173
|
+
# @param [Array<Symbol>] message_names zero or more
|
174
|
+
#
|
175
|
+
# @yield [message]
|
176
|
+
#
|
177
|
+
# @yieldparam [Message] message
|
178
|
+
#
|
179
|
+
# @return [nil]
|
180
|
+
#
|
181
|
+
def on_exit *message_names, &block
|
182
|
+
raise "on_exit must be called from a state block" unless @state
|
183
|
+
if message_names == []
|
184
|
+
raise "on_exit block already given" if @state.default_on_exit
|
185
|
+
@state.default_on_exit = block
|
186
|
+
else
|
187
|
+
save_state_handlers('on_exit', @state.on_exits, message_names, block)
|
188
|
+
end
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Declare which state the machine transitions to when one of the given
|
194
|
+
# messages is sent in this {state}.
|
195
|
+
#
|
196
|
+
# @example
|
197
|
+
# state :foo do
|
198
|
+
# on_enter do
|
199
|
+
# EM.defer proc { sleep 3 },
|
200
|
+
# proc { send_message DoneMessage.new(42) }
|
201
|
+
# end
|
202
|
+
# on_send DoneMessage, :bar
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# @param [Array<Symbol>] message_names one or more
|
206
|
+
#
|
207
|
+
# @param [Symbol] next_state_name
|
208
|
+
#
|
209
|
+
# @return [nil]
|
210
|
+
#
|
211
|
+
def on_send *message_names, next_state_name
|
212
|
+
raise "on_send must be called from a state block" unless @state
|
213
|
+
message_names.flatten.each do |name|
|
214
|
+
@state.on_sends[name] = next_state_name
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
# Called when EventMachine calls +unbind+ on the connection while it is in
|
220
|
+
# the current state. This may indicate that the connection has been closed
|
221
|
+
# or timed out. The default is to take no action.
|
222
|
+
#
|
223
|
+
# @yield [] called upon +unbind+
|
224
|
+
#
|
225
|
+
# @return [nil]
|
226
|
+
#
|
227
|
+
def on_unbind &block
|
228
|
+
raise "on_unbind must be called from a state block" unless @state
|
229
|
+
@state.on_unbind = block
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# Declare which state the machine transitions to when one of the given
|
235
|
+
# messages is received in this {state}. See {on_send} for more
|
236
|
+
# information.
|
237
|
+
#
|
238
|
+
# @param [Array<Symbol>] message_names one or more
|
239
|
+
#
|
240
|
+
# @param [Symbol] next_state_name
|
241
|
+
#
|
242
|
+
# @return [nil]
|
243
|
+
#
|
244
|
+
def on_recv *message_names, next_state_name
|
245
|
+
raise "on_recv must be called from a state block" unless @state
|
246
|
+
message_names.flatten.each do |name|
|
247
|
+
@state.on_recvs[name] = next_state_name
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# @return [Hash<Symbol, State>] map from state names (ruby symbols) to
|
253
|
+
# {State}s
|
254
|
+
#
|
255
|
+
attr_reader :states
|
256
|
+
|
257
|
+
#
|
258
|
+
# @return [State] the machine enters this state when a new connection is
|
259
|
+
# established
|
260
|
+
#
|
261
|
+
attr_reader :start_state
|
262
|
+
|
263
|
+
#
|
264
|
+
# The complete list of transitions declared in the state machine (an edge
|
265
|
+
# list).
|
266
|
+
#
|
267
|
+
# @return [Array] each entry is of the form <tt>[state_name, [:send |
|
268
|
+
# :recv, message_name], next_state_name]</tt>
|
269
|
+
#
|
270
|
+
def transitions
|
271
|
+
states.values.map{|state|
|
272
|
+
[[state.on_sends, :send], [state.on_recvs, :recv]].map {|pairs, kind|
|
273
|
+
pairs.map{|message, next_state_name|
|
274
|
+
[state.name, [kind, message], next_state_name]}}}.flatten(2)
|
275
|
+
end
|
276
|
+
|
277
|
+
#
|
278
|
+
# Print the state machine in dot (graphviz) format.
|
279
|
+
#
|
280
|
+
# By default, the 'send' edges are red and 'receive' edges are blue, and
|
281
|
+
# the start state is indicated by a double border.
|
282
|
+
#
|
283
|
+
# @param [Hash] opts extra options
|
284
|
+
#
|
285
|
+
# @option opts [IO] :io (StringIO.new) to print to
|
286
|
+
#
|
287
|
+
# @option opts [String] :graph_options ('') dot graph options
|
288
|
+
#
|
289
|
+
# @option opts [Proc] :message_name_transform transform message names
|
290
|
+
#
|
291
|
+
# @option opts [Proc] :state_name_form transform state names
|
292
|
+
#
|
293
|
+
# @option opts [String] :recv_edge_style ('color=blue')
|
294
|
+
#
|
295
|
+
# @option opts [String] :send_edge_style ('color=red')
|
296
|
+
#
|
297
|
+
# @return [IO] the +:io+ option
|
298
|
+
#
|
299
|
+
def print_state_machine_dot opts={}
|
300
|
+
io = opts[:io] || StringIO.new
|
301
|
+
graph_options = opts[:graph_options] || ''
|
302
|
+
message_name_transform = opts[:message_name_transform] || proc {|x| x}
|
303
|
+
state_name_transform = opts[:state_name_transform] || proc {|x| x}
|
304
|
+
recv_edge_style = opts[:recv_edge_style] || 'color=blue'
|
305
|
+
send_edge_style = opts[:send_edge_style] || 'color=red'
|
306
|
+
|
307
|
+
io.puts "digraph #{self.name.inspect} {\n #{graph_options}"
|
308
|
+
|
309
|
+
io.puts " #{start_state.name} [peripheries=2];" # double border
|
310
|
+
|
311
|
+
transitions.each do |state_name, (kind, message_name), next_state_name|
|
312
|
+
s0 = state_name_transform.call(state_name)
|
313
|
+
s1 = state_name_transform.call(next_state_name)
|
314
|
+
label = message_name_transform.call(message_name)
|
315
|
+
|
316
|
+
style = case kind
|
317
|
+
when :recv then
|
318
|
+
"#{recv_edge_style},label=\"#{label}\""
|
319
|
+
when :send then
|
320
|
+
"#{send_edge_style},label=\"#{label}\""
|
321
|
+
else
|
322
|
+
raise "unknown kind #{kind}"
|
323
|
+
end
|
324
|
+
io.puts " #{s0} -> #{s1} [#{style}];"
|
325
|
+
end
|
326
|
+
io.puts "}"
|
327
|
+
|
328
|
+
io
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def save_state_handlers handler_type, handlers, message_names, block
|
334
|
+
message_names.flatten.each do |name|
|
335
|
+
raise "#{handler_type} already defined for #{name}" if handlers[name]
|
336
|
+
handlers[name] = block
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
#
|
342
|
+
# Called by +EventMachine+ when a new connection has been established. This
|
343
|
+
# calls the +on_enter+ handler for the machine's start state with a +nil+
|
344
|
+
# message.
|
345
|
+
#
|
346
|
+
# Be sure to call +super+ if you override this method, or the +on_enter+
|
347
|
+
# handler for the start state will not be called.
|
348
|
+
#
|
349
|
+
# @return [nil]
|
350
|
+
#
|
351
|
+
def post_init
|
352
|
+
@state = self.class.start_state
|
353
|
+
@state.call_on_enter self, nil, nil
|
354
|
+
nil
|
355
|
+
end
|
356
|
+
|
357
|
+
#
|
358
|
+
# Called by +EventMachine+ when a connection is closed. This calls the
|
359
|
+
# {on_unbind} handler for the current state.
|
360
|
+
#
|
361
|
+
# @return [nil]
|
362
|
+
#
|
363
|
+
def unbind
|
364
|
+
#puts "#{self.class} UNBIND"
|
365
|
+
handler = @state.on_unbind
|
366
|
+
self.instance_exec(&handler) if handler
|
367
|
+
nil
|
368
|
+
end
|
369
|
+
|
370
|
+
#
|
371
|
+
# Move the state machine from its current state to the successor state that
|
372
|
+
# it should be in after receiving the given +message+, according to the
|
373
|
+
# protocol defined using the DSL.
|
374
|
+
#
|
375
|
+
# If the received message is not valid according to the protocol, then the
|
376
|
+
# protocol error handler is called (see {ProtocolError}).
|
377
|
+
#
|
378
|
+
# The precise order of events is:
|
379
|
+
# 1. the +on_exit+ handler of the current state is called with +message+
|
380
|
+
# 2. the current state is set to the successor state
|
381
|
+
# 3. the +on_enter+ handler of the new current state is called with
|
382
|
+
# +message+
|
383
|
+
#
|
384
|
+
# @param [Object] message_name the name for +message+; this is what relates
|
385
|
+
# the message data to the transitions defined with {on_send}; must be
|
386
|
+
# hashable and comparable by value; for example, a symbol, string,
|
387
|
+
# number or class makes a good message name
|
388
|
+
#
|
389
|
+
# @param [Object] message received
|
390
|
+
#
|
391
|
+
# @return [nil]
|
392
|
+
#
|
393
|
+
def transition_on_recv message_name, message
|
394
|
+
#puts "#{self.class}: RECV #{message_name}"
|
395
|
+
# look up successor state
|
396
|
+
next_state_name = @state.on_recvs[message_name]
|
397
|
+
|
398
|
+
# if there is no registered successor state, it's a protocol error
|
399
|
+
if next_state_name.nil?
|
400
|
+
raise RecvProtocolError.new(self, @state.name, message_name, message)
|
401
|
+
else
|
402
|
+
transition message_name, message, next_state_name
|
403
|
+
end
|
404
|
+
|
405
|
+
nil
|
406
|
+
end
|
407
|
+
|
408
|
+
#
|
409
|
+
# Move the state machine from its current state to the successor state that
|
410
|
+
# it should be in after sending the given +message+, according to the
|
411
|
+
# protocol defined using the DSL.
|
412
|
+
#
|
413
|
+
# If the message to be sent is not valid according to the protocol, then the
|
414
|
+
# protocol error handler is called (see {ProtocolError}). If
|
415
|
+
# the message is valid, then the precise order of events is:
|
416
|
+
# 1. this method yields the message to the supplied block (if any); the
|
417
|
+
# intention is that the block is used to actually send the message
|
418
|
+
# 2. the +on_exit+ handler of the current state is called with +message+
|
419
|
+
# 3. the current state is set to the successor state
|
420
|
+
# 4. the +on_enter+ handler of the new current state is called with
|
421
|
+
# +message+
|
422
|
+
#
|
423
|
+
# @param [Object] message received
|
424
|
+
#
|
425
|
+
# @param [Object] message_name the name for +message+; this is what relates
|
426
|
+
# the message data to the transitions defined with {on_send}; must be
|
427
|
+
# hashable and comparable by value; for example, a symbol, string,
|
428
|
+
# number or class makes a good message name
|
429
|
+
#
|
430
|
+
# @yield [message] should actually send the message, typically using
|
431
|
+
# EventMachine's +send_data+ method
|
432
|
+
#
|
433
|
+
# @yieldparam [Object] message the same message passed to this method
|
434
|
+
#
|
435
|
+
# @return [nil]
|
436
|
+
#
|
437
|
+
def transition_on_send message_name, message
|
438
|
+
#puts "#{self.class}: SEND #{message_name}"
|
439
|
+
# look up successor state
|
440
|
+
next_state_name = @state.on_sends[message_name]
|
441
|
+
|
442
|
+
# if there is no registered successor state, it's a protocol error
|
443
|
+
if next_state_name.nil?
|
444
|
+
raise SendProtocolError.new(self, @state.name, message_name, message)
|
445
|
+
else
|
446
|
+
# let the caller send the message before we transition
|
447
|
+
yield message if block_given?
|
448
|
+
|
449
|
+
transition message_name, message, next_state_name
|
450
|
+
end
|
451
|
+
|
452
|
+
nil
|
453
|
+
end
|
454
|
+
|
455
|
+
private
|
456
|
+
|
457
|
+
#
|
458
|
+
# Update @state and call appropriate on_exit and on_enter handlers.
|
459
|
+
#
|
460
|
+
def transition message_name, message, next_state_name
|
461
|
+
@state.call_on_exit self, message_name, message
|
462
|
+
@state = self.class.states[next_state_name]
|
463
|
+
@state.call_on_enter self, message_name, message
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module EventState
|
2
|
+
#
|
3
|
+
# Base class for a machine in which the messages are ruby objects. See the
|
4
|
+
# {file:README} for examples.
|
5
|
+
#
|
6
|
+
class ObjectMachine < Machine
|
7
|
+
include EventMachine::P::ObjectProtocol
|
8
|
+
|
9
|
+
#
|
10
|
+
# Return the class of the message as the message name. You can override this
|
11
|
+
# method to provide your own mapping from messages to names.
|
12
|
+
#
|
13
|
+
# @param [Object] message
|
14
|
+
#
|
15
|
+
# @return [Object] must be hashable and comparable by value; for example, a
|
16
|
+
# symbol, string, number or class makes a good message name
|
17
|
+
#
|
18
|
+
def message_name message
|
19
|
+
message.class
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Called by +EventMachine+ (actually, the +ObjectProtocol+) when a message
|
24
|
+
# is received; makes the appropriate transition.
|
25
|
+
#
|
26
|
+
# Note: if you want to receive non-messages as well, you should override
|
27
|
+
# this method in your subclass, and call +super+ only when a message is
|
28
|
+
# received.
|
29
|
+
#
|
30
|
+
# @param [Object] message received
|
31
|
+
#
|
32
|
+
# @return [nil]
|
33
|
+
#
|
34
|
+
def receive_object message
|
35
|
+
transition_on_recv message_name(message), message
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Call <tt>ObjectProtocol</tt>'s +send_object+ on the message and make the
|
40
|
+
# transition.
|
41
|
+
#
|
42
|
+
# @param [Object] message to be sent
|
43
|
+
#
|
44
|
+
# @return [nil]
|
45
|
+
#
|
46
|
+
def send_message message
|
47
|
+
raise "not on the reactor thread" unless EM.reactor_thread?
|
48
|
+
transition_on_send message_name(message), message do |msg|
|
49
|
+
send_object msg
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module EventState
|
2
|
+
#
|
3
|
+
# A state in a state machine. This class is for internal use; you will not
|
4
|
+
# usually need to use it directly.
|
5
|
+
#
|
6
|
+
# Note that the <tt>Proc</tt>s stored here are executed in the context of an
|
7
|
+
# instance of a subclass of {Machine}, rather than in the context in which
|
8
|
+
# they were defined.
|
9
|
+
#
|
10
|
+
# @attr [Symbol] name state name
|
11
|
+
#
|
12
|
+
# @attr [Hash<Symbol, Proc>] on_enters map from message names to handlers
|
13
|
+
#
|
14
|
+
# @attr [Proc, nil] default_on_enter called when the state is entered via a
|
15
|
+
# message that does not have an associated handler in +on_enters+
|
16
|
+
#
|
17
|
+
# @attr [Hash<Symbol, Proc>] on_exits map from message names to handlers
|
18
|
+
#
|
19
|
+
# @attr [Proc, nil] default_on_exit called when the state is exited via a
|
20
|
+
# message that does not have an associated handler in +on_exits+
|
21
|
+
#
|
22
|
+
# @attr [Hash<Symbol, Symbol>] on_sends map from message names to successor
|
23
|
+
# state names
|
24
|
+
#
|
25
|
+
# @attr [Hash<Symbol, Symbol>] on_recvs map from message names to successor
|
26
|
+
# state names
|
27
|
+
#
|
28
|
+
State = Struct.new(:name,
|
29
|
+
:on_enters, :default_on_enter,
|
30
|
+
:on_exits, :default_on_exit,
|
31
|
+
:on_unbind, :on_sends, :on_recvs) do
|
32
|
+
def initialize(*)
|
33
|
+
super
|
34
|
+
self.on_enters ||= {}
|
35
|
+
self.on_exits ||= {}
|
36
|
+
self.on_sends ||= {}
|
37
|
+
self.on_recvs ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# @private
|
42
|
+
#
|
43
|
+
def call_on_enter context, message_name, message
|
44
|
+
call_state_handler context, on_enters, default_on_enter,
|
45
|
+
message_name, message
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# @private
|
50
|
+
#
|
51
|
+
def call_on_exit context, message_name, message
|
52
|
+
call_state_handler context, on_exits, default_on_exit,
|
53
|
+
message_name, message
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def call_state_handler context, handlers, default_handler,
|
59
|
+
message_name, message
|
60
|
+
|
61
|
+
# use message-specific handler if it exists; otherwise use the default
|
62
|
+
handler = handlers[message_name] || default_handler
|
63
|
+
|
64
|
+
# evaluate the block in the right context, namely the machine instance
|
65
|
+
context.instance_exec(message, &handler) if handler
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/event_state.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'event_state/version'
|
4
|
+
require 'event_state/state'
|
5
|
+
require 'event_state/machine'
|
6
|
+
require 'event_state/object_machine'
|
7
|
+
|
8
|
+
#
|
9
|
+
# See the {file:README} for details.
|
10
|
+
#
|
11
|
+
module EventState
|
12
|
+
|
13
|
+
#
|
14
|
+
# Base class for {SendProtocolError} and {RecvProtocolError}.
|
15
|
+
#
|
16
|
+
# You can catch these errors by registering a block with
|
17
|
+
# <tt>EventMachine.error_handler</tt>.
|
18
|
+
#
|
19
|
+
class ProtocolError < RuntimeError
|
20
|
+
def initialize machine, state_name, action, message_name, data
|
21
|
+
@machine, @state_name, @action, @message_name, @data =
|
22
|
+
machine, state_name, action, message_name, data
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# @return [Machine] the machine that raised the error
|
27
|
+
#
|
28
|
+
attr_reader :machine
|
29
|
+
|
30
|
+
#
|
31
|
+
# @return [Symbol] the name of the state in which the error occurred
|
32
|
+
#
|
33
|
+
attr_reader :state_name
|
34
|
+
|
35
|
+
#
|
36
|
+
# @return [:recv, :send] whether the error occurred on a send or a receive
|
37
|
+
#
|
38
|
+
attr_reader :action
|
39
|
+
|
40
|
+
#
|
41
|
+
# @return [Object] the name of the message sent or received
|
42
|
+
#
|
43
|
+
attr_reader :message_name
|
44
|
+
|
45
|
+
#
|
46
|
+
# @return [Object] the message / data sent or received
|
47
|
+
#
|
48
|
+
attr_reader :data
|
49
|
+
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
#
|
53
|
+
def inspect
|
54
|
+
"#<#{self.class}: for #{machine.class} in"\
|
55
|
+
" #{state_name.inspect} state: #{action} #{message_name}>"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Error raised by {Machine} when a message that is invalid according to the
|
61
|
+
# machine's protocol is sent.
|
62
|
+
#
|
63
|
+
class SendProtocolError < ProtocolError
|
64
|
+
def initialize machine, state_name, message_name, data
|
65
|
+
super machine, state_name, :send, message_name, data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Error raised by {Machine} when a message that is invalid according to the
|
71
|
+
# machine's protocol is received.
|
72
|
+
#
|
73
|
+
class RecvProtocolError < ProtocolError
|
74
|
+
def initialize machine, state_name, message_name, data
|
75
|
+
super machine, state_name, :recv, message_name, data
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,359 @@
|
|
1
|
+
require 'event_state'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
# load example machines
|
5
|
+
require 'event_state/ex_echo'
|
6
|
+
require 'event_state/ex_readme'
|
7
|
+
require 'event_state/ex_secret'
|
8
|
+
|
9
|
+
# give more helpful errors
|
10
|
+
Thread.abort_on_exception = true
|
11
|
+
|
12
|
+
class TestEventState < Test::Unit::TestCase
|
13
|
+
include EventState
|
14
|
+
|
15
|
+
DEFAULT_HOST = 'localhost'
|
16
|
+
DEFAULT_PORT = 14159
|
17
|
+
|
18
|
+
def run_server_and_client server_class, client_class, opts={}, &block
|
19
|
+
host = opts[:host] || DEFAULT_HOST
|
20
|
+
port = opts[:port] || DEFAULT_PORT
|
21
|
+
server_args = opts[:server_args] || []
|
22
|
+
client_args = opts[:client_args] || []
|
23
|
+
|
24
|
+
client = nil
|
25
|
+
EM.run do
|
26
|
+
EM.error_handler do |e|
|
27
|
+
puts "EM ERROR: #{e.inspect}"
|
28
|
+
puts e.backtrace
|
29
|
+
end
|
30
|
+
EventMachine.start_server(host, port, server_class, *server_args)
|
31
|
+
client = EventMachine.connect(host, port, client_class,
|
32
|
+
*client_args, &block)
|
33
|
+
end
|
34
|
+
client
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_echo_test client_class
|
38
|
+
server_log = []
|
39
|
+
recorder = run_server_and_client(LoggingEchoServer, client_class,
|
40
|
+
server_args: [server_log],
|
41
|
+
client_args: [%w(foo bar baz), []]).recorder
|
42
|
+
|
43
|
+
assert_equal [
|
44
|
+
"entering listening state", # on_enter called on the start state
|
45
|
+
"exiting listening state", # when a message is received
|
46
|
+
"speaking foo", # the first noise
|
47
|
+
"exiting speaking state", # sent echo to client
|
48
|
+
"entering listening state", # now listening for next noise
|
49
|
+
"exiting listening state", # ...
|
50
|
+
"speaking bar",
|
51
|
+
"exiting speaking state",
|
52
|
+
"entering listening state",
|
53
|
+
"exiting listening state",
|
54
|
+
"speaking baz",
|
55
|
+
"exiting speaking state",
|
56
|
+
"entering listening state"], server_log
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_echo_basic
|
60
|
+
assert_equal %w(foo bar baz),
|
61
|
+
run_server_and_client(EchoServer, EchoClient,
|
62
|
+
client_args: [%w(foo bar baz), []]).recorder
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_delayed_echo
|
66
|
+
assert_equal %w(foo bar baz),
|
67
|
+
run_server_and_client(DelayedEchoServer, EchoClient,
|
68
|
+
server_args: [0.5],
|
69
|
+
client_args: [%w(foo bar baz), []]).recorder
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_echo_with_object_protocol_client
|
73
|
+
run_echo_test ObjectProtocolEchoClient
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_echo_with_event_state_client
|
77
|
+
run_echo_test EchoClient
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_secret_server
|
81
|
+
run_server_and_client(TopSecretServer, TopSecretClient)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_print_state_machine_dot
|
85
|
+
dot = EchoClient.print_state_machine_dot(:graph_options => 'rankdir=LR;')
|
86
|
+
assert_equal <<DOT, dot.string
|
87
|
+
digraph "EventState::EchoClient" {
|
88
|
+
rankdir=LR;
|
89
|
+
speaking [peripheries=2];
|
90
|
+
speaking -> listening [color=red,label="String"];
|
91
|
+
listening -> speaking [color=blue,label="String"];
|
92
|
+
}
|
93
|
+
DOT
|
94
|
+
end
|
95
|
+
|
96
|
+
class TestDSLBasic < EventState::Machine; end
|
97
|
+
|
98
|
+
def test_dsl_basic
|
99
|
+
#
|
100
|
+
# check that we get the transitions right for this simple DSL
|
101
|
+
#
|
102
|
+
trans = nil
|
103
|
+
TestDSLBasic.class_eval do
|
104
|
+
protocol do
|
105
|
+
state :foo do
|
106
|
+
on_recv :hello, :bar
|
107
|
+
end
|
108
|
+
state :bar do
|
109
|
+
on_recv :good_bye, :foo
|
110
|
+
end
|
111
|
+
end
|
112
|
+
trans = transitions
|
113
|
+
end
|
114
|
+
|
115
|
+
assert_equal [
|
116
|
+
[:foo, [:recv, :hello], :bar],
|
117
|
+
[:bar, [:recv, :good_bye], :foo]], trans
|
118
|
+
end
|
119
|
+
|
120
|
+
class TestDSLNoNestedProtocols < EventState::Machine; end
|
121
|
+
|
122
|
+
def test_dsl_no_nested_states
|
123
|
+
#
|
124
|
+
# nested protocol blocks are illegal
|
125
|
+
#
|
126
|
+
assert_raises(RuntimeError) {
|
127
|
+
TestDSLNoNestedProtocols.class_eval do
|
128
|
+
protocol do
|
129
|
+
protocol do
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
class TestDSLNoNestedStates < EventState::Machine; end
|
137
|
+
|
138
|
+
def test_dsl_no_nested_states
|
139
|
+
#
|
140
|
+
# nested state blocks are illegal
|
141
|
+
#
|
142
|
+
assert_raises(RuntimeError) {
|
143
|
+
TestDSLNoNestedStates.class_eval do
|
144
|
+
protocol do
|
145
|
+
state :foo do
|
146
|
+
state :bar do
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
class TestDSLImplicitState < EventState::Machine; end
|
155
|
+
|
156
|
+
def test_dsl_implicit_state
|
157
|
+
#
|
158
|
+
# if a state is referenced in an on_send or on_recv but is not declared with
|
159
|
+
# state, the protocol method should add it to @states when it terminates
|
160
|
+
#
|
161
|
+
inner_states = nil
|
162
|
+
outer_states = nil
|
163
|
+
TestDSLImplicitState.class_eval do
|
164
|
+
protocol do
|
165
|
+
state :foo do
|
166
|
+
on_send :bar, :baz
|
167
|
+
end
|
168
|
+
inner_states = states.dup
|
169
|
+
end
|
170
|
+
outer_states = states.dup
|
171
|
+
end
|
172
|
+
assert_nil inner_states[:baz]
|
173
|
+
assert_kind_of EventState::State, outer_states[:baz]
|
174
|
+
end
|
175
|
+
|
176
|
+
class TestDelayClient < EventState::ObjectMachine
|
177
|
+
def initialize log, delays
|
178
|
+
super
|
179
|
+
@log = log
|
180
|
+
@delays = delays
|
181
|
+
end
|
182
|
+
|
183
|
+
protocol do
|
184
|
+
state :foo do
|
185
|
+
on_send String, :bar
|
186
|
+
on_enter do
|
187
|
+
EM.defer proc {
|
188
|
+
@log << "sleeping in foo"
|
189
|
+
sleep @delays.shift
|
190
|
+
@log << "finished sleep in foo"
|
191
|
+
}, proc {
|
192
|
+
send_message 'awake!'
|
193
|
+
}
|
194
|
+
end
|
195
|
+
on_unbind do
|
196
|
+
@log << "unbound in foo"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
state :bar do
|
201
|
+
on_enter do
|
202
|
+
EM.defer proc {
|
203
|
+
@log << "sleeping in bar"
|
204
|
+
sleep @delays.shift
|
205
|
+
@log << "finished sleep in bar"
|
206
|
+
}, proc {
|
207
|
+
close_connection
|
208
|
+
}
|
209
|
+
end
|
210
|
+
on_unbind do
|
211
|
+
@log << "unbound in bar"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class TestUnbindServer < EventState::ObjectMachine
|
218
|
+
def initialize log, timeout
|
219
|
+
super
|
220
|
+
@log = log
|
221
|
+
@timeout = timeout
|
222
|
+
end
|
223
|
+
protocol do
|
224
|
+
state :foo do
|
225
|
+
on_enter do
|
226
|
+
@log << "entered foo"
|
227
|
+
self.comm_inactivity_timeout = @timeout
|
228
|
+
end
|
229
|
+
on_unbind do
|
230
|
+
@log << "unbound in foo"
|
231
|
+
EM.stop
|
232
|
+
end
|
233
|
+
|
234
|
+
on_recv String, :bar
|
235
|
+
end
|
236
|
+
state :bar do
|
237
|
+
on_enter do
|
238
|
+
@log << "entered bar"
|
239
|
+
end
|
240
|
+
on_unbind do
|
241
|
+
@log << "unbound in bar"
|
242
|
+
EM.stop
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def test_unbind_on_timeout
|
249
|
+
#
|
250
|
+
# first, set delays so that we time out in the first server state (foo)
|
251
|
+
#
|
252
|
+
server_log = []
|
253
|
+
client_log = []
|
254
|
+
run_server_and_client(TestUnbindServer, TestDelayClient,
|
255
|
+
server_args: [server_log, 1],
|
256
|
+
client_args: [client_log, [2,2]])
|
257
|
+
assert_equal [
|
258
|
+
"entered foo",
|
259
|
+
"unbound in foo"], server_log
|
260
|
+
|
261
|
+
# the client log isn't entirely deterministic; depends on threading
|
262
|
+
assert_equal "sleeping in foo", client_log.first
|
263
|
+
|
264
|
+
#
|
265
|
+
# next, set delays so that we time out in the second server state (bar)
|
266
|
+
#
|
267
|
+
server_log = []
|
268
|
+
client_log = []
|
269
|
+
run_server_and_client(TestUnbindServer, TestDelayClient,
|
270
|
+
server_args: [server_log, 1],
|
271
|
+
client_args: [client_log, [0.5,2]])
|
272
|
+
assert_equal [
|
273
|
+
"entered foo",
|
274
|
+
"entered bar",
|
275
|
+
"unbound in bar"], server_log
|
276
|
+
assert_equal [
|
277
|
+
"sleeping in foo",
|
278
|
+
"finished sleep in foo",
|
279
|
+
"sleeping in bar",
|
280
|
+
"unbound in bar"], client_log
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_readme_example
|
284
|
+
MessageEchoClient.demo
|
285
|
+
end
|
286
|
+
|
287
|
+
class TestProtocolErrorSend < EventState::ObjectMachine
|
288
|
+
protocol do
|
289
|
+
state :foo do
|
290
|
+
on_send String, :bar
|
291
|
+
on_enter do
|
292
|
+
send_message 42
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_protocol_error_on_send
|
299
|
+
#
|
300
|
+
# TestProtocolErrorSend sends an integer instead of a string; this should
|
301
|
+
# cause an error on the server side; the server doesn't shut down; it just
|
302
|
+
# calls the EM error_handler
|
303
|
+
#
|
304
|
+
error = nil
|
305
|
+
EM.run do
|
306
|
+
EM.error_handler do |e|
|
307
|
+
# we get a ConnectionNotBound error from the client, too
|
308
|
+
error = e if !error
|
309
|
+
EM.stop
|
310
|
+
end
|
311
|
+
EM.start_server DEFAULT_HOST, DEFAULT_PORT, TestProtocolErrorSend
|
312
|
+
EM.connect DEFAULT_HOST, DEFAULT_PORT do
|
313
|
+
# just want to force the server to init
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
assert_kind_of SendProtocolError, error
|
318
|
+
assert_kind_of TestProtocolErrorSend, error.machine
|
319
|
+
assert_equal :foo, error.state_name
|
320
|
+
assert_equal :send, error.action
|
321
|
+
assert_equal Fixnum, error.message_name
|
322
|
+
assert_equal 42, error.data
|
323
|
+
end
|
324
|
+
|
325
|
+
class TestProtocolErrorClient < EventState::ObjectMachine
|
326
|
+
protocol do
|
327
|
+
state :foo do
|
328
|
+
on_send Fixnum, :bar
|
329
|
+
on_enter do
|
330
|
+
send_message 42
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def test_protocol_error_on_recv
|
337
|
+
#
|
338
|
+
# the EchoServer expects a String, but TestProtocolErrorClient gives it an
|
339
|
+
# integer; this causes a protocol error
|
340
|
+
#
|
341
|
+
error = nil
|
342
|
+
EM.run do
|
343
|
+
EM.error_handler do |e|
|
344
|
+
error = e
|
345
|
+
EM.stop
|
346
|
+
end
|
347
|
+
EM.start_server DEFAULT_HOST, DEFAULT_PORT, EchoServer
|
348
|
+
EM.connect DEFAULT_HOST, DEFAULT_PORT, TestProtocolErrorClient
|
349
|
+
end
|
350
|
+
|
351
|
+
assert_kind_of RecvProtocolError, error
|
352
|
+
assert_kind_of EchoServer, error.machine
|
353
|
+
assert_equal :listening, error.state_name
|
354
|
+
assert_equal :recv, error.action
|
355
|
+
assert_equal Fixnum, error.message_name
|
356
|
+
assert_equal 42, error.data
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: event_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Lees-Miller
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-30 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &80005190 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.12.10
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *80005190
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: gemma
|
27
|
+
requirement: &80004940 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.0.0
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *80004940
|
36
|
+
description: A small embedded DSL for implementing stateful protocols in EventMachine
|
37
|
+
using finite state machines.
|
38
|
+
email:
|
39
|
+
- jdleesmiller@gmail.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- lib/event_state/version.rb
|
46
|
+
- lib/event_state/object_machine.rb
|
47
|
+
- lib/event_state/state.rb
|
48
|
+
- lib/event_state/machine.rb
|
49
|
+
- lib/event_state.rb
|
50
|
+
- README.rdoc
|
51
|
+
- test/event_state/event_state_test.rb
|
52
|
+
homepage: http://github.com/jdleesmiller/event_state
|
53
|
+
licenses: []
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --main
|
57
|
+
- README.rdoc
|
58
|
+
- --title
|
59
|
+
- event_state-0.0.1 Documentation
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
hash: 304276877
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
hash: 304276877
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project: event_state
|
82
|
+
rubygems_version: 1.8.10
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: StateMachines for EventMachines.
|
86
|
+
test_files:
|
87
|
+
- test/event_state/event_state_test.rb
|