155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
"""Tests for the public ``tono`` library API surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from typing import Literal, get_args, get_origin
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import tono
|
|
from backend.data_types import DataField, RecordTable
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_field() -> DataField:
|
|
"""A small deterministic field for fast tests."""
|
|
rng = np.random.default_rng(0)
|
|
data = rng.standard_normal((32, 32)).astype(np.float64)
|
|
return tono.field(data, xreal=1e-6, yreal=1e-6)
|
|
|
|
|
|
# ── Typed call syntax ───────────────────────────────────────────────────
|
|
|
|
def test_gaussian_filter_call(sample_field):
|
|
result = tono.GaussianFilter(sample_field, sigma=2.0)
|
|
assert isinstance(result, DataField)
|
|
assert result.data.shape == sample_field.data.shape
|
|
|
|
|
|
def test_default_filling_from_metadata(sample_field):
|
|
# sigma has a metadata default of 1.0 — call should succeed without it.
|
|
result = tono.GaussianFilter(sample_field)
|
|
assert isinstance(result, DataField)
|
|
|
|
|
|
def test_keyword_only_call(sample_field):
|
|
result = tono.GaussianFilter(field=sample_field, sigma=0.5)
|
|
assert isinstance(result, DataField)
|
|
|
|
|
|
def test_enum_input_call(sample_field):
|
|
result = tono.EdgeDetect(sample_field, method="sobel", sigma=1.0)
|
|
assert isinstance(result, DataField)
|
|
|
|
|
|
def test_multi_output_returns_tuple(sample_field):
|
|
out = tono.FFT2D(sample_field, windowing="hann", level="mean")
|
|
assert isinstance(out, tuple)
|
|
assert len(out) == 4
|
|
assert all(isinstance(x, DataField) for x in out)
|
|
|
|
|
|
def test_awkward_required_order(sample_field):
|
|
# ThresholdMask has ``threshold`` (default 0.0) followed by ``direction``
|
|
# (no default). The wrapper must handle this without raising, and the
|
|
# metadata default for threshold must still fire at call time.
|
|
mask, record = tono.ThresholdMask(
|
|
sample_field, method="otsu", direction="above"
|
|
)
|
|
assert mask.shape == sample_field.data.shape
|
|
assert isinstance(record, RecordTable)
|
|
|
|
|
|
# ── Signature introspection ─────────────────────────────────────────────
|
|
|
|
def test_signature_has_expected_params():
|
|
sig = inspect.signature(tono.GaussianFilter)
|
|
assert list(sig.parameters) == ["field", "sigma"]
|
|
assert sig.parameters["sigma"].default == 1.0
|
|
assert sig.parameters["field"].annotation is DataField
|
|
|
|
|
|
def test_signature_enum_uses_literal():
|
|
sig = inspect.signature(tono.EdgeDetect)
|
|
annotation = sig.parameters["method"].annotation
|
|
assert get_origin(annotation) is Literal
|
|
assert set(get_args(annotation)) == {"sobel", "prewitt", "laplacian", "log"}
|
|
|
|
|
|
def test_signature_return_annotation_single_output():
|
|
sig = inspect.signature(tono.GaussianFilter)
|
|
assert sig.return_annotation is DataField
|
|
|
|
|
|
def test_signature_return_annotation_multi_output():
|
|
sig = inspect.signature(tono.FFT2D)
|
|
assert sig.return_annotation is tuple
|
|
|
|
|
|
def test_docstring_contains_description_and_params():
|
|
doc = tono.EdgeDetect.__doc__ or ""
|
|
assert "Sobel" in doc or "sobel" in doc
|
|
assert "field" in doc
|
|
assert "method" in doc
|
|
assert "sigma" in doc
|
|
|
|
|
|
# ── Module-level dunder behaviour ───────────────────────────────────────
|
|
|
|
def test_dir_lists_nodes():
|
|
entries = dir(tono)
|
|
assert "GaussianFilter" in entries
|
|
assert "EdgeDetect" in entries
|
|
assert "apply" in entries # existing top-level export
|
|
assert "describe" in entries
|
|
|
|
|
|
def test_unknown_attribute_raises_attribute_error():
|
|
with pytest.raises(AttributeError, match="NotARealNode"):
|
|
tono.NotARealNode # noqa: B018
|
|
|
|
|
|
def test_dunder_lookup_raises_immediately():
|
|
# __getattr__ should short-circuit dunder lookups so that e.g. copy/pickle
|
|
# machinery doesn't trigger registry loading.
|
|
with pytest.raises(AttributeError):
|
|
tono.__some_dunder__ # noqa: B018
|
|
|
|
|
|
def test_wrappers_are_cached():
|
|
first = tono.GaussianFilter
|
|
second = tono.GaussianFilter
|
|
assert first is second
|
|
|
|
|
|
# ── describe() ──────────────────────────────────────────────────────────
|
|
|
|
def test_describe_returns_expected_keys():
|
|
info = tono.describe("EdgeDetect")
|
|
assert info["name"] == "EdgeDetect"
|
|
assert "input" in info
|
|
assert "output" in info
|
|
assert "description" in info
|
|
assert "field" in info["input"]["required"]
|
|
|
|
|
|
def test_describe_unknown_node_raises():
|
|
with pytest.raises(KeyError):
|
|
tono.describe("NotARealNode")
|
|
|
|
|
|
# ── apply() still works and matches wrapper semantics ───────────────────
|
|
|
|
def test_apply_default_filling(sample_field):
|
|
# apply() should also fill metadata defaults now.
|
|
result = tono.apply("GaussianFilter", sample_field)
|
|
assert isinstance(result, DataField)
|
|
|
|
|
|
def test_apply_and_wrapper_are_equivalent(sample_field):
|
|
via_apply = tono.apply("GaussianFilter", sample_field, sigma=1.5)
|
|
via_wrapper = tono.GaussianFilter(sample_field, sigma=1.5)
|
|
np.testing.assert_array_equal(via_apply.data, via_wrapper.data)
|