Files
meshcore-stats/tests/html/test_write_site.py

283 lines
8.7 KiB
Python

"""Tests for write_site and related output functions."""
import pytest
from meshmon.db import get_latest_metrics
from meshmon.html import (
copy_static_assets,
write_site,
)
BASE_TS = 1704067200
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,
}
@pytest.fixture(scope="module")
def html_db_cache(tmp_path_factory):
"""Create and populate a shared DB once for HTML write_site tests."""
from meshmon.db import init_db, insert_metrics
root_dir = tmp_path_factory.mktemp("html-db")
state_dir = root_dir / "state"
state_dir.mkdir()
db_path = state_dir / "metrics.db"
init_db(db_path=db_path)
now = BASE_TS
day_seconds = 86400
sample_companion_metrics = _sample_companion_metrics()
sample_repeater_metrics = _sample_repeater_metrics()
# Insert 7 days of companion data (every hour)
for day in range(7):
for hour in range(24):
ts = now - (day * day_seconds) - (hour * 3600)
metrics = sample_companion_metrics.copy()
metrics["battery_mv"] = 3700 + (hour * 10) + (day * 5)
metrics["recv"] = 100 * (day + 1) + hour
metrics["sent"] = 50 * (day + 1) + hour
insert_metrics(ts, "companion", metrics, db_path=db_path)
# Insert 7 days of repeater data (every 15 minutes)
for day in range(7):
for interval in range(96): # 24 * 4
ts = now - (day * day_seconds) - (interval * 900)
metrics = sample_repeater_metrics.copy()
metrics["bat"] = 3700 + (interval * 2) + (day * 5)
metrics["nb_recv"] = 1000 * (day + 1) + interval * 10
metrics["nb_sent"] = 500 * (day + 1) + interval * 5
insert_metrics(ts, "repeater", metrics, db_path=db_path)
return {"state_dir": state_dir, "db_path": db_path}
@pytest.fixture
def html_env(html_db_cache, tmp_out_dir, monkeypatch):
"""Env with shared DB and per-test output directory."""
monkeypatch.setenv("STATE_DIR", str(html_db_cache["state_dir"]))
monkeypatch.setenv("OUT_DIR", str(tmp_out_dir))
import meshmon.env
meshmon.env._config = None
return {"state_dir": html_db_cache["state_dir"], "out_dir": tmp_out_dir}
@pytest.fixture
def metrics_rows(html_env):
"""Get latest metrics rows for both roles."""
companion_row = get_latest_metrics("companion")
repeater_row = get_latest_metrics("repeater")
return {"companion": companion_row, "repeater": repeater_row}
class TestWriteSite:
"""Tests for write_site function."""
def test_creates_output_directory(self, html_env, metrics_rows):
"""Creates output directory if it doesn't exist."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
assert out_dir.exists()
def test_generates_repeater_pages(self, html_env, metrics_rows):
"""Generates repeater HTML pages at root."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
# Repeater pages are at root
for period in ["day", "week", "month", "year"]:
assert (out_dir / f"{period}.html").exists()
def test_generates_companion_pages(self, html_env, metrics_rows):
"""Generates companion HTML pages in subdirectory."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
companion_dir = out_dir / "companion"
assert companion_dir.exists()
for period in ["day", "week", "month", "year"]:
assert (companion_dir / f"{period}.html").exists()
def test_html_files_are_valid(self, html_env, metrics_rows):
"""Generated HTML files have valid structure."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
html_file = out_dir / "day.html"
content = html_file.read_text()
assert "<!DOCTYPE html>" in content or "<!doctype html>" in content.lower()
assert "</html>" in content
def test_handles_empty_database(self, configured_env, initialized_db):
"""Handles empty database gracefully."""
out_dir = configured_env["out_dir"]
# Should not raise - pass None for empty database
write_site(None, None)
# Should still generate pages
assert (out_dir / "day.html").exists()
class TestCopyStaticAssets:
"""Tests for copy_static_assets function."""
def test_copies_css(self, html_env):
"""Copies CSS stylesheet."""
out_dir = html_env["out_dir"]
out_dir.mkdir(parents=True, exist_ok=True)
copy_static_assets()
css_file = out_dir / "styles.css"
assert css_file.exists()
def test_copies_javascript(self, html_env):
"""Copies JavaScript files."""
out_dir = html_env["out_dir"]
out_dir.mkdir(parents=True, exist_ok=True)
copy_static_assets()
js_file = out_dir / "chart-tooltip.js"
assert js_file.exists()
def test_css_is_valid(self, html_env):
"""Copied CSS has expected content."""
out_dir = html_env["out_dir"]
out_dir.mkdir(parents=True, exist_ok=True)
copy_static_assets()
css_file = out_dir / "styles.css"
content = css_file.read_text()
assert "--bg-primary" in content
def test_requires_output_directory(self, html_env):
"""Requires output directory to exist."""
out_dir = html_env["out_dir"]
# Ensure out_dir exists
out_dir.mkdir(parents=True, exist_ok=True)
# Should not raise when directory exists
copy_static_assets()
assert (out_dir / "styles.css").exists()
def test_overwrites_existing(self, html_env):
"""Overwrites existing static files."""
out_dir = html_env["out_dir"]
out_dir.mkdir(parents=True, exist_ok=True)
# Create a fake CSS file
css_file = out_dir / "styles.css"
css_file.write_text("/* fake */")
copy_static_assets()
# Should be overwritten with real content
content = css_file.read_text()
assert content != "/* fake */"
class TestHtmlOutput:
"""Tests for HTML output structure."""
def test_pages_include_navigation(self, html_env, metrics_rows):
"""HTML pages include navigation."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
content = (out_dir / "day.html").read_text()
# Should have links to other periods
assert "week" in content.lower()
assert "month" in content.lower()
def test_pages_include_meta_tags(self, html_env, metrics_rows):
"""HTML pages include meta tags."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
content = (out_dir / "day.html").read_text()
assert "<meta" in content
assert "charset" in content.lower() or "utf-8" in content.lower()
def test_pages_include_title(self, html_env, metrics_rows):
"""HTML pages include title tag."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
content = (out_dir / "day.html").read_text()
assert "<title>" in content
assert "</title>" in content
def test_pages_reference_css(self, html_env, metrics_rows):
"""HTML pages reference stylesheet."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
content = (out_dir / "day.html").read_text()
assert 'href="styles.css"' in content
assert 'href="/styles.css"' not in content
def test_companion_pages_relative_css(self, html_env, metrics_rows):
"""Companion pages use relative path to CSS."""
out_dir = html_env["out_dir"]
write_site(metrics_rows["companion"], metrics_rows["repeater"])
content = (out_dir / "companion" / "day.html").read_text()
# Should reference parent directory CSS
assert 'href="../styles.css"' in content
assert 'href="/styles.css"' not in content