"""所見の種類 `tongue_color` の値の型(SPEC_02「kinds」,testing.md 必須対象 4).
- 値は `{"label": <slug>, "severity": "strong" | "mild" | "normal"}`
- slug は `pale`・`light_red`・`red`・`dark_red`・`purple`
- `normal` は `light_red` のときだけ
- 未知の値・未知のキー・無効な組み合わせは拒否する
"""
import json
import pytest
from pydantic import ValidationError
from tiasdata.kinds import (
TongueColor,
annotation_value_type,
dump_annotation_value,
validate_annotation_value,
)
LABELS = ["pale", "light_red", "red", "dark_red", "purple"]
NOT_LIGHT_RED = [label for label in LABELS if label != "light_red"]
class TestValidValues:
@pytest.mark.parametrize("label", NOT_LIGHT_RED)
@pytest.mark.parametrize("severity", ["strong", "mild"])
def test_non_light_red_with_strong_or_mild_is_accepted(self, label: str, severity: str) -> None:
value = validate_annotation_value("tongue_color", {"label": label, "severity": severity})
assert isinstance(value, TongueColor)
assert value.label == label
assert value.severity == severity
def test_light_red_normal_is_accepted(self) -> None:
value = validate_annotation_value(
"tongue_color", {"label": "light_red", "severity": "normal"}
)
assert value == TongueColor(label="light_red", severity="normal")
def test_value_type_of_tongue_color(self) -> None:
assert annotation_value_type("tongue_color") is TongueColor
class TestInvalidValues:
@pytest.mark.parametrize("label", NOT_LIGHT_RED)
def test_normal_other_than_light_red_is_rejected(self, label: str) -> None:
with pytest.raises(ValidationError):
validate_annotation_value("tongue_color", {"label": label, "severity": "normal"})
@pytest.mark.parametrize(
"label", ["Red", "pink", "", "淡紅", "light-red", "lightred", "dark red"]
)
def test_unknown_label_is_rejected(self, label: str) -> None:
with pytest.raises(ValidationError):
validate_annotation_value("tongue_color", {"label": label, "severity": "mild"})
@pytest.mark.parametrize("severity", ["Strong", "weak", "", "あり", "none"])
def test_unknown_severity_is_rejected(self, severity: str) -> None:
with pytest.raises(ValidationError):
validate_annotation_value("tongue_color", {"label": "red", "severity": severity})
def test_unknown_key_is_rejected(self) -> None:
with pytest.raises(ValidationError):
validate_annotation_value(
"tongue_color", {"label": "red", "severity": "mild", "note": "x"}
)
@pytest.mark.parametrize("value", [{"label": "red"}, {"severity": "mild"}, {}])
def test_missing_key_is_rejected(self, value: dict) -> None:
with pytest.raises(ValidationError):
validate_annotation_value("tongue_color", value)
@pytest.mark.parametrize(
"value",
[
{"label": 1, "severity": "mild"},
{"label": "red", "severity": None},
{"label": ["red"], "severity": "mild"},
],
)
def test_non_string_value_is_rejected(self, value: dict) -> None:
with pytest.raises(ValidationError):
validate_annotation_value("tongue_color", value)
def test_unknown_annotation_kind_is_value_error(self) -> None:
with pytest.raises(ValueError, match="no_such_kind"):
validate_annotation_value("no_such_kind", {"label": "red", "severity": "mild"})
def test_asset_kind_has_no_value_type(self) -> None:
# ファイルの種類は所見の値の型を持たない
with pytest.raises(ValueError):
annotation_value_type("tongue_crop_masked")
def test_model_is_immutable(self) -> None:
value = TongueColor(label="red", severity="mild")
with pytest.raises(ValidationError):
value.label = "pale" # type: ignore[misc]
class TestDump:
def test_dump_is_json_with_label_and_severity(self) -> None:
text = dump_annotation_value(TongueColor(label="dark_red", severity="strong"))
assert json.loads(text) == {"label": "dark_red", "severity": "strong"}
def test_dump_is_deterministic(self) -> None:
# 同じ値は常に同じ文字列(キーの順・空白が固定)
a = dump_annotation_value(
validate_annotation_value("tongue_color", {"severity": "mild", "label": "pale"})
)
b = dump_annotation_value(
validate_annotation_value("tongue_color", {"label": "pale", "severity": "mild"})
)
assert a == b
def test_dump_round_trips_through_validation(self) -> None:
original = TongueColor(label="purple", severity="mild")
restored = validate_annotation_value(
"tongue_color", json.loads(dump_annotation_value(original))
)
assert restored == original