test_scan_point_store.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import json
  2. import sys
  3. import os
  4. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  5. import pytest
  6. from core.scan_point_store import ScanPointStore
  7. def test_add_and_list_points(tmp_path):
  8. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  9. store.ensure_group("group_1", {"ptz_name": "PTZ1", "panorama_name": "PAN1"})
  10. store.add_enabled_point("group_1", pan=30.0, tilt=0.0, zoom=1, dwell_time=3.0)
  11. points = store.list_enabled_points("group_1")
  12. assert len(points) == 1
  13. assert points[0]["pan"] == 30.0
  14. def test_persistence(tmp_path):
  15. path = str(tmp_path / "scan_models.json")
  16. store = ScanPointStore(path)
  17. store.ensure_group("group_1", {"ptz_name": "PTZ1", "panorama_name": "PAN1"})
  18. store.add_enabled_point("group_1", pan=60.0, tilt=-5.0, zoom=2, dwell_time=2.0)
  19. del store
  20. store2 = ScanPointStore(path)
  21. points = store2.list_enabled_points("group_1")
  22. assert len(points) == 1
  23. assert points[0]["zoom"] == 2
  24. def test_delete_point(tmp_path):
  25. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  26. store.ensure_group("g1", {})
  27. p1 = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  28. store.add_enabled_point("g1", pan=20.0, tilt=0.0)
  29. assert len(store.list_enabled_points("g1")) == 2
  30. ok = store.delete_enabled_point("g1", p1["id"])
  31. assert ok
  32. points = store.list_enabled_points("g1")
  33. assert len(points) == 1
  34. assert points[0]["order"] == 0
  35. def test_returns_deep_copies(tmp_path):
  36. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  37. store.ensure_group("g1", {"ptz_name": "PTZ1"})
  38. point = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  39. point["pan"] = 999.0
  40. points = store.list_enabled_points("g1")
  41. assert points[0]["pan"] == 10.0
  42. group = store.get_group("g1")
  43. group["ptz_name"] = "CHANGED"
  44. assert store.get_group("g1")["ptz_name"] == "PTZ1"
  45. points[0]["tilt"] = 999.0
  46. assert store.list_enabled_points("g1")[0]["tilt"] == 0.0
  47. def test_reorder_points_exact_ids(tmp_path):
  48. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  49. store.ensure_group("g1", {})
  50. p1 = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  51. p2 = store.add_enabled_point("g1", pan=20.0, tilt=0.0)
  52. p3 = store.add_enabled_point("g1", pan=30.0, tilt=0.0)
  53. original_order = [p["id"] for p in store.list_enabled_points("g1")]
  54. assert original_order == [p1["id"], p2["id"], p3["id"]]
  55. reversed_order = [p3["id"], p2["id"], p1["id"]]
  56. store.reorder_points("g1", reversed_order)
  57. reordered = store.list_enabled_points("g1")
  58. assert [p["id"] for p in reordered] == reversed_order
  59. for idx, p in enumerate(reordered):
  60. assert p["order"] == idx
  61. def test_reorder_points_rejects_mismatched_ids(tmp_path):
  62. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  63. store.ensure_group("g1", {})
  64. p1 = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  65. p2 = store.add_enabled_point("g1", pan=20.0, tilt=0.0)
  66. with pytest.raises(ValueError, match="point_ids must contain exactly"):
  67. store.reorder_points("g1", [p1["id"]])
  68. with pytest.raises(ValueError, match="point_ids must contain exactly"):
  69. store.reorder_points("g1", [p1["id"], p2["id"], 999])
  70. with pytest.raises(ValueError, match="point_ids must contain exactly"):
  71. store.reorder_points("g1", [p1["id"], p1["id"]])
  72. def test_update_enabled_point(tmp_path):
  73. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  74. store.ensure_group("g1", {})
  75. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0, zoom=1, dwell_time=3.0)
  76. ok = store.update_enabled_point("g1", p["id"], {"pan": 45.0, "zoom": 5})
  77. assert ok
  78. updated = store.list_enabled_points("g1")[0]
  79. assert updated["pan"] == 45.0
  80. assert updated["zoom"] == 5
  81. assert updated["tilt"] == 0.0
  82. def test_invalid_inputs_raise(tmp_path):
  83. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  84. store.ensure_group("g1", {})
  85. with pytest.raises(ValueError):
  86. store.add_enabled_point("g1", pan="bad", tilt=0.0)
  87. with pytest.raises(ValueError):
  88. store.add_enabled_point("g1", pan=10.0, tilt=0.0, zoom=0)
  89. with pytest.raises(ValueError):
  90. store.add_enabled_point("g1", pan=10.0, tilt=0.0, dwell_time=0)
  91. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  92. with pytest.raises(ValueError):
  93. store.update_enabled_point("g1", p["id"], {"pan": "bad"})
  94. with pytest.raises(ValueError):
  95. store.update_enabled_point("g1", p["id"], {"zoom": -1})
  96. with pytest.raises(ValueError):
  97. store.update_enabled_point("g1", p["id"], {"dwell_time": -1.0})
  98. def test_corrupted_json_starts_fresh(tmp_path, capsys):
  99. path = str(tmp_path / "scan_models.json")
  100. with open(path, "w", encoding="utf-8") as f:
  101. f.write("{not valid json")
  102. store = ScanPointStore(path)
  103. captured = capsys.readouterr()
  104. assert "corrupted JSON" in captured.err
  105. store.ensure_group("g1", {"ptz_name": "PTZ1"})
  106. assert store.get_group("g1")["ptz_name"] == "PTZ1"
  107. def test_point_id_generation(tmp_path):
  108. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  109. store.ensure_group("g1", {})
  110. p1 = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  111. p2 = store.add_enabled_point("g1", pan=20.0, tilt=0.0)
  112. store.delete_enabled_point("g1", p1["id"])
  113. p3 = store.add_enabled_point("g1", pan=30.0, tilt=0.0)
  114. assert p1["id"] < p2["id"] < p3["id"]
  115. assert p3["id"] == p2["id"] + 1
  116. def test_set_samples_deep_copies(tmp_path):
  117. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  118. store.ensure_group("g1", {})
  119. samples = [{"x": 1, "nested": {"y": 2}}]
  120. store.set_samples("g1", samples)
  121. samples[0]["nested"]["y"] = 999
  122. assert store.get_group("g1")["samples"][0]["nested"]["y"] == 2
  123. def test_set_scan_config_deep_copies(tmp_path):
  124. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  125. store.ensure_group("g1", {})
  126. config = {"threshold": {"value": 0.5}}
  127. store.set_scan_config("g1", config)
  128. config["threshold"]["value"] = 0.9
  129. assert store.get_group("g1")["scan_config"]["threshold"]["value"] == 0.5
  130. def test_set_panorama_deep_copies(tmp_path):
  131. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  132. store.ensure_group("g1", {})
  133. panorama = {"image": {"width": 1920}}
  134. store.set_panorama("g1", panorama)
  135. panorama["image"]["width"] = 999
  136. assert store.get_group("g1")["panorama"]["image"]["width"] == 1920
  137. def test_next_point_id_seeded_from_legacy_points(tmp_path):
  138. path = str(tmp_path / "scan_models.json")
  139. legacy_data = {
  140. "camera_groups": {
  141. "g1": {
  142. "ptz_name": "g1",
  143. "panorama_name": "g1",
  144. "scan_config": {},
  145. "samples": [],
  146. "enabled_points": [
  147. {"id": 5, "pan": 10.0, "tilt": 0.0, "zoom": 1, "dwell_time": 3.0, "order": 0},
  148. {"id": 12, "pan": 20.0, "tilt": 0.0, "zoom": 1, "dwell_time": 3.0, "order": 1},
  149. ],
  150. "panorama": {},
  151. }
  152. }
  153. }
  154. with open(path, "w", encoding="utf-8") as f:
  155. json.dump(legacy_data, f)
  156. store = ScanPointStore(path)
  157. store.ensure_group("g1", {})
  158. p = store.add_enabled_point("g1", pan=30.0, tilt=0.0)
  159. assert p["id"] == 13
  160. def test_reject_boolean_values(tmp_path):
  161. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  162. store.ensure_group("g1", {})
  163. with pytest.raises(ValueError):
  164. store.add_enabled_point("g1", pan=True, tilt=0.0)
  165. with pytest.raises(ValueError):
  166. store.add_enabled_point("g1", pan=10.0, tilt=False)
  167. with pytest.raises(ValueError):
  168. store.add_enabled_point("g1", pan=10.0, tilt=0.0, zoom=True)
  169. with pytest.raises(ValueError):
  170. store.add_enabled_point("g1", pan=10.0, tilt=0.0, dwell_time=True)
  171. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  172. with pytest.raises(ValueError):
  173. store.update_enabled_point("g1", p["id"], {"pan": True})
  174. with pytest.raises(ValueError):
  175. store.update_enabled_point("g1", p["id"], {"tilt": False})
  176. with pytest.raises(ValueError):
  177. store.update_enabled_point("g1", p["id"], {"zoom": True})
  178. with pytest.raises(ValueError):
  179. store.update_enabled_point("g1", p["id"], {"dwell_time": True})
  180. def test_update_unknown_key_raises(tmp_path):
  181. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  182. store.ensure_group("g1", {})
  183. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  184. with pytest.raises(ValueError, match="Unknown field: bad_key"):
  185. store.update_enabled_point("g1", p["id"], {"bad_key": 123})
  186. def test_update_rejects_boolean_order(tmp_path):
  187. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  188. store.ensure_group("g1", {})
  189. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  190. with pytest.raises(ValueError, match="order must be an integer"):
  191. store.update_enabled_point("g1", p["id"], {"order": True})
  192. def test_update_rejects_non_finite_values(tmp_path):
  193. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  194. store.ensure_group("g1", {})
  195. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  196. with pytest.raises(ValueError, match="pan must be finite"):
  197. store.update_enabled_point("g1", p["id"], {"pan": float("nan")})
  198. with pytest.raises(ValueError, match="tilt must be finite"):
  199. store.update_enabled_point("g1", p["id"], {"tilt": float("inf")})
  200. with pytest.raises(ValueError, match="dwell_time must be finite"):
  201. store.update_enabled_point("g1", p["id"], {"dwell_time": float("-inf")})
  202. def test_update_rejects_negative_order(tmp_path):
  203. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  204. store.ensure_group("g1", {})
  205. p = store.add_enabled_point("g1", pan=10.0, tilt=0.0)
  206. with pytest.raises(ValueError, match="order must be non-negative"):
  207. store.update_enabled_point("g1", p["id"], {"order": -1})
  208. def test_add_rejects_non_finite_values(tmp_path):
  209. store = ScanPointStore(str(tmp_path / "scan_models.json"))
  210. store.ensure_group("g1", {})
  211. with pytest.raises(ValueError, match="pan must be finite"):
  212. store.add_enabled_point("g1", pan=float("nan"), tilt=0.0)
  213. with pytest.raises(ValueError, match="dwell_time must be finite"):
  214. store.add_enabled_point("g1", pan=10.0, tilt=0.0, dwell_time=float("inf"))