Skip to content

Commit e8dc5f3

Browse files
committed
tests: add tests for 'run application' transition
1 parent b403260 commit e8dc5f3

1 file changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
"""Tests for transitioning Manager from 'connected' to 'world_ready' state."""
2+
3+
import io
4+
import pytest
5+
import builtins
6+
from manager.manager.manager import Manager
7+
8+
9+
class DummyConsumer:
10+
"""A dummy consumer to capture messages sent by the Manager."""
11+
12+
def __init__(self):
13+
"""
14+
Initialize the DummyConsumer with empty message storage.
15+
16+
This constructor sets up the messages list and last_message attribute.
17+
"""
18+
self.messages = []
19+
self.last_message = None
20+
21+
def send_message(self, *args, **kwargs):
22+
"""
23+
Capture and store a message sent by the Manager.
24+
25+
Stores the message arguments and updates the last_message attribute.
26+
"""
27+
self.messages.append((args, kwargs))
28+
self.last_message = (args, kwargs)
29+
30+
31+
@pytest.fixture
32+
def manager(monkeypatch):
33+
"""Fixture to provide a Manager instance with patched dependencies for testing."""
34+
35+
# Patch subprocess.check_output for ROS_DISTRO and IMAGE_TAG
36+
def fake_check_output(cmd, *a, **k):
37+
if "ROS_DISTRO" in cmd[-1]:
38+
return b"humble"
39+
if "IMAGE_TAG" in cmd[-1]:
40+
return b"test_image_tag"
41+
return b""
42+
43+
monkeypatch.setattr("subprocess.check_output", fake_check_output)
44+
45+
# Patch check_gpu_acceleration where it is used
46+
monkeypatch.setattr(
47+
"manager.manager.manager.check_gpu_acceleration", lambda x=None: "OFF"
48+
)
49+
50+
# Patch os.makedirs and os.path.isdir to avoid real FS operations
51+
monkeypatch.setattr("os.makedirs", lambda path, exist_ok=False: None)
52+
monkeypatch.setattr("os.path.isdir", lambda path: True)
53+
54+
# Patch LauncherWorld to avoid launching real processes
55+
class DummyLauncherWorld:
56+
def __init__(self, *a, **k):
57+
self.launched = False
58+
59+
def launch(self):
60+
self.launched = True
61+
62+
def run(self):
63+
self.launched = True
64+
# Simulate running the world
65+
return
66+
67+
def terminate(self):
68+
pass
69+
70+
monkeypatch.setattr("manager.manager.manager.LauncherWorld", DummyLauncherWorld)
71+
72+
# Patch Server and FileWatchdog to avoid starting real servers
73+
class DummyServer:
74+
def __init__(self, port, update_callback):
75+
self.port = port
76+
self.update_callback = update_callback
77+
self.started = False
78+
79+
def start(self):
80+
self.started = True
81+
82+
def stop(self):
83+
self.started = False
84+
85+
class DummyFileWatchdog:
86+
def __init__(self, path, update_callback):
87+
self.path = path
88+
self.update_callback = update_callback
89+
self.started = False
90+
91+
def start(self):
92+
self.started = True
93+
94+
def stop(self):
95+
self.started = False
96+
97+
class DummyVisualizationLauncher:
98+
def __init__(self, *args, **kwargs):
99+
self.launchers = []
100+
101+
def run(self):
102+
# Simulate running the visualization launcher
103+
return
104+
105+
def terminate(self):
106+
pass
107+
108+
monkeypatch.setattr(
109+
"manager.manager.manager.LauncherVisualization", DummyVisualizationLauncher
110+
)
111+
monkeypatch.setattr("manager.manager.manager.Server", DummyServer)
112+
monkeypatch.setattr("manager.manager.manager.FileWatchdog", DummyFileWatchdog)
113+
114+
# Setup Manager with dummy consumer
115+
m = Manager(host="localhost", port=12345)
116+
m.consumer = DummyConsumer()
117+
# Move to 'connected' state first
118+
m.trigger("connect", event=None)
119+
return m
120+
121+
122+
def setup_manager_to_visualization_ready(manager):
123+
"""Move manager to visualization_ready state."""
124+
125+
# Initial state should be 'connected'
126+
assert manager.state == "connected"
127+
128+
# Use ConfigurationModel for valid world config
129+
from manager.libs.launch_world_model import ConfigurationModel
130+
131+
valid_world_cfg = ConfigurationModel(
132+
world="test_world", launch_file_path="/path/to/launch_file.launch"
133+
).model_dump()
134+
135+
event_data = {
136+
"world": valid_world_cfg,
137+
"robot": {
138+
"world": None, # No robot specified
139+
"robot_config": {"name": "test_robot", "type": "simple"},
140+
},
141+
}
142+
manager.trigger("launch_world", data=event_data)
143+
144+
# State should now be 'world_ready'
145+
assert manager.state == "world_ready"
146+
147+
# Trigger visualization ready state
148+
manager.trigger(
149+
"prepare_visualization",
150+
data={
151+
"type": "gazebo_rae",
152+
"file": "test_file",
153+
},
154+
)
155+
156+
assert manager.state == "visualization_ready"
157+
158+
159+
def test_visualization_ready_to_application_running_valid(manager, monkeypatch):
160+
"""
161+
Test transitioning from 'visualization_ready' to 'application_running' state.
162+
163+
This test verifies the state transitions in case of valid values.
164+
"""
165+
setup_manager_to_visualization_ready(manager)
166+
167+
class DummyProc:
168+
def __init__(self):
169+
self.pid = 123
170+
171+
def kill(self):
172+
pass
173+
174+
def suspend(self):
175+
pass
176+
177+
original_open = builtins.open
178+
179+
def fake_open(file, mode="r", *args, **kwargs):
180+
if file == "/workspace/code/app.zip":
181+
if "w" in mode:
182+
return io.BytesIO()
183+
elif "r" in mode:
184+
return io.BytesIO(b"fake zip content")
185+
return original_open(file, mode, *args, **kwargs)
186+
187+
# Mock file system and subprocess operations
188+
monkeypatch.setattr("os.path.isfile", lambda path: True)
189+
monkeypatch.setattr("os.listdir", lambda path: ["0", "1", "2"])
190+
monkeypatch.setattr("builtins.open", fake_open)
191+
monkeypatch.setattr("subprocess.Popen", lambda *a, **k: DummyProc())
192+
monkeypatch.setattr("os.mkdir", lambda path: None)
193+
monkeypatch.setattr("os.path.exists", lambda path: True)
194+
monkeypatch.setattr("shutil.rmtree", lambda path: None)
195+
monkeypatch.setattr(
196+
"zipfile.ZipFile",
197+
lambda *a, **k: type(
198+
"Zip",
199+
(),
200+
{"extractall": lambda self, path: None, "close": lambda self: None},
201+
)(),
202+
)
203+
monkeypatch.setattr("base64.b64decode", lambda s: b"print('hello')")
204+
monkeypatch.setattr(
205+
"manager.manager.manager.Manager.unpause_sim", lambda self: None
206+
)
207+
# Mock linter to return no errors
208+
manager.linter.evaluate_code = lambda code, ros_version: ""
209+
# Trigger application running state
210+
manager.trigger(
211+
"run_application",
212+
data={"type": "robotics-academy", "code": "data:base64,ZmFrZV9jb2Rl"},
213+
)
214+
# Assert state is now application_running
215+
assert manager.state == "application_running"
216+
217+
218+
def test_on_run_application_missing_code(manager, monkeypatch):
219+
"""Test running application with missing code file."""
220+
setup_manager_to_visualization_ready(manager)
221+
222+
# Mock file system so code file is missing
223+
monkeypatch.setattr("os.path.isfile", lambda path: False)
224+
# Mock open for app.zip to avoid FileNotFoundError
225+
original_open = builtins.open
226+
227+
def fake_open(file, mode="r", *args, **kwargs):
228+
if file == "/workspace/code/app.zip":
229+
import io
230+
231+
return io.BytesIO()
232+
return original_open(file, mode, *args, **kwargs)
233+
234+
monkeypatch.setattr("builtins.open", fake_open)
235+
# Mock other unimportant operations
236+
monkeypatch.setattr("os.listdir", lambda path: ["0", "1", "2"])
237+
monkeypatch.setattr("subprocess.Popen", lambda *a, **k: None)
238+
monkeypatch.setattr("os.mkdir", lambda path: None)
239+
monkeypatch.setattr("os.path.exists", lambda path: True)
240+
monkeypatch.setattr("shutil.rmtree", lambda path: None)
241+
monkeypatch.setattr(
242+
"zipfile.ZipFile",
243+
lambda *a, **k: type(
244+
"Zip",
245+
(),
246+
{"extractall": lambda self, path: None, "close": lambda self: None},
247+
)(),
248+
)
249+
monkeypatch.setattr("base64.b64decode", lambda s: b"print('hello')")
250+
monkeypatch.setattr(
251+
"manager.manager.manager.Manager.unpause_sim", lambda self: None
252+
)
253+
# Mock linter to return no errors
254+
manager.linter.evaluate_code = lambda code, ros_version: ""
255+
# Prep data
256+
data = {"type": "robotics-academy", "code": "data:base64,ZmFrZV9jb2Rl"}
257+
# Trigger run_application with missing code
258+
with pytest.raises(Exception, match="User code not found"):
259+
manager.trigger("run_application", data=data)
260+
assert manager.application_process is None
261+
# Ensure state is still visualization_ready
262+
assert manager.state == "visualization_ready"
263+
264+
265+
def test_on_run_application_corrupt_zip(manager, monkeypatch):
266+
"""Test running application with corrupt zip/base64."""
267+
setup_manager_to_visualization_ready(manager)
268+
269+
# Mock file system so code dir exists
270+
monkeypatch.setattr("os.path.isfile", lambda path: True)
271+
monkeypatch.setattr("os.path.exists", lambda path: True)
272+
monkeypatch.setattr("os.mkdir", lambda path: None)
273+
monkeypatch.setattr("os.listdir", lambda path: ["0", "1", "2"])
274+
monkeypatch.setattr("shutil.rmtree", lambda path: None)
275+
# Mock open for app.zip to avoid FileNotFoundError
276+
original_open = builtins.open
277+
278+
def fake_open(file, mode="r", *args, **kwargs):
279+
if file == "/workspace/code/app.zip":
280+
import io
281+
282+
return io.BytesIO()
283+
return original_open(file, mode, *args, **kwargs)
284+
285+
monkeypatch.setattr("builtins.open", fake_open)
286+
# Simulate corrupt base64 decoding
287+
monkeypatch.setattr(
288+
"base64.b64decode", lambda s: (_ for _ in ()).throw(Exception("Corrupt base64"))
289+
)
290+
# Mock other unimportant operations
291+
monkeypatch.setattr(
292+
"zipfile.ZipFile",
293+
lambda *a, **k: type(
294+
"Zip",
295+
(),
296+
{"extractall": lambda self, path: None, "close": lambda self: None},
297+
)(),
298+
)
299+
monkeypatch.setattr(
300+
"manager.manager.manager.Manager.unpause_sim", lambda self: None
301+
)
302+
manager.linter.evaluate_code = lambda code, ros_version: ""
303+
data = {"type": "robotics-academy", "code": "data:base64,ZmFrZV9jb2Rl"}
304+
with pytest.raises(Exception, match="Corrupt base64"):
305+
manager.trigger("run_application", data=data)
306+
assert manager.application_process is None
307+
assert manager.state == "visualization_ready"

0 commit comments

Comments
 (0)