mcnoaa-tides/tests/test_charts.py
Ryan Malloy 9f6d7bb4ac Rename package from noaa-tides to mcnoaa-tides
Distribution name, import package, entry point script, MCP config,
and all internal references updated. Git tracks the directory rename
so file history is preserved.
2026-02-22 16:53:56 -07:00

176 lines
5.4 KiB
Python

"""Tests for chart rendering and visualization tool registration."""
import pytest
from fastmcp import Client
from mcnoaa_tides.charts.conditions import render_conditions_html, render_conditions_png
from mcnoaa_tides.charts.tides import (
_parse_observed,
_parse_predictions,
render_tide_chart_html,
render_tide_chart_png,
)
# Mock data (matches conftest.py fixtures)
MOCK_PREDICTIONS = {
"predictions": [
{"t": "2026-02-21 04:30", "v": "4.521", "type": "H"},
{"t": "2026-02-21 10:42", "v": "-0.123", "type": "L"},
{"t": "2026-02-21 16:55", "v": "5.012", "type": "H"},
{"t": "2026-02-21 23:08", "v": "0.234", "type": "L"},
]
}
MOCK_WATER_LEVEL = {
"data": [
{"t": "2026-02-21 00:00", "v": "2.34", "s": "0.003", "f": "0,0,0,0", "q": "p"},
{"t": "2026-02-21 00:06", "v": "2.38", "s": "0.003", "f": "0,0,0,0", "q": "p"},
]
}
MOCK_WIND = {
"data": [
{"t": "2026-02-21 00:00", "s": "12.5", "d": "225.00", "dr": "SW", "g": "18.2", "f": "0,0"}
]
}
MOCK_AIR_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "42.3", "f": "0,0,0"}]}
MOCK_WATER_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "38.7", "f": "0,0,0"}]}
MOCK_PRESSURE = {"data": [{"t": "2026-02-21 00:00", "v": "1013.2", "f": "0,0,0"}]}
# -- Parsing helpers --
def test_parse_predictions():
preds = MOCK_PREDICTIONS["predictions"]
times, values, markers = _parse_predictions(preds)
assert len(times) == 4
assert len(values) == 4
assert len(markers) == 4
assert markers[0]["type"] == "H"
assert markers[1]["type"] == "L"
assert values[0] == pytest.approx(4.521)
def test_parse_observed():
obs = MOCK_WATER_LEVEL["data"]
times, values = _parse_observed(obs)
assert len(times) == 2
assert values[0] == pytest.approx(2.34)
def test_parse_observed_skips_blank_values():
data = [
{"t": "2026-02-21 00:00", "v": "2.34"},
{"t": "2026-02-21 00:06", "v": ""},
{"t": "2026-02-21 00:12", "v": None},
]
times, values = _parse_observed(data)
assert len(times) == 1
# -- Tide chart rendering --
def test_tide_chart_png_returns_bytes():
preds = MOCK_PREDICTIONS["predictions"]
result = render_tide_chart_png(preds, station_name="Test Station")
assert isinstance(result, bytes)
assert len(result) > 0
# PNG magic bytes
assert result[:4] == b"\x89PNG"
def test_tide_chart_png_with_observed():
preds = MOCK_PREDICTIONS["predictions"]
obs = MOCK_WATER_LEVEL["data"]
result = render_tide_chart_png(preds, observed=obs, station_name="Providence")
assert isinstance(result, bytes)
assert result[:4] == b"\x89PNG"
def test_tide_chart_html_returns_string():
preds = MOCK_PREDICTIONS["predictions"]
result = render_tide_chart_html(preds, station_name="Test Station")
assert isinstance(result, str)
assert "<html>" in result.lower() or "<!doctype" in result.lower()
assert "plotly" in result.lower()
def test_tide_chart_html_with_observed():
preds = MOCK_PREDICTIONS["predictions"]
obs = MOCK_WATER_LEVEL["data"]
result = render_tide_chart_html(preds, observed=obs, station_name="Providence")
assert isinstance(result, str)
assert "Observed" in result
# -- Conditions dashboard rendering --
def _build_snapshot(**overrides) -> dict:
"""Build a snapshot dict from mock data."""
snapshot = {
"station_id": "8454000",
"predictions": MOCK_PREDICTIONS,
"water_level": MOCK_WATER_LEVEL,
"wind": MOCK_WIND,
"air_temperature": MOCK_AIR_TEMP,
"water_temperature": MOCK_WATER_TEMP,
"air_pressure": MOCK_PRESSURE,
}
snapshot.update(overrides)
return snapshot
def test_conditions_png_returns_bytes():
snapshot = _build_snapshot()
result = render_conditions_png(snapshot, station_name="Providence")
assert isinstance(result, bytes)
assert result[:4] == b"\x89PNG"
def test_conditions_png_partial_data():
"""Dashboard should render even with missing products."""
snapshot = _build_snapshot()
del snapshot["wind"]
del snapshot["air_temperature"]
result = render_conditions_png(snapshot, station_name="Providence")
assert isinstance(result, bytes)
assert result[:4] == b"\x89PNG"
def test_conditions_png_empty_snapshot():
"""Dashboard with no data produces a placeholder image."""
result = render_conditions_png({"station_id": "8454000"})
assert isinstance(result, bytes)
assert result[:4] == b"\x89PNG"
def test_conditions_html_returns_string():
snapshot = _build_snapshot()
result = render_conditions_html(snapshot, station_name="Providence")
assert isinstance(result, str)
assert "plotly" in result.lower()
def test_conditions_html_empty_snapshot():
result = render_conditions_html({"station_id": "8454000"})
assert isinstance(result, str)
assert "No data available" in result
# -- Tool registration --
async def test_visualization_tools_registered(mcp_client: Client):
"""The 2 new visualization tools should appear in the tool list."""
tools = await mcp_client.list_tools()
names = {t.name for t in tools}
assert "visualize_tides" in names
assert "visualize_conditions" in names
async def test_total_tool_count(mcp_client: Client):
"""Verify total tool count after adding visualization tools (7 + 2 = 9)."""
tools = await mcp_client.list_tools()
assert len(tools) == 9