host.camera_visualizer.tests.test_routes
Tests for camera visualizer route modules and field cache.
These test the Python route logic directly without starting an HTTP server. Run with: pytest host/camera_visualizer/tests/ -v
Generated by Claude.
1""" 2Tests for camera visualizer route modules and field cache. 3 4These test the Python route logic directly without starting an HTTP server. 5Run with: pytest host/camera_visualizer/tests/ -v 6 7Generated by Claude. 8""" 9 10import json 11import os 12import tempfile 13import zipfile 14 15# Ensure project root is on path 16import sys 17sys.path.insert(0, os.path.abspath( 18 os.path.join(os.path.dirname(__file__), '..', '..', '..') 19)) 20 21from host.camera_visualizer.routes import config, fields, cad, points 22from host.camera_visualizer.field_cache import FieldCache 23 24 25# ── Config routes ───────────────────────────────────────────────────── 26 27class TestConfigRoutes: 28 def test_config_json_has_required_keys(self): 29 cfg = json.loads(config.build_config_json()) 30 assert 'robot_frame' in cfg 31 assert 'cameras' in cfg 32 assert 'mechanisms' in cfg 33 34 def test_robot_frame_has_dimensions(self): 35 cfg = json.loads(config.build_config_json()) 36 frame = cfg['robot_frame'] 37 assert 'width_inches' in frame 38 assert 'length_inches' in frame 39 assert 'width_meters' in frame 40 assert 'length_meters' in frame 41 assert frame['width_meters'] > 0 42 assert frame['length_meters'] > 0 43 44 def test_cameras_have_required_fields(self): 45 cfg = json.loads(config.build_config_json()) 46 for cam in cfg['cameras']: 47 for key in ['name', 'x_inches', 'y_inches', 'z_inches', 48 'yaw_deg', 'pitch_deg', 'fov_deg']: 49 assert key in cam, f"Missing '{key}' in camera {cam.get('name')}" 50 51 def test_mechanisms_have_required_fields(self): 52 cfg = json.loads(config.build_config_json()) 53 for mech in cfg['mechanisms']: 54 for key in ['name', 'type', 'x_meters', 'y_meters', 'z_meters']: 55 assert key in mech, f"Missing '{key}' in mechanism {mech.get('name')}" 56 57 def test_build_page_injects_config(self): 58 here = os.path.join( 59 os.path.dirname(__file__), '..' 60 ) 61 page = config.build_page(here) 62 assert b'window.__ROBOT_CONFIG__' in page 63 assert b'robot_frame' in page 64 assert b'<html' in page 65 66 67# ── Field tag routes ────────────────────────────────────────────────── 68 69class TestFieldTagRoutes: 70 def test_field_tags_returns_tags(self): 71 result = fields.get_field_tags() 72 assert 'tags' in result 73 assert len(result['tags']) > 0 74 75 def test_field_tags_have_positions(self): 76 result = fields.get_field_tags() 77 for tag in result['tags']: 78 assert 'id' in tag 79 assert 'x' in tag 80 assert 'y' in tag 81 assert 'z' in tag 82 assert isinstance(tag['id'], int) 83 assert tag['z'] > 0 # all tags are above ground 84 85 def test_field_tags_have_rotations(self): 86 result = fields.get_field_tags() 87 for tag in result['tags']: 88 assert 'yaw_deg' in tag 89 90 def test_field_dimensions(self): 91 result = fields.get_field_tags() 92 assert 'field_length' in result 93 assert 'field_width' in result 94 # Standard FRC field is ~16.5m x ~8m 95 assert 15 < result['field_length'] < 18 96 assert 7 < result['field_width'] < 9 97 98 def test_tag_count_2026(self): 99 """2026 field should have 32 tags.""" 100 result = fields.get_field_tags() 101 assert len(result['tags']) == 32 102 103 104# ── CAD routes ──────────────────────────────────────────────────────── 105 106class TestCadRoutes: 107 def test_list_cad_models_with_real_dir(self): 108 cad_dir = os.path.join( 109 os.path.dirname(__file__), '..', '..', 'cad_tools', 'models' 110 ) 111 models = cad.list_cad_models(cad_dir) 112 # May or may not have models, but should be a list 113 assert isinstance(models, list) 114 for m in models: 115 assert 'name' in m 116 assert 'url' in m 117 assert 'size' in m 118 119 def test_list_cad_models_empty_dir(self): 120 with tempfile.TemporaryDirectory() as tmpdir: 121 models = cad.list_cad_models(tmpdir) 122 assert models == [] 123 124 def test_list_cad_models_nonexistent_dir(self): 125 models = cad.list_cad_models('/nonexistent/path') 126 assert models == [] 127 128 def test_serve_cad_file_rejects_bad_extension(self): 129 with tempfile.TemporaryDirectory() as tmpdir: 130 # Create a .py file that should be rejected 131 with open(os.path.join(tmpdir, 'evil.py'), 'w') as f: 132 f.write('import os') 133 data, mime = cad.serve_cad_file(tmpdir, 'evil.py') 134 assert data is None 135 136 def test_serve_cad_file_rejects_path_traversal(self): 137 with tempfile.TemporaryDirectory() as tmpdir: 138 data, mime = cad.serve_cad_file(tmpdir, '../../../etc/passwd') 139 assert data is None 140 141 def test_serve_cad_file_missing(self): 142 with tempfile.TemporaryDirectory() as tmpdir: 143 data, mime = cad.serve_cad_file(tmpdir, 'nonexistent.glb') 144 assert data is None 145 146 def test_serve_cad_file_glb(self): 147 with tempfile.TemporaryDirectory() as tmpdir: 148 test_data = b'\x00\x01\x02\x03' 149 with open(os.path.join(tmpdir, 'test.glb'), 'wb') as f: 150 f.write(test_data) 151 data, mime = cad.serve_cad_file(tmpdir, 'test.glb') 152 assert data == test_data 153 assert mime == 'model/gltf-binary' 154 155 def test_serve_cad_file_gltf(self): 156 with tempfile.TemporaryDirectory() as tmpdir: 157 test_data = b'{"asset":{}}' 158 with open(os.path.join(tmpdir, 'test.gltf'), 'wb') as f: 159 f.write(test_data) 160 data, mime = cad.serve_cad_file(tmpdir, 'test.gltf') 161 assert data == test_data 162 assert mime == 'model/gltf+json' 163 164 165# ── Points routes ───────────────────────────────────────────────────── 166 167class TestPointRoutes: 168 def test_load_points_empty(self): 169 result = points.load_points('/nonexistent/dir', 'default') 170 data = json.loads(result) 171 assert data['points'] == [] 172 173 def test_save_and_load_roundtrip(self): 174 with tempfile.TemporaryDirectory() as tmpdir: 175 test_data = {"name": "test", "points": [ 176 {"id": 1, "name": "test", "x": 1.5, "y": 2.5, "z": 0.5} 177 ]} 178 points.save_points(tmpdir, 'myset', json.dumps(test_data).encode()) 179 loaded = json.loads(points.load_points(tmpdir, 'myset')) 180 assert loaded == test_data 181 182 def test_save_creates_directories(self): 183 with tempfile.TemporaryDirectory() as tmpdir: 184 pts_dir = os.path.join(tmpdir, 'sub', 'points') 185 points.save_points(pts_dir, 'test', b'{}') 186 assert os.path.isdir(pts_dir) 187 188 def test_list_point_sets_empty(self): 189 result = points.list_point_sets('/nonexistent/dir') 190 assert result == [] 191 192 def test_list_point_sets(self): 193 with tempfile.TemporaryDirectory() as tmpdir: 194 points.save_points(tmpdir, 'set_a', 195 json.dumps({"name": "Set A", "points": [{"id": 1}]}).encode()) 196 points.save_points(tmpdir, 'set_b', 197 json.dumps({"name": "Set B", "points": []}).encode()) 198 sets = points.list_point_sets(tmpdir) 199 assert len(sets) == 2 200 names = [s['name'] for s in sets] 201 assert 'Set A' in names 202 assert 'Set B' in names 203 # Check count 204 a = next(s for s in sets if s['name'] == 'Set A') 205 assert a['count'] == 1 206 207 208# ── Field cache ─────────────────────────────────────────────────────── 209 210class TestFieldCache: 211 def test_field_list_not_empty(self): 212 cache = FieldCache('/tmp/test') 213 fields = cache.get_field_list() 214 assert len(fields) >= 4 215 216 def test_field_list_has_robot_view(self): 217 cache = FieldCache('/tmp/test') 218 ids = [f['id'] for f in cache.get_field_list()] 219 assert 'robot-view' in ids 220 221 def test_field_list_has_2026(self): 222 cache = FieldCache('/tmp/test') 223 ids = [f['id'] for f in cache.get_field_list()] 224 assert '2026-field' in ids 225 226 def test_field_list_has_evergreen(self): 227 cache = FieldCache('/tmp/test') 228 ids = [f['id'] for f in cache.get_field_list()] 229 assert 'evergreen' in ids 230 231 def test_field_list_has_cat_box(self): 232 cache = FieldCache('/tmp/test') 233 ids = [f['id'] for f in cache.get_field_list()] 234 assert 'cat-box' in ids 235 236 def test_extract_evergreen_returns_none(self): 237 """Evergreen is builtin, no zip entry.""" 238 cache = FieldCache('/tmp/test') 239 config, glb = cache.extract_field('evergreen') 240 assert config is None 241 242 def test_get_field_info_valid(self): 243 cache = FieldCache('/tmp/test') 244 info = cache.get_field_info('2026-field') 245 assert info is not None 246 assert info['name'] == '2026 Field' 247 assert 'zipEntry' in info 248 249 def test_get_field_info_invalid(self): 250 cache = FieldCache('/tmp/test') 251 assert cache.get_field_info('nonexistent') is None 252 253 def test_extract_builtin_returns_none(self): 254 """Builtin scenes (robot-view, cat-box) have no zip entry.""" 255 cache = FieldCache('/tmp/test') 256 config, glb = cache.extract_field('robot-view') 257 assert config is None 258 assert glb is None 259 260 def test_extract_field_from_test_bundle(self): 261 """Create a minimal test bundle and verify extraction.""" 262 with tempfile.TemporaryDirectory() as tmpdir: 263 cache_dir = os.path.join(tmpdir, 'fields') 264 bundle_path = os.path.join(tmpdir, 'bundle.zip') 265 266 # Build a fake inner zip with config.json and model.glb 267 inner_buf = _make_test_field_zip( 268 config={"name": "Test Field", "widthInches": 651}, 269 glb_data=b'fake-glb-data', 270 ) 271 272 # Build outer bundle containing the inner zip 273 with zipfile.ZipFile(bundle_path, 'w') as outer: 274 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 275 276 cache = FieldCache(cache_dir, bundle_path) 277 cfg, glb = cache.extract_field('2026-field') 278 279 assert cfg is not None 280 assert cfg['name'] == 'Test Field' 281 assert glb == b'fake-glb-data' 282 283 def test_extract_field_caches_locally(self): 284 """Second extraction should read from local cache, not bundle.""" 285 with tempfile.TemporaryDirectory() as tmpdir: 286 cache_dir = os.path.join(tmpdir, 'fields') 287 bundle_path = os.path.join(tmpdir, 'bundle.zip') 288 289 inner_buf = _make_test_field_zip( 290 config={"name": "Cached"}, 291 glb_data=b'glb-bytes', 292 ) 293 with zipfile.ZipFile(bundle_path, 'w') as outer: 294 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 295 296 cache = FieldCache(cache_dir, bundle_path) 297 cache.extract_field('2026-field') 298 299 # Delete the bundle — cache should still work 300 os.remove(bundle_path) 301 302 cfg, glb = cache.extract_field('2026-field') 303 assert cfg['name'] == 'Cached' 304 assert glb == b'glb-bytes' 305 306 307def _make_test_field_zip(config: dict, glb_data: bytes) -> bytes: 308 """Create a minimal field zip (config.json + model.glb) in memory.""" 309 import io 310 buf = io.BytesIO() 311 with zipfile.ZipFile(buf, 'w') as z: 312 z.writestr('config.json', json.dumps(config)) 313 z.writestr('model.glb', glb_data) 314 return buf.getvalue()
class
TestConfigRoutes:
28class TestConfigRoutes: 29 def test_config_json_has_required_keys(self): 30 cfg = json.loads(config.build_config_json()) 31 assert 'robot_frame' in cfg 32 assert 'cameras' in cfg 33 assert 'mechanisms' in cfg 34 35 def test_robot_frame_has_dimensions(self): 36 cfg = json.loads(config.build_config_json()) 37 frame = cfg['robot_frame'] 38 assert 'width_inches' in frame 39 assert 'length_inches' in frame 40 assert 'width_meters' in frame 41 assert 'length_meters' in frame 42 assert frame['width_meters'] > 0 43 assert frame['length_meters'] > 0 44 45 def test_cameras_have_required_fields(self): 46 cfg = json.loads(config.build_config_json()) 47 for cam in cfg['cameras']: 48 for key in ['name', 'x_inches', 'y_inches', 'z_inches', 49 'yaw_deg', 'pitch_deg', 'fov_deg']: 50 assert key in cam, f"Missing '{key}' in camera {cam.get('name')}" 51 52 def test_mechanisms_have_required_fields(self): 53 cfg = json.loads(config.build_config_json()) 54 for mech in cfg['mechanisms']: 55 for key in ['name', 'type', 'x_meters', 'y_meters', 'z_meters']: 56 assert key in mech, f"Missing '{key}' in mechanism {mech.get('name')}" 57 58 def test_build_page_injects_config(self): 59 here = os.path.join( 60 os.path.dirname(__file__), '..' 61 ) 62 page = config.build_page(here) 63 assert b'window.__ROBOT_CONFIG__' in page 64 assert b'robot_frame' in page 65 assert b'<html' in page
def
test_robot_frame_has_dimensions(self):
35 def test_robot_frame_has_dimensions(self): 36 cfg = json.loads(config.build_config_json()) 37 frame = cfg['robot_frame'] 38 assert 'width_inches' in frame 39 assert 'length_inches' in frame 40 assert 'width_meters' in frame 41 assert 'length_meters' in frame 42 assert frame['width_meters'] > 0 43 assert frame['length_meters'] > 0
class
TestFieldTagRoutes:
70class TestFieldTagRoutes: 71 def test_field_tags_returns_tags(self): 72 result = fields.get_field_tags() 73 assert 'tags' in result 74 assert len(result['tags']) > 0 75 76 def test_field_tags_have_positions(self): 77 result = fields.get_field_tags() 78 for tag in result['tags']: 79 assert 'id' in tag 80 assert 'x' in tag 81 assert 'y' in tag 82 assert 'z' in tag 83 assert isinstance(tag['id'], int) 84 assert tag['z'] > 0 # all tags are above ground 85 86 def test_field_tags_have_rotations(self): 87 result = fields.get_field_tags() 88 for tag in result['tags']: 89 assert 'yaw_deg' in tag 90 91 def test_field_dimensions(self): 92 result = fields.get_field_tags() 93 assert 'field_length' in result 94 assert 'field_width' in result 95 # Standard FRC field is ~16.5m x ~8m 96 assert 15 < result['field_length'] < 18 97 assert 7 < result['field_width'] < 9 98 99 def test_tag_count_2026(self): 100 """2026 field should have 32 tags.""" 101 result = fields.get_field_tags() 102 assert len(result['tags']) == 32
class
TestCadRoutes:
107class TestCadRoutes: 108 def test_list_cad_models_with_real_dir(self): 109 cad_dir = os.path.join( 110 os.path.dirname(__file__), '..', '..', 'cad_tools', 'models' 111 ) 112 models = cad.list_cad_models(cad_dir) 113 # May or may not have models, but should be a list 114 assert isinstance(models, list) 115 for m in models: 116 assert 'name' in m 117 assert 'url' in m 118 assert 'size' in m 119 120 def test_list_cad_models_empty_dir(self): 121 with tempfile.TemporaryDirectory() as tmpdir: 122 models = cad.list_cad_models(tmpdir) 123 assert models == [] 124 125 def test_list_cad_models_nonexistent_dir(self): 126 models = cad.list_cad_models('/nonexistent/path') 127 assert models == [] 128 129 def test_serve_cad_file_rejects_bad_extension(self): 130 with tempfile.TemporaryDirectory() as tmpdir: 131 # Create a .py file that should be rejected 132 with open(os.path.join(tmpdir, 'evil.py'), 'w') as f: 133 f.write('import os') 134 data, mime = cad.serve_cad_file(tmpdir, 'evil.py') 135 assert data is None 136 137 def test_serve_cad_file_rejects_path_traversal(self): 138 with tempfile.TemporaryDirectory() as tmpdir: 139 data, mime = cad.serve_cad_file(tmpdir, '../../../etc/passwd') 140 assert data is None 141 142 def test_serve_cad_file_missing(self): 143 with tempfile.TemporaryDirectory() as tmpdir: 144 data, mime = cad.serve_cad_file(tmpdir, 'nonexistent.glb') 145 assert data is None 146 147 def test_serve_cad_file_glb(self): 148 with tempfile.TemporaryDirectory() as tmpdir: 149 test_data = b'\x00\x01\x02\x03' 150 with open(os.path.join(tmpdir, 'test.glb'), 'wb') as f: 151 f.write(test_data) 152 data, mime = cad.serve_cad_file(tmpdir, 'test.glb') 153 assert data == test_data 154 assert mime == 'model/gltf-binary' 155 156 def test_serve_cad_file_gltf(self): 157 with tempfile.TemporaryDirectory() as tmpdir: 158 test_data = b'{"asset":{}}' 159 with open(os.path.join(tmpdir, 'test.gltf'), 'wb') as f: 160 f.write(test_data) 161 data, mime = cad.serve_cad_file(tmpdir, 'test.gltf') 162 assert data == test_data 163 assert mime == 'model/gltf+json'
def
test_list_cad_models_with_real_dir(self):
108 def test_list_cad_models_with_real_dir(self): 109 cad_dir = os.path.join( 110 os.path.dirname(__file__), '..', '..', 'cad_tools', 'models' 111 ) 112 models = cad.list_cad_models(cad_dir) 113 # May or may not have models, but should be a list 114 assert isinstance(models, list) 115 for m in models: 116 assert 'name' in m 117 assert 'url' in m 118 assert 'size' in m
def
test_serve_cad_file_rejects_bad_extension(self):
129 def test_serve_cad_file_rejects_bad_extension(self): 130 with tempfile.TemporaryDirectory() as tmpdir: 131 # Create a .py file that should be rejected 132 with open(os.path.join(tmpdir, 'evil.py'), 'w') as f: 133 f.write('import os') 134 data, mime = cad.serve_cad_file(tmpdir, 'evil.py') 135 assert data is None
def
test_serve_cad_file_glb(self):
147 def test_serve_cad_file_glb(self): 148 with tempfile.TemporaryDirectory() as tmpdir: 149 test_data = b'\x00\x01\x02\x03' 150 with open(os.path.join(tmpdir, 'test.glb'), 'wb') as f: 151 f.write(test_data) 152 data, mime = cad.serve_cad_file(tmpdir, 'test.glb') 153 assert data == test_data 154 assert mime == 'model/gltf-binary'
def
test_serve_cad_file_gltf(self):
156 def test_serve_cad_file_gltf(self): 157 with tempfile.TemporaryDirectory() as tmpdir: 158 test_data = b'{"asset":{}}' 159 with open(os.path.join(tmpdir, 'test.gltf'), 'wb') as f: 160 f.write(test_data) 161 data, mime = cad.serve_cad_file(tmpdir, 'test.gltf') 162 assert data == test_data 163 assert mime == 'model/gltf+json'
class
TestPointRoutes:
168class TestPointRoutes: 169 def test_load_points_empty(self): 170 result = points.load_points('/nonexistent/dir', 'default') 171 data = json.loads(result) 172 assert data['points'] == [] 173 174 def test_save_and_load_roundtrip(self): 175 with tempfile.TemporaryDirectory() as tmpdir: 176 test_data = {"name": "test", "points": [ 177 {"id": 1, "name": "test", "x": 1.5, "y": 2.5, "z": 0.5} 178 ]} 179 points.save_points(tmpdir, 'myset', json.dumps(test_data).encode()) 180 loaded = json.loads(points.load_points(tmpdir, 'myset')) 181 assert loaded == test_data 182 183 def test_save_creates_directories(self): 184 with tempfile.TemporaryDirectory() as tmpdir: 185 pts_dir = os.path.join(tmpdir, 'sub', 'points') 186 points.save_points(pts_dir, 'test', b'{}') 187 assert os.path.isdir(pts_dir) 188 189 def test_list_point_sets_empty(self): 190 result = points.list_point_sets('/nonexistent/dir') 191 assert result == [] 192 193 def test_list_point_sets(self): 194 with tempfile.TemporaryDirectory() as tmpdir: 195 points.save_points(tmpdir, 'set_a', 196 json.dumps({"name": "Set A", "points": [{"id": 1}]}).encode()) 197 points.save_points(tmpdir, 'set_b', 198 json.dumps({"name": "Set B", "points": []}).encode()) 199 sets = points.list_point_sets(tmpdir) 200 assert len(sets) == 2 201 names = [s['name'] for s in sets] 202 assert 'Set A' in names 203 assert 'Set B' in names 204 # Check count 205 a = next(s for s in sets if s['name'] == 'Set A') 206 assert a['count'] == 1
def
test_save_and_load_roundtrip(self):
174 def test_save_and_load_roundtrip(self): 175 with tempfile.TemporaryDirectory() as tmpdir: 176 test_data = {"name": "test", "points": [ 177 {"id": 1, "name": "test", "x": 1.5, "y": 2.5, "z": 0.5} 178 ]} 179 points.save_points(tmpdir, 'myset', json.dumps(test_data).encode()) 180 loaded = json.loads(points.load_points(tmpdir, 'myset')) 181 assert loaded == test_data
def
test_list_point_sets(self):
193 def test_list_point_sets(self): 194 with tempfile.TemporaryDirectory() as tmpdir: 195 points.save_points(tmpdir, 'set_a', 196 json.dumps({"name": "Set A", "points": [{"id": 1}]}).encode()) 197 points.save_points(tmpdir, 'set_b', 198 json.dumps({"name": "Set B", "points": []}).encode()) 199 sets = points.list_point_sets(tmpdir) 200 assert len(sets) == 2 201 names = [s['name'] for s in sets] 202 assert 'Set A' in names 203 assert 'Set B' in names 204 # Check count 205 a = next(s for s in sets if s['name'] == 'Set A') 206 assert a['count'] == 1
class
TestFieldCache:
211class TestFieldCache: 212 def test_field_list_not_empty(self): 213 cache = FieldCache('/tmp/test') 214 fields = cache.get_field_list() 215 assert len(fields) >= 4 216 217 def test_field_list_has_robot_view(self): 218 cache = FieldCache('/tmp/test') 219 ids = [f['id'] for f in cache.get_field_list()] 220 assert 'robot-view' in ids 221 222 def test_field_list_has_2026(self): 223 cache = FieldCache('/tmp/test') 224 ids = [f['id'] for f in cache.get_field_list()] 225 assert '2026-field' in ids 226 227 def test_field_list_has_evergreen(self): 228 cache = FieldCache('/tmp/test') 229 ids = [f['id'] for f in cache.get_field_list()] 230 assert 'evergreen' in ids 231 232 def test_field_list_has_cat_box(self): 233 cache = FieldCache('/tmp/test') 234 ids = [f['id'] for f in cache.get_field_list()] 235 assert 'cat-box' in ids 236 237 def test_extract_evergreen_returns_none(self): 238 """Evergreen is builtin, no zip entry.""" 239 cache = FieldCache('/tmp/test') 240 config, glb = cache.extract_field('evergreen') 241 assert config is None 242 243 def test_get_field_info_valid(self): 244 cache = FieldCache('/tmp/test') 245 info = cache.get_field_info('2026-field') 246 assert info is not None 247 assert info['name'] == '2026 Field' 248 assert 'zipEntry' in info 249 250 def test_get_field_info_invalid(self): 251 cache = FieldCache('/tmp/test') 252 assert cache.get_field_info('nonexistent') is None 253 254 def test_extract_builtin_returns_none(self): 255 """Builtin scenes (robot-view, cat-box) have no zip entry.""" 256 cache = FieldCache('/tmp/test') 257 config, glb = cache.extract_field('robot-view') 258 assert config is None 259 assert glb is None 260 261 def test_extract_field_from_test_bundle(self): 262 """Create a minimal test bundle and verify extraction.""" 263 with tempfile.TemporaryDirectory() as tmpdir: 264 cache_dir = os.path.join(tmpdir, 'fields') 265 bundle_path = os.path.join(tmpdir, 'bundle.zip') 266 267 # Build a fake inner zip with config.json and model.glb 268 inner_buf = _make_test_field_zip( 269 config={"name": "Test Field", "widthInches": 651}, 270 glb_data=b'fake-glb-data', 271 ) 272 273 # Build outer bundle containing the inner zip 274 with zipfile.ZipFile(bundle_path, 'w') as outer: 275 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 276 277 cache = FieldCache(cache_dir, bundle_path) 278 cfg, glb = cache.extract_field('2026-field') 279 280 assert cfg is not None 281 assert cfg['name'] == 'Test Field' 282 assert glb == b'fake-glb-data' 283 284 def test_extract_field_caches_locally(self): 285 """Second extraction should read from local cache, not bundle.""" 286 with tempfile.TemporaryDirectory() as tmpdir: 287 cache_dir = os.path.join(tmpdir, 'fields') 288 bundle_path = os.path.join(tmpdir, 'bundle.zip') 289 290 inner_buf = _make_test_field_zip( 291 config={"name": "Cached"}, 292 glb_data=b'glb-bytes', 293 ) 294 with zipfile.ZipFile(bundle_path, 'w') as outer: 295 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 296 297 cache = FieldCache(cache_dir, bundle_path) 298 cache.extract_field('2026-field') 299 300 # Delete the bundle — cache should still work 301 os.remove(bundle_path) 302 303 cfg, glb = cache.extract_field('2026-field') 304 assert cfg['name'] == 'Cached' 305 assert glb == b'glb-bytes'
def
test_extract_evergreen_returns_none(self):
237 def test_extract_evergreen_returns_none(self): 238 """Evergreen is builtin, no zip entry.""" 239 cache = FieldCache('/tmp/test') 240 config, glb = cache.extract_field('evergreen') 241 assert config is None
Evergreen is builtin, no zip entry.
def
test_extract_builtin_returns_none(self):
254 def test_extract_builtin_returns_none(self): 255 """Builtin scenes (robot-view, cat-box) have no zip entry.""" 256 cache = FieldCache('/tmp/test') 257 config, glb = cache.extract_field('robot-view') 258 assert config is None 259 assert glb is None
Builtin scenes (robot-view, cat-box) have no zip entry.
def
test_extract_field_from_test_bundle(self):
261 def test_extract_field_from_test_bundle(self): 262 """Create a minimal test bundle and verify extraction.""" 263 with tempfile.TemporaryDirectory() as tmpdir: 264 cache_dir = os.path.join(tmpdir, 'fields') 265 bundle_path = os.path.join(tmpdir, 'bundle.zip') 266 267 # Build a fake inner zip with config.json and model.glb 268 inner_buf = _make_test_field_zip( 269 config={"name": "Test Field", "widthInches": 651}, 270 glb_data=b'fake-glb-data', 271 ) 272 273 # Build outer bundle containing the inner zip 274 with zipfile.ZipFile(bundle_path, 'w') as outer: 275 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 276 277 cache = FieldCache(cache_dir, bundle_path) 278 cfg, glb = cache.extract_field('2026-field') 279 280 assert cfg is not None 281 assert cfg['name'] == 'Test Field' 282 assert glb == b'fake-glb-data'
Create a minimal test bundle and verify extraction.
def
test_extract_field_caches_locally(self):
284 def test_extract_field_caches_locally(self): 285 """Second extraction should read from local cache, not bundle.""" 286 with tempfile.TemporaryDirectory() as tmpdir: 287 cache_dir = os.path.join(tmpdir, 'fields') 288 bundle_path = os.path.join(tmpdir, 'bundle.zip') 289 290 inner_buf = _make_test_field_zip( 291 config={"name": "Cached"}, 292 glb_data=b'glb-bytes', 293 ) 294 with zipfile.ZipFile(bundle_path, 'w') as outer: 295 outer.writestr('Field3d_2026FRCFieldV1.zip', inner_buf) 296 297 cache = FieldCache(cache_dir, bundle_path) 298 cache.extract_field('2026-field') 299 300 # Delete the bundle — cache should still work 301 os.remove(bundle_path) 302 303 cfg, glb = cache.extract_field('2026-field') 304 assert cfg['name'] == 'Cached' 305 assert glb == b'glb-bytes'
Second extraction should read from local cache, not bundle.