"""Unit tests for the hameter.discovery module.""" import threading from unittest.mock import MagicMock, patch import pytest from hameter.discovery import run_discovery_for_web from hameter.config import GeneralConfig from hameter.state import AppState, PipelineStatus VALID_LINE_1 = '{"Time":"2026-03-05T10:00:00Z","Type":"SCM","Message":{"ID":12345,"Consumption":50000}}' VALID_LINE_2 = '{"Time":"2026-03-05T10:00:05Z","Type":"SCM","Message":{"ID":67890,"Consumption":12000}}' VALID_LINE_3 = '{"Time":"2026-03-05T10:00:10Z","Type":"SCM","Message":{"ID":12345,"Consumption":50100}}' @pytest.fixture def config(): return GeneralConfig() @pytest.fixture def shutdown_event(): return threading.Event() @pytest.fixture def app_state(): return AppState() @patch("hameter.discovery.SubprocessManager") def test_run_discovery_for_web_records_results( mock_spm_cls, config, shutdown_event, app_state ): """Parsed meter readings are recorded via app_state.""" mock_proc = MagicMock() mock_spm_cls.return_value = mock_proc mock_proc.start_discovery_mode.return_value = True calls = [0] def get_line(timeout=1.0): calls[0] += 1 lines = [VALID_LINE_1, VALID_LINE_2, VALID_LINE_3] if calls[0] <= len(lines): return lines[calls[0] - 1] shutdown_event.set() return None mock_proc.get_line.side_effect = get_line run_discovery_for_web( config, shutdown_event, app_state, duration=300, stop_event=None, ) results = app_state.get_discovery_results() assert 12345 in results assert 67890 in results assert results[12345]["count"] >= 2 mock_proc.stop.assert_called_once() @patch("hameter.discovery.SubprocessManager") def test_run_discovery_for_web_start_failure( mock_spm_cls, config, shutdown_event, app_state ): """Start failure sets ERROR status.""" mock_proc = MagicMock() mock_spm_cls.return_value = mock_proc mock_proc.start_discovery_mode.return_value = False run_discovery_for_web( config, shutdown_event, app_state, duration=120, stop_event=None, ) assert app_state.status == PipelineStatus.ERROR mock_proc.get_line.assert_not_called() @patch("hameter.discovery.SubprocessManager") def test_run_discovery_for_web_respects_stop_event( mock_spm_cls, config, shutdown_event, app_state ): """stop_event causes early exit.""" mock_proc = MagicMock() mock_spm_cls.return_value = mock_proc mock_proc.start_discovery_mode.return_value = True stop_event = threading.Event() stop_event.set() mock_proc.get_line.return_value = VALID_LINE_1 run_discovery_for_web( config, shutdown_event, app_state, duration=300, stop_event=stop_event, ) mock_proc.stop.assert_called_once() @patch("hameter.discovery.SubprocessManager") def test_run_discovery_for_web_handles_invalid_json( mock_spm_cls, config, shutdown_event, app_state ): """Invalid lines are skipped without crashing.""" mock_proc = MagicMock() mock_spm_cls.return_value = mock_proc mock_proc.start_discovery_mode.return_value = True calls = [0] lines = [ "not json", "{malformed{{", "", VALID_LINE_1, ] def get_line(timeout=1.0): calls[0] += 1 if calls[0] <= len(lines): return lines[calls[0] - 1] shutdown_event.set() return None mock_proc.get_line.side_effect = get_line run_discovery_for_web( config, shutdown_event, app_state, duration=300, stop_event=None, ) results = app_state.get_discovery_results() assert 12345 in results assert len(results) == 1 mock_proc.stop.assert_called_once()