"""ObjectManager pending edit lifecycle — stage_add, stage_remove, has_pending_edit, clear_pending_edits. No GPU required.""" import unittest from CelerisLab.body.manager import ObjectManager from CelerisLab.body.objects import SimObject from CelerisLab.body.geometry.circle import CircleGeometry def _make_circle_obj(cx: float, cy: float, radius: float, is_sensor: bool = False) -> SimObject: """Create a minimal SimObject with CircleGeometry.""" geom = CircleGeometry(cx, cy, radius) return SimObject( obj_id=-1, geometry=geom, center=(cx, cy), radius=radius, is_sensor=is_sensor, ) class TestPendingEditLifecycle(unittest.TestCase): """Test the pending edit state machine on ObjectManager.""" def setUp(self): # ObjectManager requires nx, ny, nz, nq, cfg. Use a minimal cfg stub. self.cfg = _StubCfg(dim=2) self.mgr = ObjectManager(nx=64, ny=32, nz=1, nq=9, cfg=self.cfg) # -- stage_add ----------------------------------------------------------- def test_stage_add_sets_edit_active(self): obj = _make_circle_obj(30, 16, 5) self.assertFalse(self.mgr.has_pending_edit()) self.mgr.stage_add(obj) self.assertTrue(self.mgr.has_pending_edit()) def test_stage_add_does_not_change_formal_count(self): obj = _make_circle_obj(30, 16, 5) self.assertEqual(self.mgr.count, 0) self.mgr.stage_add(obj) self.assertEqual(self.mgr.count, 0) def test_stage_add_multiple(self): for i in range(3): self.mgr.stage_add(_make_circle_obj(10 + i * 10, 16, 3)) self.assertTrue(self.mgr.has_pending_edit()) # -- stage_remove -------------------------------------------------------- def test_stage_remove_valid_id(self): body_id = self.mgr.add(_make_circle_obj(30, 16, 5)) self.mgr.stage_remove(body_id) self.assertTrue(self.mgr.has_pending_edit()) def test_stage_remove_invalid_id_raises(self): with self.assertRaises(IndexError): self.mgr.stage_remove(999) def test_stage_remove_does_not_change_formal_count(self): body_id = self.mgr.add(_make_circle_obj(30, 16, 5)) self.assertEqual(self.mgr.count, 1) self.mgr.stage_remove(body_id) self.assertEqual(self.mgr.count, 1) # -- has_pending_edit ---------------------------------------------------- def test_has_pending_edit_false_initially(self): self.assertFalse(self.mgr.has_pending_edit()) def test_has_pending_edit_false_after_clear(self): self.mgr.stage_add(_make_circle_obj(30, 16, 5)) self.mgr.clear_pending_edits() self.assertFalse(self.mgr.has_pending_edit()) def test_has_pending_edit_false_after_empty_stage(self): # If we add and then remove the same pending add, edit is still # "active" (edit_active=True) but has no pending content. # has_pending_edit should return False. obj = _make_circle_obj(30, 16, 5) self.mgr.stage_add(obj) # Manually clear pending_add to simulate an empty edit window self.mgr._pending_add.clear() self.assertFalse(self.mgr.has_pending_edit()) # -- clear_pending_edits ------------------------------------------------- def test_clear_resets_all_pending(self): body_id = self.mgr.add(_make_circle_obj(30, 16, 5)) self.mgr.stage_add(_make_circle_obj(40, 16, 3)) self.mgr.stage_remove(body_id) self.assertTrue(self.mgr.has_pending_edit()) self.mgr.clear_pending_edits() self.assertFalse(self.mgr.has_pending_edit()) self.assertEqual(len(self.mgr._pending_add), 0) self.assertEqual(len(self.mgr._pending_remove), 0) self.assertFalse(self.mgr._edit_active) def test_clear_preserves_formal_registry(self): body_id = self.mgr.add(_make_circle_obj(30, 16, 5)) self.mgr.stage_remove(body_id) self.mgr.clear_pending_edits() # Formal object should still be there self.assertEqual(self.mgr.count, 1) self.assertEqual(self.mgr.get(body_id).obj_id, body_id) # -- Combination: add + remove ------------------------------------------- def test_stage_add_and_remove_together(self): body_id = self.mgr.add(_make_circle_obj(30, 16, 5)) self.mgr.stage_add(_make_circle_obj(40, 16, 3)) self.mgr.stage_remove(body_id) self.assertTrue(self.mgr.has_pending_edit()) # Formal count unchanged self.assertEqual(self.mgr.count, 1) # -- Formal add (pre-initialize path) ------------------------------------ def test_formal_add_still_works(self): """The existing add() path must remain functional.""" obj = _make_circle_obj(30, 16, 5) body_id = self.mgr.add(obj) self.assertEqual(body_id, 0) self.assertEqual(self.mgr.count, 1) def test_formal_add_and_pending_coexist(self): """Formal add + pending stage should not interfere.""" self.mgr.add(_make_circle_obj(30, 16, 5)) self.mgr.stage_add(_make_circle_obj(40, 16, 3)) self.assertEqual(self.mgr.count, 1) self.assertTrue(self.mgr.has_pending_edit()) class _StubCfg: """Minimal LBMConfig-like stub for ObjectManager construction.""" def __init__(self, dim: int = 2): self.dim = dim self.is_d3q19 = (dim == 3) if __name__ == "__main__": unittest.main()