"""Tests for render script entry points. These tests verify the render_charts.py, render_site.py, and render_reports.py scripts can be imported and their main() functions work correctly. """ import json from pathlib import Path from unittest.mock import MagicMock, patch from tests.scripts.conftest import load_script_module def load_script(script_name: str): """Load a script as a module.""" return load_script_module(script_name) class TestRenderChartsImport: """Verify render_charts.py imports correctly.""" def test_imports_successfully(self, configured_env): """Script should import without errors.""" module = load_script("render_charts.py") assert hasattr(module, "main") assert callable(module.main) def test_main_calls_init_db(self, configured_env): """main() should initialize database.""" module = load_script("render_charts.py") with ( patch.object(module, "init_db") as mock_init, patch.object(module, "get_metric_count", return_value=0), ): module.main() mock_init.assert_called_once() def test_main_checks_metric_counts(self, configured_env): """main() should check for data before rendering.""" module = load_script("render_charts.py") with ( patch.object(module, "init_db"), patch.object(module, "get_metric_count") as mock_count, ): mock_count.return_value = 0 module.main() # Should check both companion and repeater assert [call.args[0] for call in mock_count.call_args_list] == [ "companion", "repeater", ] def test_main_renders_when_data_exists(self, configured_env): """main() should render charts when data exists.""" module = load_script("render_charts.py") with ( patch.object(module, "init_db"), patch.object(module, "get_metric_count", return_value=100), patch.object(module, "render_all_charts") as mock_render, patch.object(module, "save_chart_stats") as mock_save, ): mock_render.return_value = (["chart1.svg"], {"bat": {}}) module.main() # Should render for both roles assert [call.args[0] for call in mock_render.call_args_list] == [ "companion", "repeater", ] assert [call.args[0] for call in mock_save.call_args_list] == [ "companion", "repeater", ] class TestRenderSiteImport: """Verify render_site.py imports correctly.""" def test_imports_successfully(self, configured_env): """Script should import without errors.""" module = load_script("render_site.py") assert hasattr(module, "main") assert callable(module.main) def test_main_calls_init_db(self, configured_env): """main() should initialize database.""" module = load_script("render_site.py") with ( patch.object(module, "init_db") as mock_init, patch.object(module, "get_latest_metrics", return_value=None), patch.object(module, "write_site", return_value=[]), ): module.main() mock_init.assert_called_once() def test_main_loads_latest_metrics(self, configured_env): """main() should load latest metrics for both roles.""" module = load_script("render_site.py") with ( patch.object(module, "init_db"), patch.object(module, "get_latest_metrics") as mock_get, patch.object(module, "write_site", return_value=[]), ): mock_get.return_value = {"battery_mv": 3850} module.main() # Should get metrics for both companion and repeater assert [call.args[0] for call in mock_get.call_args_list] == [ "companion", "repeater", ] def test_main_calls_write_site(self, configured_env): """main() should call write_site with metrics.""" module = load_script("render_site.py") companion_metrics = {"battery_mv": 3850, "ts": 12345} repeater_metrics = {"bat": 3900, "ts": 12346} with ( patch.object(module, "init_db"), patch.object(module, "get_latest_metrics") as mock_get, patch.object(module, "write_site") as mock_write, ): mock_get.side_effect = [companion_metrics, repeater_metrics] mock_write.return_value = ["day.html", "week.html"] module.main() mock_write.assert_called_once_with(companion_metrics, repeater_metrics) def test_creates_html_files_for_all_periods(self, configured_env, initialized_db, tmp_path): """Should create HTML files for day/week/month/year periods.""" module = load_script("render_site.py") out_dir = configured_env["out_dir"] # Use real write_site but mock the templates to avoid complex setup with ( patch.object(module, "init_db"), patch.object(module, "get_latest_metrics") as mock_get, ): mock_get.return_value = {"battery_mv": 3850, "ts": 12345} # Let write_site run - it will create the files module.main() # Verify HTML files exist and have content for period in ["day", "week", "month", "year"]: html_file = out_dir / f"{period}.html" assert html_file.exists(), f"{period}.html should exist" content = html_file.read_text() assert len(content) > 0, f"{period}.html should have content" assert "" in content or "" in content assert "" in content assert "" in content class TestReportNavigation: """Test prev/next navigation in reports.""" def test_monthly_report_with_prev_next(self, configured_env, tmp_path, monkeypatch): """Monthly report should build prev/next navigation links.""" monkeypatch.setenv("OUT_DIR", str(tmp_path)) import meshmon.env meshmon.env._config = None module = load_script("render_reports.py") mock_agg = MagicMock() mock_agg.daily = [{"day": 1}] prev_report_data = None next_report_data = None def capture_render(agg, node_name, report_type, prev_report, next_report): nonlocal prev_report_data, next_report_data prev_report_data = prev_report next_report_data = next_report return "" with ( patch.object(module, "aggregate_monthly", return_value=mock_agg), patch.object(module, "render_report_page", side_effect=capture_render), patch.object(module, "format_monthly_txt", return_value="TXT"), patch.object(module, "monthly_to_json", return_value={}), ): # Call with prev and next periods module.render_monthly_report( "repeater", 2024, 6, prev_period=(2024, 5), next_period=(2024, 7) ) assert prev_report_data is not None assert prev_report_data["url"] == "/reports/repeater/2024/05/" assert prev_report_data["label"] == "May 2024" assert next_report_data is not None assert next_report_data["url"] == "/reports/repeater/2024/07/" assert next_report_data["label"] == "Jul 2024" def test_yearly_report_with_prev_next(self, configured_env, tmp_path, monkeypatch): """Yearly report should build prev/next navigation links.""" monkeypatch.setenv("OUT_DIR", str(tmp_path)) import meshmon.env meshmon.env._config = None module = load_script("render_reports.py") mock_agg = MagicMock() mock_agg.monthly = [{"month": 1}] prev_report_data = None next_report_data = None def capture_render(agg, node_name, report_type, prev_report, next_report): nonlocal prev_report_data, next_report_data prev_report_data = prev_report next_report_data = next_report return "" with ( patch.object(module, "aggregate_yearly", return_value=mock_agg), patch.object(module, "render_report_page", side_effect=capture_render), patch.object(module, "format_yearly_txt", return_value="TXT"), patch.object(module, "yearly_to_json", return_value={}), ): # Call with prev and next years module.render_yearly_report("repeater", 2024, prev_year=2023, next_year=2025) assert prev_report_data is not None assert prev_report_data["url"] == "/reports/repeater/2023/" assert prev_report_data["label"] == "2023" assert next_report_data is not None assert next_report_data["url"] == "/reports/repeater/2025/" assert next_report_data["label"] == "2025" class TestMainWithData: """Test main() function with actual data periods.""" def test_main_renders_reports_when_data_exists(self, configured_env, tmp_path, monkeypatch): """main() should render monthly and yearly reports when data exists.""" monkeypatch.setenv("OUT_DIR", str(tmp_path)) import meshmon.env meshmon.env._config = None module = load_script("render_reports.py") def mock_periods(role): if role == "repeater": return [(2024, 11), (2024, 12)] return [] mock_monthly_agg = MagicMock() mock_monthly_agg.daily = [{"day": 1}] mock_yearly_agg = MagicMock() mock_yearly_agg.monthly = [{"month": 11}] with ( patch.object(module, "init_db"), patch.object(module, "get_available_periods", side_effect=mock_periods), patch.object(module, "aggregate_monthly", return_value=mock_monthly_agg), patch.object(module, "aggregate_yearly", return_value=mock_yearly_agg), patch.object(module, "render_report_page", return_value=""), patch.object(module, "format_monthly_txt", return_value="TXT"), patch.object(module, "format_yearly_txt", return_value="TXT"), patch.object(module, "monthly_to_json", return_value={}), patch.object(module, "yearly_to_json", return_value={}), patch.object(module, "render_reports_index", return_value=""), ): module.main() # Verify reports were created repeater_dir = tmp_path / "reports" / "repeater" assert (repeater_dir / "2024" / "11" / "index.html").exists() assert (repeater_dir / "2024" / "12" / "index.html").exists() assert (repeater_dir / "2024" / "index.html").exists() def test_main_creates_index_with_content(self, configured_env, tmp_path, monkeypatch): """main() should create reports index with valid content.""" monkeypatch.setenv("OUT_DIR", str(tmp_path)) import meshmon.env meshmon.env._config = None module = load_script("render_reports.py") index_html = """