event_state 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|