Software /
code /
prosody
Comparison
spec/util_fsm_spec.lua @ 13019:8a2f75e38eb2
util.fsm: New utility lib for finite state machines
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Thu, 17 Mar 2022 17:45:27 +0000 |
comparison
equal
deleted
inserted
replaced
13018:9ed4a8502c54 | 13019:8a2f75e38eb2 |
---|---|
1 describe("util.fsm", function () | |
2 local new_fsm = require "util.fsm".new; | |
3 | |
4 do | |
5 local fsm = new_fsm({ | |
6 transitions = { | |
7 { name = "melt", from = "solid", to = "liquid" }; | |
8 { name = "freeze", from = "liquid", to = "solid" }; | |
9 }; | |
10 }); | |
11 | |
12 it("works", function () | |
13 local water = fsm:init("liquid"); | |
14 water:freeze(); | |
15 assert.equal("solid", water.state); | |
16 water:melt(); | |
17 assert.equal("liquid", water.state); | |
18 end); | |
19 | |
20 it("does not allow invalid transitions", function () | |
21 local water = fsm:init("liquid"); | |
22 assert.has_errors(function () | |
23 water:melt(); | |
24 end, "Invalid state transition: liquid cannot melt"); | |
25 | |
26 water:freeze(); | |
27 assert.equal("solid", water.state); | |
28 | |
29 water:melt(); | |
30 assert.equal("liquid", water.state); | |
31 | |
32 assert.has_errors(function () | |
33 water:melt(); | |
34 end, "Invalid state transition: liquid cannot melt"); | |
35 end); | |
36 end | |
37 | |
38 it("notifies observers", function () | |
39 local n = 0; | |
40 local has_become_solid = spy.new(function (transition) | |
41 assert.is_table(transition); | |
42 assert.equal("solid", transition.to); | |
43 assert.is_not_nil(transition.instance); | |
44 n = n + 1; | |
45 if n == 1 then | |
46 assert.is_nil(transition.from); | |
47 assert.is_nil(transition.from_attr); | |
48 elseif n == 2 then | |
49 assert.equal("liquid", transition.from); | |
50 assert.is_nil(transition.from_attr); | |
51 assert.equal("freeze", transition.name); | |
52 end | |
53 end); | |
54 local is_melting = spy.new(function (transition) | |
55 assert.is_table(transition); | |
56 assert.equal("melt", transition.name); | |
57 assert.is_not_nil(transition.instance); | |
58 end); | |
59 local fsm = new_fsm({ | |
60 transitions = { | |
61 { name = "melt", from = "solid", to = "liquid" }; | |
62 { name = "freeze", from = "liquid", to = "solid" }; | |
63 }; | |
64 state_handlers = { | |
65 solid = has_become_solid; | |
66 }; | |
67 | |
68 transition_handlers = { | |
69 melt = is_melting; | |
70 }; | |
71 }); | |
72 | |
73 local water = fsm:init("liquid"); | |
74 assert.spy(has_become_solid).was_not_called(); | |
75 | |
76 local ice = fsm:init("solid"); --luacheck: ignore 211/ice | |
77 assert.spy(has_become_solid).was_called(1); | |
78 | |
79 water:freeze(); | |
80 | |
81 assert.spy(is_melting).was_not_called(); | |
82 water:melt(); | |
83 assert.spy(is_melting).was_called(1); | |
84 end); | |
85 | |
86 local function test_machine(fsm_spec, expected_transitions, test_func) | |
87 fsm_spec.handlers = fsm_spec.handlers or {}; | |
88 fsm_spec.handlers.transitioned = function (transition) | |
89 local expected_transition = table.remove(expected_transitions, 1); | |
90 assert.same(expected_transition, { | |
91 name = transition.name; | |
92 to = transition.to; | |
93 to_attr = transition.to_attr; | |
94 from = transition.from; | |
95 from_attr = transition.from_attr; | |
96 }); | |
97 end; | |
98 local fsm = new_fsm(fsm_spec); | |
99 test_func(fsm); | |
100 assert.equal(0, #expected_transitions); | |
101 end | |
102 | |
103 | |
104 it("handles transitions with the same name", function () | |
105 local expected_transitions = { | |
106 { name = nil , from = "none", to = "A" }; | |
107 { name = "step", from = "A", to = "B" }; | |
108 { name = "step", from = "B", to = "C" }; | |
109 { name = "step", from = "C", to = "D" }; | |
110 }; | |
111 | |
112 test_machine({ | |
113 default_state = "none"; | |
114 transitions = { | |
115 { name = "step", from = "A", to = "B" }; | |
116 { name = "step", from = "B", to = "C" }; | |
117 { name = "step", from = "C", to = "D" }; | |
118 }; | |
119 }, expected_transitions, function (fsm) | |
120 local i = fsm:init("A"); | |
121 i:step(); -- B | |
122 i:step(); -- C | |
123 i:step(); -- D | |
124 assert.has_errors(function () | |
125 i:step(); | |
126 end, "Invalid state transition: D cannot step"); | |
127 end); | |
128 end); | |
129 | |
130 it("handles supports wildcard transitions", function () | |
131 local expected_transitions = { | |
132 { name = nil , from = "none", to = "A" }; | |
133 { name = "step", from = "A", to = "B" }; | |
134 { name = "step", from = "B", to = "C" }; | |
135 { name = "reset", from = "C", to = "A" }; | |
136 { name = "step", from = "A", to = "B" }; | |
137 { name = "step", from = "B", to = "C" }; | |
138 { name = "step", from = "C", to = "D" }; | |
139 }; | |
140 | |
141 test_machine({ | |
142 default_state = "none"; | |
143 transitions = { | |
144 { name = "step", from = "A", to = "B" }; | |
145 { name = "step", from = "B", to = "C" }; | |
146 { name = "step", from = "C", to = "D" }; | |
147 { name = "reset", from = "*", to = "A" }; | |
148 }; | |
149 }, expected_transitions, function (fsm) | |
150 local i = fsm:init("A"); | |
151 i:step(); -- B | |
152 i:step(); -- C | |
153 i:reset(); -- A | |
154 i:step(); -- B | |
155 i:step(); -- C | |
156 i:step(); -- D | |
157 assert.has_errors(function () | |
158 i:step(); | |
159 end, "Invalid state transition: D cannot step"); | |
160 end); | |
161 end); | |
162 | |
163 it("supports specifying multiple from states", function () | |
164 local expected_transitions = { | |
165 { name = nil , from = "none", to = "A" }; | |
166 { name = "step", from = "A", to = "B" }; | |
167 { name = "step", from = "B", to = "C" }; | |
168 { name = "reset", from = "C", to = "A" }; | |
169 { name = "step", from = "A", to = "B" }; | |
170 { name = "step", from = "B", to = "C" }; | |
171 { name = "step", from = "C", to = "D" }; | |
172 }; | |
173 | |
174 test_machine({ | |
175 default_state = "none"; | |
176 transitions = { | |
177 { name = "step", from = "A", to = "B" }; | |
178 { name = "step", from = "B", to = "C" }; | |
179 { name = "step", from = "C", to = "D" }; | |
180 { name = "reset", from = {"B", "C", "D"}, to = "A" }; | |
181 }; | |
182 }, expected_transitions, function (fsm) | |
183 local i = fsm:init("A"); | |
184 i:step(); -- B | |
185 i:step(); -- C | |
186 i:reset(); -- A | |
187 assert.has_errors(function () | |
188 i:reset(); | |
189 end, "Invalid state transition: A cannot reset"); | |
190 i:step(); -- B | |
191 i:step(); -- C | |
192 i:step(); -- D | |
193 assert.has_errors(function () | |
194 i:step(); | |
195 end, "Invalid state transition: D cannot step"); | |
196 end); | |
197 end); | |
198 | |
199 it("handles transitions with the same start/end state", function () | |
200 local expected_transitions = { | |
201 { name = nil , from = "none", to = "A" }; | |
202 { name = "step", from = "A", to = "B" }; | |
203 { name = "step", from = "B", to = "B" }; | |
204 { name = "step", from = "B", to = "B" }; | |
205 }; | |
206 | |
207 test_machine({ | |
208 default_state = "none"; | |
209 transitions = { | |
210 { name = "step", from = "A", to = "B" }; | |
211 { name = "step", from = "B", to = "B" }; | |
212 }; | |
213 }, expected_transitions, function (fsm) | |
214 local i = fsm:init("A"); | |
215 i:step(); -- B | |
216 i:step(); -- B | |
217 i:step(); -- B | |
218 end); | |
219 end); | |
220 | |
221 it("can identify instances of a specific fsm", function () | |
222 local fsm1 = new_fsm({ default_state = "a" }); | |
223 local fsm2 = new_fsm({ default_state = "a" }); | |
224 | |
225 local i1 = fsm1:init(); | |
226 local i2 = fsm2:init(); | |
227 | |
228 assert.truthy(fsm1:is_instance(i1)); | |
229 assert.truthy(fsm2:is_instance(i2)); | |
230 | |
231 assert.falsy(fsm1:is_instance(i2)); | |
232 assert.falsy(fsm2:is_instance(i1)); | |
233 end); | |
234 | |
235 it("errors when an invalid initial state is passed", function () | |
236 local fsm1 = new_fsm({ | |
237 transitions = { | |
238 { name = "", from = "A", to = "B" }; | |
239 }; | |
240 }); | |
241 | |
242 assert.has_no_errors(function () | |
243 fsm1:init("A"); | |
244 end); | |
245 | |
246 assert.has_errors(function () | |
247 fsm1:init("C"); | |
248 end); | |
249 end); | |
250 end); |