Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions tests/coordinators/test_detector_setup_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,3 +1031,60 @@ def test_parallel_coordinator_instances(
# Both coordinators should see the same state
assert coordinator1.is_detector_initialized() is True
assert coordinator2.is_detector_initialized() is True


class TestConfigureZonesAndParamUpdates:
def test_configure_zones_missing_deps_raises(self, detector_setup_coordinator):
detector_setup_coordinator.detector_service = None
with pytest.raises(CoordinatorValidationError, match="DetectorService is required"):
detector_setup_coordinator.configure_zones([{"type": "arena"}])

def test_configure_zones_legacy_dict_list(self, detector_setup_coordinator):
legacy_zones = [
{"type": "arena", "polygon": [[0, 0], [10, 0], [10, 10], [0, 10]]},
{
"type": "roi",
"polygon": [[2, 2], [4, 2], [4, 4], [2, 4]],
"name": "ROI1",
"color": "#ff0000",
},
]
success = detector_setup_coordinator.configure_zones(
legacy_zones, video_width=640, video_height=480
)
assert success is True

def test_update_detector_parameters_invalid_scope(self, detector_setup_coordinator):
from zebtrack.core.exceptions import ValidationError

with pytest.raises(ValidationError, match="Invalid scope"):
detector_setup_coordinator.update_detector_parameters(
{"conf_threshold": 0.5}, scope="invalid_scope"
)

def test_update_detector_parameters_value_error_raises_validation_error(
self, detector_setup_coordinator
):
from zebtrack.core.exceptions import ValidationError

detector_setup_coordinator.detector_service.update_tracking_parameters.side_effect = (
ValueError("Out of range")
)
with pytest.raises(ValidationError, match="Invalid detector parameter"):
detector_setup_coordinator.update_detector_parameters({"conf_threshold": 99.0})

def test_update_detector_parameters_generic_error_raises_coordinator_error(
self, detector_setup_coordinator
):
detector_setup_coordinator.detector_service.update_tracking_parameters.side_effect = (
RuntimeError("Crash")
)
with pytest.raises(
DetectorSetupCoordinatorError, match="Failed to update detector parameters"
):
detector_setup_coordinator.update_detector_parameters({"conf_threshold": 0.5})

def test_update_detector_parameters_maps_keys(self, detector_setup_coordinator):
params = {"confidence_threshold": 0.6, "track_buffer": 40}
success = detector_setup_coordinator.update_detector_parameters(params)
assert success is True
57 changes: 57 additions & 0 deletions tests/coordinators/test_live_batch_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,60 @@ def test_persist_session_to_project_data_existing(coordinator):
# Status should be updated, and metadata merged
assert batch["videos"][0]["status"] == "recorded"
assert batch["videos"][0]["metadata"]["group"] == "G1"


class TestUnifiedReportGeneration:
def test_generate_unified_report_no_videos(self, coordinator):
batch = BatchMetadata(batch_id="b1", group="G", day="1", subject_id="S", session_paths=[])
assert coordinator._generate_unified_report(batch) is False

def test_generate_unified_report_no_project_root(self, coordinator):
coordinator.project_manager.project_root = None
coordinator.project_manager.project_path = None
batch = BatchMetadata(
batch_id="b1", group="G", day="1", subject_id="S", session_paths=[Path("v1.mp4")]
)
assert coordinator._generate_unified_report(batch) is False

def test_generate_unified_report_no_summaries(self, coordinator, tmp_path):
coordinator.project_manager.project_root = tmp_path
coordinator.project_manager.find_video_entry.return_value = None
batch = BatchMetadata(
batch_id="b1", group="G", day="1", subject_id="S", session_paths=[Path("v1.mp4")]
)
assert coordinator._generate_unified_report(batch) is False

def test_generate_unified_report_success(self, coordinator, tmp_path):
coordinator.project_manager.project_root = tmp_path
coordinator.project_manager.find_video_entry.return_value = {
"summary_excel": str(tmp_path / "summary.xlsx")
}
batch = BatchMetadata(
batch_id="b1", session_paths=[Path("v1.mp4")], group="G1", day="1", subject_id="S1"
)

with patch.object(
coordinator, "_resolve_summary_excel_path", return_value=tmp_path / "summary.xlsx"
):
ok = coordinator._generate_unified_report(batch)
assert ok is True
coordinator.analysis_service.aggregate_session_summaries.assert_called_once()
coordinator.project_manager.register_batch_outputs.assert_called_once()


class TestCollectMultiAquariumOutputs:
def test_collect_none_or_missing_dir(self):
assert LiveBatchCoordinator._collect_multi_aquarium_outputs(None, {}) == {}
assert LiveBatchCoordinator._collect_multi_aquarium_outputs(Path("/nonexistent"), {}) == {}

def test_collect_with_aquarium_subdirectories(self, tmp_path):
aq1 = tmp_path / "aquarium_1"
aq1.mkdir()
(aq1 / "1_ProcessingArea_test.parquet").write_text("data")

outputs = LiveBatchCoordinator._collect_multi_aquarium_outputs(
tmp_path, {"group": "G1", "subject_id": "S1", "day": 1}
)
assert 0 in outputs
assert outputs[0]["group"] == "G1"
assert outputs[0]["subject_id"] == "S1"
99 changes: 99 additions & 0 deletions tests/coordinators/test_live_calibration_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from zebtrack.coordinators.live_calibration_coordinator import (
LiveCalibrationCoordinator,
)
from zebtrack.ui.event_bus_v2 import UIEvents

# ---------------------------------------------------------------------------
# Helpers
Expand Down Expand Up @@ -1218,3 +1219,101 @@ def _make_camera(settings_obj=None, **kwargs):
assert 3 in captured_indices, "Camera deve ser criada com o índice ad-hoc (3)"
# settings global restaurado após a construção.
assert coordinator.settings.camera.index == 0


class TestPrepareZonesForLiveSession:
def test_ensure_zones_auto_detect_success(self):
coordinator: Any = _make_coordinator()
coordinator.root = MagicMock()
coordinator.project_manager.get_project_type.return_value = "live"
coordinator.project_manager.get_zone_data.return_value = None
coordinator.run_live_calibration = MagicMock(return_value=True)
coordinator._wait_for_zone_confirmation = MagicMock(return_value=True)

mock_dialog = MagicMock()
mock_dialog.show.return_value = {"method": "auto"}

with patch(
"zebtrack.ui.dialogs.zone_calibration_dialog.ZoneCalibrationDialog",
return_value=mock_dialog,
):
res = coordinator.ensure_zones_before_recording(camera_index=0)

assert res is True
coordinator.run_live_calibration.assert_called_once()
coordinator._wait_for_zone_confirmation.assert_called_once()
coordinator.event_bus.publish.assert_called()

def test_ensure_zones_auto_detect_user_cancelled(self):
coordinator: Any = _make_coordinator()
coordinator.root = MagicMock()
coordinator.project_manager.get_project_type.return_value = "live"
coordinator.project_manager.get_zone_data.return_value = None
coordinator.run_live_calibration = MagicMock(return_value=False)
coordinator._last_calibration_cancelled = True

mock_dialog = MagicMock()
mock_dialog.show.return_value = {"method": "auto"}

with patch(
"zebtrack.ui.dialogs.zone_calibration_dialog.ZoneCalibrationDialog",
return_value=mock_dialog,
):
res = coordinator.ensure_zones_before_recording(camera_index=0)

assert res is False

def test_ensure_zones_auto_detect_failed_fallback_to_manual(self):
coordinator: Any = _make_coordinator()
coordinator.root = MagicMock()
coordinator.project_manager.get_project_type.return_value = "live"
coordinator.project_manager.get_zone_data.return_value = None
coordinator.run_live_calibration = MagicMock(return_value=False)
coordinator._last_calibration_cancelled = False
coordinator._capture_reference_frame_for_zones = MagicMock(return_value=True)
coordinator._wait_for_zone_confirmation = MagicMock(return_value=True)

mock_dialog = MagicMock()
mock_dialog.show.return_value = {"method": "auto"}

with patch(
"zebtrack.ui.dialogs.zone_calibration_dialog.ZoneCalibrationDialog",
return_value=mock_dialog,
):
res = coordinator.ensure_zones_before_recording(camera_index=0)

assert res is True
coordinator._capture_reference_frame_for_zones.assert_called_once()
coordinator._wait_for_zone_confirmation.assert_called_once()
event_types = [c[0][0].type for c in coordinator.event_bus.publish.call_args_list]
assert UIEvents.UI_SELECT_TAB in event_types
assert UIEvents.UI_UPDATE_ZONE_LIST in event_types

def test_ensure_zones_calibration_dialog_cancelled(self):
coordinator: Any = _make_coordinator()
coordinator.root = MagicMock()
coordinator.project_manager.get_project_type.return_value = "live"
coordinator.project_manager.get_zone_data.return_value = None

mock_dialog = MagicMock()
mock_dialog.show.return_value = None # user cancelled dialog

with patch(
"zebtrack.ui.dialogs.zone_calibration_dialog.ZoneCalibrationDialog",
return_value=mock_dialog,
):
res = coordinator.ensure_zones_before_recording(camera_index=0)

assert res is False


class TestPolygonSourceManagement:
def test_source_lifecycle(self):
coordinator = _make_coordinator()
assert coordinator.last_polygon_source is None

coordinator._set_last_polygon_source("auto")
assert coordinator.last_polygon_source == "auto"

coordinator.clear_last_polygon_source()
assert coordinator.last_polygon_source is None
Loading
Loading