Tests: 79 total (32 new), covering LRU cache eviction, BBox validation, _parse_rgb error handling, colormap ID extraction, client retry logic, WMS response validation, dimension clamping, geocode caching, classification colormaps, and scientific notation interval parsing. Shutdown: register atexit handler to close httpx connection pool when the MCP server exits, since FastMCP Middleware has no on_shutdown hook.
217 lines
6.7 KiB
Python
217 lines
6.7 KiB
Python
"""Tests for mcgibs.colormaps -- XML parsing and natural-language explanations."""
|
|
|
|
import pytest
|
|
|
|
from mcgibs.colormaps import (
|
|
_describe_rgb,
|
|
_parse_interval_value,
|
|
explain_colormap,
|
|
parse_colormap,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_colormap
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_colormap_map_count(colormap_xml: str):
|
|
"""Sample XML contains exactly 2 ColorMap elements."""
|
|
result = parse_colormap(colormap_xml)
|
|
assert len(result.maps) == 2
|
|
|
|
|
|
def test_parse_colormap_data_entries(colormap_xml: str):
|
|
"""First ColorMap has 14 data entries, correct title and units."""
|
|
result = parse_colormap(colormap_xml)
|
|
first = result.maps[0]
|
|
|
|
assert len(first.entries) == 14
|
|
assert first.title == "Surface Air Temperature"
|
|
assert first.units == "K"
|
|
|
|
|
|
def test_parse_colormap_nodata_entry(colormap_xml: str):
|
|
"""Second ColorMap has a nodata entry labelled 'Missing Data'."""
|
|
result = parse_colormap(colormap_xml)
|
|
second = result.maps[1]
|
|
|
|
nodata = [e for e in second.entries if e.nodata]
|
|
assert len(nodata) == 1
|
|
assert nodata[0].label == "Missing Data"
|
|
|
|
|
|
def test_parse_colormap_rgb_values(colormap_xml: str):
|
|
"""First entry of the first ColorMap has rgb (227, 245, 255)."""
|
|
result = parse_colormap(colormap_xml)
|
|
first_entry = result.maps[0].entries[0]
|
|
assert first_entry.rgb == (227, 245, 255)
|
|
|
|
|
|
def test_parse_colormap_value_intervals(colormap_xml: str):
|
|
"""First entry value is '[-INF,200.0)', last entry is '[320.0,+INF)'."""
|
|
result = parse_colormap(colormap_xml)
|
|
entries = result.maps[0].entries
|
|
|
|
assert entries[0].value == "[-INF,200.0)"
|
|
assert entries[-1].value == "[320.0,+INF)"
|
|
|
|
|
|
def test_parse_colormap_legend(colormap_xml: str):
|
|
"""First ColorMap has legend entries with expected tooltips."""
|
|
result = parse_colormap(colormap_xml)
|
|
legend = result.maps[0].legend
|
|
|
|
assert len(legend) == 5
|
|
tooltips = [le.tooltip for le in legend]
|
|
assert "< 200 K" in tooltips
|
|
assert "200 - 210 K" in tooltips
|
|
assert "> 320 K" in tooltips
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _parse_interval_value
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_interval_value_bounded():
|
|
"""Bounded interval '[200.0,200.5)' parses to (200.0, 200.5)."""
|
|
assert _parse_interval_value("[200.0,200.5)") == (200.0, 200.5)
|
|
|
|
|
|
def test_parse_interval_value_neg_inf():
|
|
"""Negative infinity '[-INF,200.0)' parses to (None, 200.0)."""
|
|
assert _parse_interval_value("[-INF,200.0)") == (None, 200.0)
|
|
|
|
|
|
def test_parse_interval_value_pos_inf():
|
|
"""Positive infinity '[320.0,+INF)' parses to (320.0, None)."""
|
|
assert _parse_interval_value("[320.0,+INF)") == (320.0, None)
|
|
|
|
|
|
def test_parse_interval_value_single():
|
|
"""Single value '[42]' parses to (42.0, 42.0)."""
|
|
assert _parse_interval_value("[42]") == (42.0, 42.0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# explain_colormap
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_explain_colormap_includes_title(colormap_xml: str):
|
|
"""Explanation text contains the layer title."""
|
|
cms = parse_colormap(colormap_xml)
|
|
text = explain_colormap(cms)
|
|
assert "Surface Air Temperature" in text
|
|
|
|
|
|
def test_explain_colormap_includes_units(colormap_xml: str):
|
|
"""Explanation mentions the native unit and Celsius conversion."""
|
|
cms = parse_colormap(colormap_xml)
|
|
text = explain_colormap(cms)
|
|
|
|
assert "(K)" in text
|
|
assert "C)" in text # Celsius conversion appears as e.g. "(-73 C)"
|
|
|
|
|
|
def test_explain_colormap_nodata_mention(colormap_xml: str):
|
|
"""Explanation mentions 'Missing Data' from the nodata entry."""
|
|
cms = parse_colormap(colormap_xml)
|
|
text = explain_colormap(cms)
|
|
assert "Missing Data" in text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _describe_rgb
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("rgb", "expected_substring"),
|
|
[
|
|
((255, 0, 0), "red"),
|
|
((0, 128, 0), "green"),
|
|
((0, 0, 255), "blue"),
|
|
((255, 255, 255), "white"),
|
|
((0, 0, 0), "black"),
|
|
((255, 255, 0), "yellow"),
|
|
],
|
|
)
|
|
def test_describe_rgb_basic(rgb: tuple[int, int, int], expected_substring: str):
|
|
"""Known RGB triples produce color names containing the expected word."""
|
|
result = _describe_rgb(rgb)
|
|
assert expected_substring in result.lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _parse_rgb error handling (H3 Hamilton fix)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_rgb_valid():
|
|
from mcgibs.colormaps import _parse_rgb
|
|
|
|
assert _parse_rgb("255,128,0") == (255, 128, 0)
|
|
|
|
|
|
def test_parse_rgb_too_few_parts():
|
|
from mcgibs.colormaps import _parse_rgb
|
|
|
|
assert _parse_rgb("255,128") == (0, 0, 0)
|
|
|
|
|
|
def test_parse_rgb_empty_string():
|
|
from mcgibs.colormaps import _parse_rgb
|
|
|
|
assert _parse_rgb("") == (0, 0, 0)
|
|
|
|
|
|
def test_parse_rgb_non_numeric():
|
|
from mcgibs.colormaps import _parse_rgb
|
|
|
|
assert _parse_rgb("abc,def,ghi") == (0, 0, 0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _parse_interval_value edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_parse_interval_value_bare_number():
|
|
"""Bare number without brackets (used in sea ice colormaps)."""
|
|
assert _parse_interval_value("42") == (42.0, 42.0)
|
|
|
|
|
|
def test_parse_interval_value_empty_string():
|
|
assert _parse_interval_value("") == (None, None)
|
|
|
|
|
|
def test_parse_interval_value_scientific_notation():
|
|
low, high = _parse_interval_value("[1.5e2,2.0e2)")
|
|
assert low == 150.0
|
|
assert high == 200.0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# explain_colormap: classification colormap
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_explain_colormap_classification():
|
|
"""Classification colormaps should produce categorical descriptions."""
|
|
from mcgibs.models import ColorMap, ColorMapEntry, ColorMapSet
|
|
|
|
entries = [
|
|
ColorMapEntry(rgb=(0, 0, 255), label="Water"),
|
|
ColorMapEntry(rgb=(0, 128, 0), label="Forest"),
|
|
ColorMapEntry(rgb=(255, 255, 0), label="Desert"),
|
|
]
|
|
cm = ColorMap(title="Land Cover", legend_type="classification", entries=entries)
|
|
cms = ColorMapSet(maps=[cm])
|
|
|
|
text = explain_colormap(cms)
|
|
assert "Land Cover" in text
|
|
assert "classification" in text
|
|
assert "Water" in text
|
|
assert "Forest" in text
|