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);