mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-03-28 17:42:55 +01:00
283 lines
8.7 KiB
Python
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
|