Mercurial > prosody-hg
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 |
| parents | |
| children |
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); |
