Distribution name, import package, entry point script, MCP config, and all internal references updated. Git tracks the directory rename so file history is preserved.
176 lines
5.4 KiB
Python
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
|