Skip to content

Commit cc1fb87

Browse files
committed
StatusFlagGuard tests
1 parent b2923f6 commit cc1fb87

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"""Tests for StatusFlagGuard decorator."""
2+
3+
import pytest
4+
from py_trees import common
5+
from py_trees.behaviour import Behaviour
6+
from py_trees.common import Status
7+
from pydantic import BaseModel
8+
9+
from redis_release.bht.decorators import StatusFlagGuard
10+
11+
12+
class StatusFlagContainer(BaseModel):
13+
"""Container for holding status flags."""
14+
15+
status_flag: Status | None = None
16+
17+
18+
class SuccessBehaviour(Behaviour):
19+
"""A behaviour that always succeeds."""
20+
21+
def update(self) -> Status:
22+
return Status.SUCCESS
23+
24+
25+
class FailureBehaviour(Behaviour):
26+
"""A behaviour that always fails."""
27+
28+
def update(self) -> Status:
29+
return Status.FAILURE
30+
31+
32+
class RunningBehaviour(Behaviour):
33+
"""A behaviour that always returns RUNNING."""
34+
35+
def update(self) -> Status:
36+
return Status.RUNNING
37+
38+
39+
class TestStatusFlagGuardInitialization:
40+
"""Test StatusFlagGuard initialization."""
41+
42+
def test_init_with_valid_container(self) -> None:
43+
"""Test initialization with valid container."""
44+
container = StatusFlagContainer()
45+
child = SuccessBehaviour(name="child")
46+
guard = StatusFlagGuard(
47+
name="test_guard",
48+
child=child,
49+
container=container,
50+
flag="status_flag",
51+
)
52+
assert guard.name == "test_guard"
53+
assert guard.flag == "status_flag"
54+
assert guard.guard_status == Status.FAILURE
55+
56+
def test_init_with_none_name_failure(self) -> None:
57+
"""Test initialization with None name generates default name for FAILURE."""
58+
container = StatusFlagContainer()
59+
child = SuccessBehaviour(name="child")
60+
guard = StatusFlagGuard(
61+
name=None,
62+
child=child,
63+
container=container,
64+
flag="status_flag",
65+
guard_status=common.Status.FAILURE,
66+
)
67+
assert guard.name == "Unless status_flag failed"
68+
69+
def test_init_with_none_name_success(self) -> None:
70+
"""Test initialization with None name generates default name for SUCCESS."""
71+
container = StatusFlagContainer()
72+
child = SuccessBehaviour(name="child")
73+
guard = StatusFlagGuard(
74+
name=None,
75+
child=child,
76+
container=container,
77+
flag="status_flag",
78+
guard_status=common.Status.SUCCESS,
79+
)
80+
assert guard.name == "Unless status_flag succeeded"
81+
82+
def test_init_with_custom_guard_status(self) -> None:
83+
"""Test initialization with custom guard_status."""
84+
container = StatusFlagContainer()
85+
child = SuccessBehaviour(name="child")
86+
guard = StatusFlagGuard(
87+
name="test_guard",
88+
child=child,
89+
container=container,
90+
flag="status_flag",
91+
guard_status=Status.SUCCESS,
92+
)
93+
assert guard.guard_status == Status.SUCCESS
94+
95+
def test_init_with_nonexistent_field(self) -> None:
96+
"""Test initialization fails with nonexistent field."""
97+
container = StatusFlagContainer()
98+
child = SuccessBehaviour(name="child")
99+
with pytest.raises(ValueError, match="Field 'nonexistent' does not exist"):
100+
StatusFlagGuard(
101+
name="test_guard",
102+
child=child,
103+
container=container,
104+
flag="nonexistent",
105+
)
106+
107+
def test_init_with_invalid_flag_type(self) -> None:
108+
"""Test initialization fails when flag has invalid type."""
109+
110+
class BadContainer(BaseModel):
111+
status_flag: str = "invalid"
112+
113+
container = BadContainer()
114+
child = SuccessBehaviour(name="child")
115+
with pytest.raises(TypeError, match="must be either common.Status or None"):
116+
StatusFlagGuard(
117+
name="test_guard",
118+
child=child,
119+
container=container,
120+
flag="status_flag",
121+
)
122+
123+
def test_init_with_invalid_guard_status(self) -> None:
124+
"""Test initialization fails with invalid guard_status."""
125+
container = StatusFlagContainer()
126+
child = SuccessBehaviour(name="child")
127+
with pytest.raises(ValueError, match="guard_status must be FAILURE or SUCCESS"):
128+
StatusFlagGuard(
129+
name="test_guard",
130+
child=child,
131+
container=container,
132+
flag="status_flag",
133+
guard_status=Status.RUNNING,
134+
)
135+
136+
137+
class TestStatusFlagGuardGuarding:
138+
"""Test StatusFlagGuard guarding behavior."""
139+
140+
def test_guard_prevents_execution_when_flag_matches_guard_status(self) -> None:
141+
"""Test that guard prevents execution when flag matches guard_status."""
142+
container = StatusFlagContainer(status_flag=Status.FAILURE)
143+
child = SuccessBehaviour(name="child")
144+
guard = StatusFlagGuard(
145+
name="test_guard",
146+
child=child,
147+
container=container,
148+
flag="status_flag",
149+
guard_status=Status.FAILURE,
150+
)
151+
152+
# Guard should return FAILURE without executing child
153+
status = guard.update()
154+
assert status == Status.FAILURE
155+
156+
def test_guard_allows_execution_when_flag_is_none(self) -> None:
157+
"""Test that guard allows execution when flag is None."""
158+
container = StatusFlagContainer(status_flag=None)
159+
child = SuccessBehaviour(name="child")
160+
guard = StatusFlagGuard(
161+
name="test_guard",
162+
child=child,
163+
container=container,
164+
flag="status_flag",
165+
guard_status=Status.FAILURE,
166+
)
167+
168+
# Child should execute and return SUCCESS
169+
guard.decorated.status = Status.SUCCESS
170+
status = guard.update()
171+
assert status == Status.SUCCESS
172+
173+
def test_guard_allows_execution_when_flag_differs_from_guard_status(self) -> None:
174+
"""Test that guard allows execution when flag differs from guard_status."""
175+
container = StatusFlagContainer(status_flag=Status.SUCCESS)
176+
child = SuccessBehaviour(name="child")
177+
guard = StatusFlagGuard(
178+
name="test_guard",
179+
child=child,
180+
container=container,
181+
flag="status_flag",
182+
guard_status=Status.FAILURE,
183+
)
184+
185+
# Child should execute and return SUCCESS
186+
guard.decorated.status = Status.SUCCESS
187+
status = guard.update()
188+
assert status == Status.SUCCESS
189+
190+
191+
class TestStatusFlagGuardFlagUpdate:
192+
"""Test StatusFlagGuard flag update behavior."""
193+
194+
def test_flag_updated_on_child_success(self) -> None:
195+
"""Test that flag is updated to child's status on success."""
196+
container = StatusFlagContainer(status_flag=None)
197+
child = SuccessBehaviour(name="child")
198+
guard = StatusFlagGuard(
199+
name="test_guard",
200+
child=child,
201+
container=container,
202+
flag="status_flag",
203+
)
204+
205+
# Simulate child execution - update() should update the flag
206+
guard.decorated.status = Status.SUCCESS
207+
status = guard.update()
208+
assert status == Status.SUCCESS
209+
assert container.status_flag == Status.SUCCESS
210+
211+
def test_flag_updated_on_child_failure(self) -> None:
212+
"""Test that flag is updated to child's status on failure."""
213+
container = StatusFlagContainer(status_flag=None)
214+
child = FailureBehaviour(name="child")
215+
guard = StatusFlagGuard(
216+
name="test_guard",
217+
child=child,
218+
container=container,
219+
flag="status_flag",
220+
)
221+
222+
# Simulate child execution - update() should update the flag
223+
guard.decorated.status = Status.FAILURE
224+
status = guard.update()
225+
assert status == Status.FAILURE
226+
assert container.status_flag == Status.FAILURE
227+
228+
def test_flag_updated_on_child_running(self) -> None:
229+
"""Test that flag is updated to child's status on RUNNING."""
230+
container = StatusFlagContainer(status_flag=None)
231+
child = RunningBehaviour(name="child")
232+
guard = StatusFlagGuard(
233+
name="test_guard",
234+
child=child,
235+
container=container,
236+
flag="status_flag",
237+
)
238+
239+
# Simulate child execution - update() should update the flag even for RUNNING
240+
guard.decorated.status = Status.RUNNING
241+
status = guard.update()
242+
assert status == Status.RUNNING
243+
assert container.status_flag == Status.RUNNING
244+
245+
def test_flag_not_updated_when_guard_active(self) -> None:
246+
"""Test that flag is not updated when guard is active."""
247+
container = StatusFlagContainer(status_flag=Status.FAILURE)
248+
child = SuccessBehaviour(name="child")
249+
guard = StatusFlagGuard(
250+
name="test_guard",
251+
child=child,
252+
container=container,
253+
flag="status_flag",
254+
guard_status=Status.FAILURE,
255+
)
256+
257+
# Guard is active, flag should not be updated
258+
guard.decorated.status = Status.SUCCESS
259+
status = guard.update()
260+
assert status == Status.FAILURE
261+
assert container.status_flag == Status.FAILURE
262+
263+
264+
class TestStatusFlagGuardWithDifferentGuardStatus:
265+
"""Test StatusFlagGuard with different guard_status values."""
266+
267+
def test_guard_with_failure_status(self) -> None:
268+
"""Test guard with FAILURE as guard_status."""
269+
container = StatusFlagContainer(status_flag=Status.FAILURE)
270+
child = SuccessBehaviour(name="child")
271+
guard = StatusFlagGuard(
272+
name="test_guard",
273+
child=child,
274+
container=container,
275+
flag="status_flag",
276+
guard_status=Status.FAILURE,
277+
)
278+
279+
# Guard should return FAILURE
280+
status = guard.update()
281+
assert status == Status.FAILURE
282+
283+
def test_guard_with_success_status(self) -> None:
284+
"""Test guard with SUCCESS as guard_status."""
285+
container = StatusFlagContainer(status_flag=Status.SUCCESS)
286+
child = FailureBehaviour(name="child")
287+
guard = StatusFlagGuard(
288+
name="test_guard",
289+
child=child,
290+
container=container,
291+
flag="status_flag",
292+
guard_status=Status.SUCCESS,
293+
)
294+
295+
# Guard should return SUCCESS
296+
status = guard.update()
297+
assert status == Status.SUCCESS

0 commit comments

Comments
 (0)