import unittest import os import sys import types from io import BytesIO from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import ui_server from ui_server import HTML, INPUT_EXTENSIONS, MAX_JSON_BODY_BYTES, active_video_name, archive_files, archive_path, camera_bridge_source, cleanup_empty_recordings, content_disposition, copy_exact, h264_cache_path, input_upload_path, latest_file, model_upload_path, normalize_control, parse_perf, parse_quality, parse_range_header, parse_source, read_json_file, resolve_model_path, tail_text, udp_probe_path class UIServerTests(unittest.TestCase): def test_camera_bridge_source_contains_requested_capture_mode(self): source = camera_bridge_source( "http://host.docker.internal:8091/stream.mjpg", 0, 1920, 1080, 30, ) self.assertIn("index=0", source) self.assertIn("width=1920", source) self.assertIn("height=1080", source) self.assertIn("fps=30", source) def test_html_has_separate_settings_and_log_drawer(self): self.assertIn('id="settingsTab"', HTML) self.assertIn('data-tab="settings"', HTML) self.assertIn('id="logDrawer"', HTML) self.assertIn('value="guidance_v1"', HTML) self.assertIn('id="errorObjectId"', HTML) self.assertIn('id="errorHost"', HTML) self.assertIn('placeholder="192.168.1.10"', HTML) self.assertIn('src = \'/stream.mjpg?t=\'', HTML) self.assertIn('if (streamRunning && !wasRunning) reconnectFrame();', HTML) self.assertIn('', HTML) self.assertIn('', HTML) self.assertIn('id="packetConstructorFields"', HTML) self.assertIn('id="packetByteMap"', HTML) self.assertIn('id="packetFieldList"', HTML) self.assertIn('id="addPacketField"', HTML) self.assertIn('data-action="remove"', HTML) self.assertIn('id="packetHeaderSize"', HTML) self.assertIn("postJson('/api/udp-probe'", HTML) self.assertIn('Адрес привязки (обычно 0.0.0.0)', HTML) def test_custom_udp_packet_schema_is_normalized(self): state = normalize_control({ "source_mode": "udp_custom_live", "packet_preset": "custom", "packet_schema": { "assembly": "datagram", "header_size": 12, "flags_offset": 99, "start_mask": "0x80", }, }) self.assertEqual(state["packet_preset"], "custom") self.assertEqual(state["packet_schema"]["assembly"], "datagram") self.assertEqual(state["packet_schema"]["header_size"], 12) self.assertEqual(state["packet_schema"]["flags_offset"], -1) self.assertEqual(state["packet_schema"]["start_mask"], 0x80) def test_visual_packet_layout_controls_parser_offsets(self): state = normalize_control({ "source_mode": "udp_custom_live", "packet_preset": "custom", "packet_layout": [ {"role": "skip", "size": 2, "label": "magic"}, {"role": "field", "size": 2, "label": "temperature"}, {"role": "flags", "size": 1, "label": "flags"}, {"role": "sequence", "size": 3, "label": "frame"}, ], }) self.assertEqual(state["packet_schema"]["header_size"], 8) self.assertEqual(state["packet_schema"]["flags_offset"], 4) self.assertEqual(state["packet_schema"]["sequence_offset"], 5) self.assertEqual(state["packet_schema"]["sequence_size"], 3) self.assertEqual( state["packet_schema"]["read_fields"], [{"name": "temperature", "offset": 2, "size": 2}], ) def test_custom_frame_size_and_fps_are_supported(self): state = normalize_control({"quality": "1536x864", "fps": 47}) self.assertEqual(state["quality"], "1536x864") self.assertEqual(state["fps"], 47) self.assertEqual(parse_quality("9000x2"), (8192, 16)) self.assertIn('id="frameWidth"', HTML) self.assertIn('id="frameHeight"', HTML) self.assertIn('id="fps" type="number"', HTML) self.assertIn('', HTML) self.assertIn('udp_raw_gray16_40404', HTML) def test_large_upload_uses_streaming_ui_with_progress(self): self.assertIn("new XMLHttpRequest()", HTML) self.assertIn('id="uploadProgress"', HTML) self.assertIn('id="cancelUpload"', HTML) self.assertNotIn('accept=".mp4,.avi,.mov,.mkv,.m4v"', HTML) self.assertNotIn("new FormData()", HTML) def test_copy_exact_streams_only_requested_bytes(self): source = BytesIO(b"abcdefgh") target = BytesIO() self.assertEqual(copy_exact(source, target, 6, chunk_size=2), 6) self.assertEqual(target.getvalue(), b"abcdef") self.assertEqual(source.read(), b"gh") def test_upload_path_accepts_extensionless_dump_and_rejects_traversal(self): self.assertEqual(input_upload_path("1785156788883112336").name, "1785156788883112336") self.assertEqual(input_upload_path("camera.vendor-format").name, "camera.vendor-format") self.assertIsNone(input_upload_path("../dump")) self.assertIsNone(input_upload_path(r"..\dump")) def test_input_list_keeps_unknown_video_and_treats_transport_stream_as_video(self): from tempfile import TemporaryDirectory old_input = ui_server.INPUT_DIR try: with TemporaryDirectory() as tmp: root = Path(tmp) (root / "clip.ts").write_bytes(b"ts") (root / "clip.vendor").write_bytes(b"video") (root / "extensionless-video").write_bytes(b"\x1aE\xdf\xa3video") mik_header = ( (59004).to_bytes(2, "little") + (8).to_bytes(2, "little") + bytes((0, 2, 7, 0)) + (256).to_bytes(4, "little") ) (root / "extensionless-mik").write_bytes(mik_header) (root / "camera.udp").write_bytes(b"dump") (root / ".partial.upload").write_bytes(b"partial") ui_server.INPUT_DIR = root rows = {row["name"]: row["kind"] for row in ui_server.input_video_files()} self.assertEqual(rows["clip.ts"], "video") self.assertEqual(rows["clip.vendor"], "video") self.assertEqual(rows["extensionless-video"], "video") self.assertEqual(rows["extensionless-mik"], "udp_dump") self.assertEqual(rows["camera.udp"], "udp_dump") self.assertNotIn(".partial.upload", rows) finally: ui_server.INPUT_DIR = old_input def test_tail_text_returns_last_lines(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: log = Path(tmp) / "main.log" log.write_text("one\ntwo\nthree\n", encoding="utf-8") self.assertEqual(tail_text(log, 2), "two\nthree") def test_read_json_file_returns_empty_dict_for_missing_file(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: self.assertEqual(read_json_file(Path(tmp) / "missing.json"), {}) def test_latest_file_returns_newest_match(self): from tempfile import TemporaryDirectory from pathlib import Path import os with TemporaryDirectory() as tmp: root = Path(tmp) old = root / "out_1.mp4" new = root / "out_2.mp4" old.write_text("old", encoding="utf-8") new.write_text("new", encoding="utf-8") os.utime(old, (1, 1)) os.utime(new, (2, 2)) self.assertEqual(latest_file(root, "*.mp4"), new) def test_archive_path_rejects_path_traversal(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: root = Path(tmp) self.assertIsNone(archive_path(root, "../evil.mp4")) def test_archive_path_accepts_url_encoded_name(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: root = Path(tmp) self.assertEqual(archive_path(root, "clip%201.mp4"), root / "clip 1.mp4") def test_archive_path_rejects_symlink_outside_archive(self): from tempfile import TemporaryDirectory with TemporaryDirectory() as tmp: root = Path(tmp) / "archive" outside = Path(tmp) / "secret.mp4" root.mkdir() outside.write_bytes(b"secret") link = root / "public.mp4" try: os.symlink(outside, link) except (OSError, NotImplementedError): self.skipTest("symlinks unavailable") self.assertIsNone(archive_path(root, "public.mp4")) def test_json_body_limit_rejects_oversized_request(self): handler = ui_server.Handler.__new__(ui_server.Handler) handler.headers = {"Content-Length": str(MAX_JSON_BODY_BYTES + 1)} with self.assertRaisesRegex(ValueError, "too large"): handler.read_json_body() def test_udp_probe_path_accepts_capture_and_rejects_traversal(self): from tempfile import TemporaryDirectory with TemporaryDirectory() as tmp: root = Path(tmp) self.assertEqual(udp_probe_path(root, "capture.udp"), root / "capture.udp") self.assertEqual(udp_probe_path(root, "capture.json"), root / "capture.json") self.assertIsNone(udp_probe_path(root, "../capture.udp")) self.assertIsNone(udp_probe_path(root, "capture.mp4")) def test_archive_files_lists_mp4_metadata(self): from tempfile import TemporaryDirectory from pathlib import Path import os with TemporaryDirectory() as tmp: root = Path(tmp) video = root / "clip.mp4" video.write_bytes(b"1234") os.utime(video, (3, 3)) self.assertEqual(archive_files(root)[0]["name"], "clip.mp4") self.assertEqual(archive_files(root)[0]["size"], 4) def test_cleanup_empty_recordings_removes_only_broken_mp4(self): from tempfile import TemporaryDirectory with TemporaryDirectory() as tmp: root = Path(tmp) broken = root / "broken.mp4" valid = root / "valid.mp4" broken.write_bytes(b"x" * 44) valid.write_bytes(b"x" * 1024) self.assertEqual(cleanup_empty_recordings(root), 1) self.assertFalse(broken.exists()) self.assertTrue(valid.exists()) def test_h264_cache_path_tracks_source_version(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: root = Path(tmp) video = root / "clip.mp4" video.write_bytes(b"1234") cache = h264_cache_path(video, root / ".downloads") self.assertEqual(cache.parent, root / ".downloads") self.assertTrue(cache.name.endswith(".h264.mp4")) self.assertIn("clip", cache.name) self.assertIn("4", cache.name) def test_content_disposition_keeps_utf8_filename(self): header = content_disposition("тест.mp4") self.assertIn("attachment;", header) self.assertIn("filename*=", header) self.assertIn("%D1%82%D0%B5%D1%81%D1%82.mp4", header) def test_active_video_name_uses_marker_only_when_file_exists(self): from tempfile import TemporaryDirectory from pathlib import Path with TemporaryDirectory() as tmp: root = Path(tmp) marker = root / ".active_video" marker.write_text("clip.mp4", encoding="utf-8") self.assertIsNone(active_video_name(root, marker)) (root / "clip.mp4").write_bytes(b"1234") self.assertEqual(active_video_name(root, marker), "clip.mp4") def test_control_payload_clears_stale_active_marker(self): from tempfile import TemporaryDirectory old_out = ui_server.OUT_DIR old_marker = ui_server.ACTIVE_VIDEO_PATH old_process = ui_server.CONTROL_PROCESS try: with TemporaryDirectory() as tmp: root = Path(tmp) marker = root / ".active_video" (root / "stale.mp4").write_bytes(b"1234") marker.write_text("stale.mp4", encoding="utf-8") ui_server.OUT_DIR = root ui_server.ACTIVE_VIDEO_PATH = marker ui_server.CONTROL_PROCESS = None payload = ui_server.control_payload() self.assertFalse(payload["running"]) self.assertEqual(payload["active_video"], "") self.assertFalse(marker.exists()) finally: ui_server.OUT_DIR = old_out ui_server.ACTIVE_VIDEO_PATH = old_marker ui_server.CONTROL_PROCESS = old_process def test_parse_range_header_supports_suffix_range(self): self.assertEqual(parse_range_header("bytes=-4", 10), (6, 9)) def test_parse_range_header_rejects_out_of_range(self): self.assertIsNone(parse_range_header("bytes=20-30", 10)) def test_parse_source_reads_opened_source_log(self): log = "[entrypoint] source=0 backend=json\nOpened source: 0 (camera)\n" self.assertEqual(parse_source(log), "0 (camera)") def test_parse_perf_reads_realtime_pass_counters(self): log = "[perf] fps~58.0 iter p50=3.0 p95=28.0 | yolo p50=25.0 p95=34.0 skip=4 pass=120 analysisEvery=2\n" perf = parse_perf(log) self.assertEqual(perf["fps"], "58.0") self.assertEqual(perf["skip"], "4") self.assertEqual(perf["pass"], "120") self.assertEqual(perf["analysis_every"], "2") if __name__ == "__main__": unittest.main()