"""Integration test fixtures.""" import os import time from unittest.mock import AsyncMock, MagicMock import pytest _INTEGRATION_ENV = { "REPORT_LOCATION_NAME": "Test Location", "REPORT_LOCATION_SHORT": "Test", "REPEATER_DISPLAY_NAME": "Test Repeater", "COMPANION_DISPLAY_NAME": "Test Companion", "MESH_TRANSPORT": "serial", "MESH_SERIAL_PORT": "/dev/ttyACM0", } RENDERED_CHART_METRICS = { "companion": ["battery_mv", "recv", "contacts"], "repeater": ["bat", "nb_recv", "last_rssi"], } def _sample_companion_metrics() -> dict[str, float]: return { "battery_mv": 3850.0, "uptime_secs": 86400.0, "contacts": 5.0, "recv": 1234.0, "sent": 567.0, "errors": 0.0, } def _sample_repeater_metrics() -> dict[str, float]: return { "bat": 3920.0, "uptime": 172800.0, "last_rssi": -85.0, "last_snr": 7.5, "noise_floor": -115.0, "tx_queue_len": 0.0, "nb_recv": 5678.0, "nb_sent": 2345.0, "airtime": 3600.0, "rx_airtime": 7200.0, "flood_dups": 12.0, "direct_dups": 5.0, "sent_flood": 100.0, "recv_flood": 200.0, "sent_direct": 50.0, "recv_direct": 75.0, } def _populate_db_with_history( db_path, sample_companion_metrics: dict[str, float], sample_repeater_metrics: dict[str, float], days: int = 30, companion_step_seconds: int = 3600, repeater_step_seconds: int = 900, ) -> None: from meshmon.db import insert_metrics now = int(time.time()) day_seconds = 86400 companion_steps = day_seconds // companion_step_seconds repeater_steps = day_seconds // repeater_step_seconds # Insert companion data (default: 30 days, hourly) for day in range(days): for step in range(companion_steps): ts = now - (day * day_seconds) - (step * companion_step_seconds) metrics = sample_companion_metrics.copy() # Vary values to create realistic patterns metrics["battery_mv"] = 3700 + (step * 5) + (day % 7) * 10 metrics["recv"] = 100 + day * 10 + step metrics["sent"] = 50 + day * 5 + step metrics["uptime_secs"] = (days - day) * day_seconds + step * companion_step_seconds insert_metrics(ts, "companion", metrics, db_path=db_path) # Insert repeater data (default: 30 days, every 15 minutes) for day in range(days): for interval in range(repeater_steps): ts = now - (day * day_seconds) - (interval * repeater_step_seconds) metrics = sample_repeater_metrics.copy() # Vary values to create realistic patterns metrics["bat"] = 3800 + (interval % 24) * 5 + (day % 7) * 10 metrics["nb_recv"] = 1000 + day * 100 + interval metrics["nb_sent"] = 500 + day * 50 + interval metrics["uptime"] = (days - day) * day_seconds + interval * repeater_step_seconds metrics["last_rssi"] = -90 + (interval % 20) metrics["last_snr"] = 5 + (interval % 10) * 0.5 insert_metrics(ts, "repeater", metrics, db_path=db_path) @pytest.fixture def reports_env(reports_db_cache, tmp_out_dir, monkeypatch): """Integration env wired to the shared reports DB and per-test output.""" monkeypatch.setenv("STATE_DIR", str(reports_db_cache["state_dir"])) monkeypatch.setenv("OUT_DIR", str(tmp_out_dir)) for key, value in _INTEGRATION_ENV.items(): monkeypatch.setenv(key, value) import meshmon.env meshmon.env._config = None return { "state_dir": reports_db_cache["state_dir"], "out_dir": tmp_out_dir, } @pytest.fixture(scope="session") def rendered_chart_metrics(): """Minimal chart set to keep integration rendering tests fast.""" return RENDERED_CHART_METRICS @pytest.fixture def populated_db_with_history(reports_db_cache, reports_env): """Shared database populated with a fixed history window for integration tests.""" return reports_db_cache["db_path"] @pytest.fixture(scope="module") def reports_db_cache(tmp_path_factory): """Create and populate a shared reports DB once per module.""" from meshmon.db import init_db root_dir = tmp_path_factory.mktemp("reports-db") state_dir = root_dir / "state" state_dir.mkdir() db_path = state_dir / "metrics.db" init_db(db_path=db_path) _populate_db_with_history( db_path, _sample_companion_metrics(), _sample_repeater_metrics(), days=14, companion_step_seconds=7200, repeater_step_seconds=7200, ) return { "state_dir": state_dir, "db_path": db_path, } @pytest.fixture(scope="module") def rendered_charts_cache(tmp_path_factory): """Cache rendered charts once per module to speed up integration tests.""" from meshmon.charts import render_all_charts, save_chart_stats from meshmon.db import init_db root_dir = tmp_path_factory.mktemp("rendered-charts") state_dir = root_dir / "state" out_dir = root_dir / "out" state_dir.mkdir() out_dir.mkdir() env_keys = ["STATE_DIR", "OUT_DIR", *_INTEGRATION_ENV.keys()] previous_env = {key: os.environ.get(key) for key in env_keys} os.environ["STATE_DIR"] = str(state_dir) os.environ["OUT_DIR"] = str(out_dir) for key, value in _INTEGRATION_ENV.items(): os.environ[key] = value import meshmon.env meshmon.env._config = None db_path = state_dir / "metrics.db" init_db(db_path=db_path) _populate_db_with_history( db_path, _sample_companion_metrics(), _sample_repeater_metrics(), days=7, companion_step_seconds=3600, repeater_step_seconds=3600, ) for role in ["companion", "repeater"]: charts, stats = render_all_charts(role, metrics=RENDERED_CHART_METRICS[role]) save_chart_stats(role, stats) yield { "state_dir": state_dir, "out_dir": out_dir, "db_path": db_path, } for key, value in previous_env.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value meshmon.env._config = None @pytest.fixture def rendered_charts(rendered_charts_cache, monkeypatch): """Expose cached charts with env wired for per-test access.""" state_dir = rendered_charts_cache["state_dir"] out_dir = rendered_charts_cache["out_dir"] monkeypatch.setenv("STATE_DIR", str(state_dir)) monkeypatch.setenv("OUT_DIR", str(out_dir)) for key, value in _INTEGRATION_ENV.items(): monkeypatch.setenv(key, value) import meshmon.env meshmon.env._config = None return rendered_charts_cache @pytest.fixture def mock_meshcore_successful_collection(sample_companion_metrics): """Mock MeshCore client that returns successful responses.""" mc = MagicMock() mc.commands = MagicMock() mc.contacts = {} mc.disconnect = AsyncMock() # Helper to create successful event def make_event(event_type: str, payload: dict): event = MagicMock() event.type = MagicMock() event.type.name = event_type event.payload = payload return event # Mock all commands to return success - use AsyncMock directly without invoking mc.commands.send_appstart = AsyncMock(return_value=make_event("SELF_INFO", {})) mc.commands.send_device_query = AsyncMock(return_value=make_event("DEVICE_INFO", {})) mc.commands.get_time = AsyncMock(return_value=make_event("TIME", {"time": 1234567890})) mc.commands.get_self_telemetry = AsyncMock(return_value=make_event("TELEMETRY", {})) mc.commands.get_custom_vars = AsyncMock(return_value=make_event("CUSTOM_VARS", {})) mc.commands.get_contacts = AsyncMock( return_value=make_event("CONTACTS", {"contact1": {}, "contact2": {}}) ) mc.commands.get_stats_core = AsyncMock( return_value=make_event( "STATS_CORE", {"battery_mv": sample_companion_metrics["battery_mv"], "uptime_secs": 86400}, ) ) mc.commands.get_stats_radio = AsyncMock( return_value=make_event("STATS_RADIO", {"noise_floor": -115, "last_rssi": -85}) ) mc.commands.get_stats_packets = AsyncMock( return_value=make_event( "STATS_PACKETS", {"recv": sample_companion_metrics["recv"], "sent": sample_companion_metrics["sent"]}, ) ) return mc @pytest.fixture def full_integration_env(configured_env, monkeypatch): """Full integration environment with per-test directories.""" for key, value in _INTEGRATION_ENV.items(): monkeypatch.setenv(key, value) import meshmon.env meshmon.env._config = None return { "state_dir": configured_env["state_dir"], "out_dir": configured_env["out_dir"], }