Skip to content

Commit 92a9c42

Browse files
committed
added tests
1 parent 4c70ef3 commit 92a9c42

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import Any
5+
from unittest.mock import AsyncMock, MagicMock
6+
7+
import pytest
8+
9+
from pyobs.robotic.scripts import Script
10+
from pyobs.robotic.scripts.control.cases import CasesRunner
11+
from pyobs.robotic.scripts.control.conditional import ConditionalRunner
12+
from pyobs.robotic.scripts.control.parallel import ParallelRunner
13+
from pyobs.robotic.scripts.control.selector import SelectorScript
14+
from pyobs.robotic.scripts.control.sequential import SequentialRunner
15+
from pyobs.utils.enums import MotionStatus
16+
17+
# ── helper scripts ────────────────────────────────────────────────────────────
18+
19+
20+
class AlwaysRunScript(Script):
21+
ran: bool = False
22+
23+
async def can_run(self, data: Any) -> bool:
24+
return True
25+
26+
async def run(self, data: Any) -> None:
27+
self.ran = True
28+
29+
30+
class NeverRunScript(Script):
31+
ran: bool = False
32+
33+
async def can_run(self, data: Any) -> bool:
34+
return False
35+
36+
async def run(self, data: Any) -> None:
37+
self.ran = True
38+
39+
40+
class TrackingScript(Script):
41+
"""Records calls for ordering/parallel verification."""
42+
43+
order: list[str] = []
44+
name: str = "unnamed"
45+
46+
async def can_run(self, data: Any) -> bool:
47+
return True
48+
49+
async def run(self, data: Any) -> None:
50+
self.order.append(self.name)
51+
await asyncio.sleep(0.01)
52+
53+
54+
# ── SequentialRunner ──────────────────────────────────────────────────────────
55+
56+
57+
@pytest.mark.asyncio
58+
async def test_sequential_can_run_all_true() -> None:
59+
runner = SequentialRunner(scripts=[AlwaysRunScript(), AlwaysRunScript()])
60+
assert await runner.can_run(None) is True
61+
62+
63+
@pytest.mark.asyncio
64+
async def test_sequential_can_run_one_false() -> None:
65+
runner = SequentialRunner(scripts=[AlwaysRunScript(), NeverRunScript()])
66+
assert await runner.can_run(None) is False
67+
68+
69+
@pytest.mark.asyncio
70+
async def test_sequential_can_run_check_first_only() -> None:
71+
"""check_all_can_run=False only checks the first script."""
72+
runner = SequentialRunner(
73+
scripts=[AlwaysRunScript(), NeverRunScript()],
74+
check_all_can_run=False,
75+
)
76+
assert await runner.can_run(None) is True
77+
78+
79+
@pytest.mark.asyncio
80+
async def test_sequential_runs_all_scripts() -> None:
81+
s1, s2 = AlwaysRunScript(), AlwaysRunScript()
82+
runner = SequentialRunner(scripts=[s1, s2])
83+
await runner.run(None)
84+
assert s1.ran
85+
assert s2.ran
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_sequential_skips_scripts_that_cannot_run() -> None:
90+
s1, s2 = NeverRunScript(), AlwaysRunScript()
91+
runner = SequentialRunner(scripts=[s1, s2])
92+
await runner.run(None)
93+
assert not s1.ran
94+
assert s2.ran
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_sequential_runs_in_order() -> None:
99+
order: list[str] = []
100+
101+
class Ordered(Script):
102+
n: str
103+
104+
async def can_run(self, data: Any) -> bool:
105+
return True
106+
107+
async def run(self, data: Any) -> None:
108+
order.append(self.n)
109+
110+
runner = SequentialRunner(scripts=[Ordered(n="1"), Ordered(n="2"), Ordered(n="3")])
111+
await runner.run(None)
112+
assert order == ["1", "2", "3"]
113+
114+
115+
# ── ParallelRunner ────────────────────────────────────────────────────────────
116+
117+
118+
@pytest.mark.asyncio
119+
async def test_parallel_can_run_all_true() -> None:
120+
runner = ParallelRunner(scripts=[AlwaysRunScript(), AlwaysRunScript()])
121+
assert await runner.can_run(None) is True
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_parallel_can_run_one_false() -> None:
126+
runner = ParallelRunner(scripts=[AlwaysRunScript(), NeverRunScript()])
127+
assert await runner.can_run(None) is False
128+
129+
130+
@pytest.mark.asyncio
131+
async def test_parallel_can_run_any_with_check_false() -> None:
132+
"""check_all_can_run=False: passes if any script can run."""
133+
runner = ParallelRunner(
134+
scripts=[NeverRunScript(), AlwaysRunScript()],
135+
check_all_can_run=False,
136+
)
137+
assert await runner.can_run(None) is True
138+
139+
140+
@pytest.mark.asyncio
141+
async def test_parallel_runs_all_scripts() -> None:
142+
s1, s2 = AlwaysRunScript(), AlwaysRunScript()
143+
runner = ParallelRunner(scripts=[s1, s2])
144+
await runner.run(None)
145+
assert s1.ran
146+
assert s2.ran
147+
148+
149+
@pytest.mark.asyncio
150+
async def test_parallel_skips_scripts_that_cannot_run() -> None:
151+
s1, s2 = NeverRunScript(), AlwaysRunScript()
152+
runner = ParallelRunner(scripts=[s1, s2])
153+
await runner.run(None)
154+
assert not s1.ran
155+
assert s2.ran
156+
157+
158+
@pytest.mark.asyncio
159+
async def test_parallel_runs_concurrently() -> None:
160+
"""Scripts run concurrently — both start before either finishes."""
161+
started: list[str] = []
162+
finished: list[str] = []
163+
164+
class Timed(Script):
165+
n: str
166+
167+
async def can_run(self, data: Any) -> bool:
168+
return True
169+
170+
async def run(self, data: Any) -> None:
171+
started.append(self.n)
172+
await asyncio.sleep(0.05)
173+
finished.append(self.n)
174+
175+
runner = ParallelRunner(scripts=[Timed(n="A"), Timed(n="B")])
176+
await runner.run(None)
177+
178+
assert set(started) == {"A", "B"}
179+
assert set(finished) == {"A", "B"}
180+
# both started before either finished (concurrency)
181+
assert len(started) == 2
182+
183+
184+
@pytest.mark.asyncio
185+
async def test_parallel_exception_does_not_stop_others() -> None:
186+
"""Exception in one script is caught; other scripts still run."""
187+
188+
class FailingScript(Script):
189+
async def can_run(self, data: Any) -> bool:
190+
return True
191+
192+
async def run(self, data: Any) -> None:
193+
raise RuntimeError("intentional failure")
194+
195+
s2 = AlwaysRunScript()
196+
runner = ParallelRunner(scripts=[FailingScript(), s2])
197+
await runner.run(None) # should not raise
198+
assert s2.ran
199+
200+
201+
# ── CasesRunner ───────────────────────────────────────────────────────────────
202+
203+
204+
@pytest.mark.asyncio
205+
async def test_cases_selects_matching_case() -> None:
206+
s1, s2 = AlwaysRunScript(), AlwaysRunScript()
207+
runner = CasesRunner(expression="1", cases={1: s1, 2: s2})
208+
await runner.run(None)
209+
assert s1.ran
210+
assert not s2.ran
211+
212+
213+
@pytest.mark.asyncio
214+
async def test_cases_falls_through_to_else() -> None:
215+
s_else = AlwaysRunScript()
216+
runner = CasesRunner(expression="99", cases={1: AlwaysRunScript(), "else": s_else})
217+
await runner.run(None)
218+
assert s_else.ran
219+
220+
221+
@pytest.mark.asyncio
222+
async def test_cases_raises_on_no_match_no_else() -> None:
223+
runner = CasesRunner(expression="99", cases={1: AlwaysRunScript()})
224+
with pytest.raises(ValueError, match="Invalid choice"):
225+
await runner.run(None)
226+
227+
228+
@pytest.mark.asyncio
229+
async def test_cases_can_run_delegates_to_selected_script() -> None:
230+
runner = CasesRunner(expression="1", cases={1: AlwaysRunScript(), 2: NeverRunScript()})
231+
assert await runner.can_run(None) is True
232+
233+
runner2 = CasesRunner(expression="2", cases={1: AlwaysRunScript(), 2: NeverRunScript()})
234+
assert await runner2.can_run(None) is False
235+
236+
237+
@pytest.mark.asyncio
238+
async def test_cases_get_fits_headers() -> None:
239+
class HeaderScript(Script):
240+
async def can_run(self, data: Any) -> bool:
241+
return True
242+
243+
async def run(self, data: Any) -> None:
244+
pass
245+
246+
def get_fits_headers(self, namespaces: list[str] | None = None) -> dict[str, Any]:
247+
return {"KEY": ("value", "comment")}
248+
249+
runner = CasesRunner(expression="1", cases={1: HeaderScript()})
250+
headers = runner.get_fits_headers()
251+
assert "KEY" in headers
252+
253+
254+
# ── ConditionalRunner ─────────────────────────────────────────────────────────
255+
256+
257+
@pytest.mark.asyncio
258+
async def test_conditional_runs_true_branch() -> None:
259+
s_true, s_false = AlwaysRunScript(), AlwaysRunScript()
260+
runner = ConditionalRunner(condition="True", true=s_true, false=s_false)
261+
await runner.run(None)
262+
assert s_true.ran
263+
assert not s_false.ran
264+
265+
266+
@pytest.mark.asyncio
267+
async def test_conditional_runs_false_branch() -> None:
268+
s_true, s_false = AlwaysRunScript(), AlwaysRunScript()
269+
runner = ConditionalRunner(condition="False", true=s_true, false=s_false)
270+
await runner.run(None)
271+
assert not s_true.ran
272+
assert s_false.ran
273+
274+
275+
@pytest.mark.asyncio
276+
async def test_conditional_no_false_branch_is_noop() -> None:
277+
s_true = AlwaysRunScript()
278+
runner = ConditionalRunner(condition="False", true=s_true)
279+
await runner.run(None) # should not raise
280+
assert not s_true.ran
281+
282+
283+
@pytest.mark.asyncio
284+
async def test_conditional_can_run_true_branch() -> None:
285+
runner = ConditionalRunner(condition="True", true=AlwaysRunScript())
286+
assert await runner.can_run(None) is True
287+
288+
runner2 = ConditionalRunner(condition="True", true=NeverRunScript())
289+
assert await runner2.can_run(None) is False
290+
291+
292+
@pytest.mark.asyncio
293+
async def test_conditional_can_run_no_script_returns_true() -> None:
294+
"""When condition is False and no false branch, can_run returns True."""
295+
runner = ConditionalRunner(condition="False", true=NeverRunScript())
296+
assert await runner.can_run(None) is True
297+
298+
299+
@pytest.mark.asyncio
300+
async def test_conditional_get_fits_headers_no_script() -> None:
301+
runner = ConditionalRunner(condition="False", true=AlwaysRunScript())
302+
assert runner.get_fits_headers() == {}
303+
304+
305+
# ── SelectorScript ────────────────────────────────────────────────────────────
306+
307+
308+
@pytest.mark.asyncio
309+
async def test_selector_can_run_when_parked() -> None:
310+
selector = MagicMock()
311+
selector.get_motion_status = AsyncMock(return_value=MotionStatus.PARKED)
312+
313+
script = SelectorScript(mode="imaging", selector="selector")
314+
script._comm = MagicMock()
315+
script._comm.proxy = AsyncMock(return_value=selector)
316+
317+
assert await script.can_run(None) is True
318+
319+
320+
@pytest.mark.asyncio
321+
async def test_selector_can_run_when_positioned() -> None:
322+
selector = MagicMock()
323+
selector.get_motion_status = AsyncMock(return_value=MotionStatus.POSITIONED)
324+
325+
script = SelectorScript(mode="imaging", selector="selector")
326+
script._comm = MagicMock()
327+
script._comm.proxy = AsyncMock(return_value=selector)
328+
329+
assert await script.can_run(None) is True
330+
331+
332+
@pytest.mark.asyncio
333+
async def test_selector_cannot_run_when_moving() -> None:
334+
selector = MagicMock()
335+
selector.get_motion_status = AsyncMock(return_value=MotionStatus.SLEWING)
336+
337+
script = SelectorScript(mode="imaging", selector="selector")
338+
script._comm = MagicMock()
339+
script._comm.proxy = AsyncMock(return_value=selector)
340+
341+
assert await script.can_run(None) is False
342+
343+
344+
@pytest.mark.asyncio
345+
async def test_selector_run_sets_mode() -> None:
346+
selector = MagicMock()
347+
selector.set_mode = AsyncMock()
348+
349+
script = SelectorScript(mode="spectroscopy", selector="selector")
350+
script._comm = MagicMock()
351+
script._comm.proxy = AsyncMock(return_value=selector)
352+
353+
await script.run(None)
354+
selector.set_mode.assert_called_once_with("spectroscopy")

0 commit comments

Comments
 (0)