Files
veritext/tests/test_validators/test_constraint.py
Kai Chappell 7f23586406 validator tests
Add comprehensive tests for metric validators, constraint validators,
and composite validators covering pass/fail cases and error handling.
2025-03-29 13:30:23 +00:00

301 lines
11 KiB
Python

"""Tests for constraint validators."""
import pytest
from veritext.core.exceptions import InvalidThresholdError
from veritext.core.types import ValidationContext
from veritext.validators import contains, excludes, length, readability
from veritext.validators.constraint import (
ContainsValidator,
ExcludesValidator,
LengthValidator,
ReadabilityValidator,
)
class TestLengthValidator:
def test_min_chars_passes(self) -> None:
validator = LengthValidator(min_chars=10)
context = ValidationContext()
result = validator.check("hello world!", context)
assert result.passed is True
assert result.name == "length"
assert result.actual["chars"] == 12
def test_min_chars_fails(self) -> None:
validator = LengthValidator(min_chars=20)
context = ValidationContext()
result = validator.check("hello", context)
assert result.passed is False
assert "< min" in result.message
def test_max_chars_passes(self) -> None:
validator = LengthValidator(max_chars=20)
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is True
assert result.actual["chars"] == 11
def test_max_chars_fails(self) -> None:
validator = LengthValidator(max_chars=5)
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is False
assert "> max" in result.message
def test_min_words_passes(self) -> None:
validator = LengthValidator(min_words=3)
context = ValidationContext()
result = validator.check("the quick brown fox", context)
assert result.passed is True
assert result.actual["words"] == 4
def test_min_words_fails(self) -> None:
validator = LengthValidator(min_words=10)
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is False
assert "words < min" in result.message
def test_max_words_passes(self) -> None:
validator = LengthValidator(max_words=5)
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is True
def test_max_words_fails(self) -> None:
validator = LengthValidator(max_words=2)
context = ValidationContext()
result = validator.check("the quick brown fox", context)
assert result.passed is False
assert "words > max" in result.message
def test_combined_constraints(self) -> None:
validator = LengthValidator(
min_chars=5, max_chars=50, min_words=2, max_words=10
)
context = ValidationContext()
result = validator.check("the quick brown fox", context)
assert result.passed is True
assert "min_chars" in result.threshold
assert "max_chars" in result.threshold
assert "min_words" in result.threshold
assert "max_words" in result.threshold
def test_requires_constraints(self) -> None:
with pytest.raises(InvalidThresholdError, match="At least one"):
LengthValidator()
def test_rejects_negative_values(self) -> None:
with pytest.raises(InvalidThresholdError, match="min_chars must be >= 0"):
LengthValidator(min_chars=-1)
with pytest.raises(InvalidThresholdError, match="max_chars must be >= 0"):
LengthValidator(max_chars=-1)
with pytest.raises(InvalidThresholdError, match="min_words must be >= 0"):
LengthValidator(min_words=-1)
with pytest.raises(InvalidThresholdError, match="max_words must be >= 0"):
LengthValidator(max_words=-1)
def test_rejects_inverted_range(self) -> None:
with pytest.raises(InvalidThresholdError, match="cannot exceed max_chars"):
LengthValidator(min_chars=100, max_chars=50)
with pytest.raises(InvalidThresholdError, match="cannot exceed max_words"):
LengthValidator(min_words=20, max_words=5)
def test_length_factory(self) -> None:
validator = length(min_chars=10, max_words=100)
assert isinstance(validator, LengthValidator)
assert validator.name == "length"
class TestReadabilityValidator:
def test_max_grade_passes(self) -> None:
validator = ReadabilityValidator(max_grade=12.0)
context = ValidationContext()
# Simple text should have low grade level
result = validator.check("The cat sat on the mat. It was a nice day.", context)
assert result.passed is True
assert result.name == "readability"
assert "grade" in result.actual
def test_max_grade_fails(self) -> None:
validator = ReadabilityValidator(max_grade=1.0)
context = ValidationContext()
# Complex text
result = validator.check(
"The implementation of sophisticated methodologies necessitates "
"detailed analytical frameworks for systematic evaluation.",
context,
)
assert result.passed is False
assert "grade level" in result.message
assert "> max" in result.message
def test_min_ease_passes(self) -> None:
validator = ReadabilityValidator(min_ease=30.0)
context = ValidationContext()
# Simple text should have high reading ease
result = validator.check("The cat sat. The dog ran. It was fun.", context)
assert result.passed is True
assert "ease" in result.actual
def test_min_ease_fails(self) -> None:
validator = ReadabilityValidator(min_ease=100.0)
context = ValidationContext()
result = validator.check(
"The implementation of sophisticated methodologies necessitates "
"detailed analytical frameworks.",
context,
)
assert result.passed is False
assert "reading ease" in result.message
assert "< min" in result.message
def test_combined_grade_and_ease(self) -> None:
validator = ReadabilityValidator(max_grade=12.0, min_ease=30.0)
context = ValidationContext()
result = validator.check("The cat sat on the mat.", context)
assert "max_grade" in result.threshold
assert "min_ease" in result.threshold
def test_requires_constraints(self) -> None:
with pytest.raises(InvalidThresholdError, match="At least one"):
ReadabilityValidator()
def test_readability_factory(self) -> None:
validator = readability(max_grade=8.0, min_ease=60.0)
assert isinstance(validator, ReadabilityValidator)
assert validator.name == "readability"
class TestContainsValidator:
def test_finds_all_patterns(self) -> None:
validator = ContainsValidator(patterns=["hello", "world"])
context = ValidationContext()
result = validator.check("Hello World!", context)
assert result.passed is True
assert result.name == "contains"
assert result.actual["found"] == 2
assert result.actual["missing"] == []
def test_fails_on_missing_pattern(self) -> None:
validator = ContainsValidator(patterns=["hello", "goodbye"])
context = ValidationContext()
result = validator.check("Hello World!", context)
assert result.passed is False
assert "goodbye" in result.actual["missing"]
assert "missing" in result.message
def test_case_insensitive_default(self) -> None:
validator = ContainsValidator(patterns=["HELLO"])
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is True
def test_case_sensitive(self) -> None:
validator = ContainsValidator(patterns=["HELLO"], case_sensitive=True)
context = ValidationContext()
result = validator.check("hello world", context)
assert result.passed is False
def test_regex_patterns(self) -> None:
validator = ContainsValidator(patterns=[r"\d{3}-\d{4}"])
context = ValidationContext()
result = validator.check("Call 555-1234 for info", context)
assert result.passed is True
def test_rejects_empty_patterns(self) -> None:
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
ContainsValidator(patterns=[])
def test_rejects_invalid_regex(self) -> None:
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
ContainsValidator(patterns=[r"[invalid"])
def test_contains_factory(self) -> None:
validator = contains(patterns=["test"], case_sensitive=True)
assert isinstance(validator, ContainsValidator)
assert validator.name == "contains"
class TestExcludesValidator:
def test_passes_when_absent(self) -> None:
validator = ExcludesValidator(patterns=["bad", "forbidden"])
context = ValidationContext()
result = validator.check("This is good text.", context)
assert result.passed is True
assert result.name == "excludes"
assert result.actual["found"] == []
def test_fails_when_found(self) -> None:
validator = ExcludesValidator(patterns=["bad", "forbidden"])
context = ValidationContext()
result = validator.check("This is bad text.", context)
assert result.passed is False
assert "bad" in result.actual["found"]
assert "forbidden" in result.message
def test_case_insensitive_default(self) -> None:
validator = ExcludesValidator(patterns=["BAD"])
context = ValidationContext()
result = validator.check("This is bad text.", context)
assert result.passed is False
def test_case_sensitive(self) -> None:
validator = ExcludesValidator(patterns=["BAD"], case_sensitive=True)
context = ValidationContext()
result = validator.check("This is bad text.", context)
assert result.passed is True
def test_regex_patterns(self) -> None:
validator = ExcludesValidator(patterns=[r"\b\d{4}\b"]) # 4-digit numbers
context = ValidationContext()
# Should fail when pattern found
result = validator.check("PIN is 1234", context)
assert result.passed is False
# Should pass when pattern absent
result = validator.check("No numbers here", context)
assert result.passed is True
def test_rejects_empty_patterns(self) -> None:
with pytest.raises(InvalidThresholdError, match="cannot be empty"):
ExcludesValidator(patterns=[])
def test_rejects_invalid_regex(self) -> None:
with pytest.raises(InvalidThresholdError, match="Invalid regex"):
ExcludesValidator(patterns=[r"[invalid"])
def test_excludes_factory(self) -> None:
validator = excludes(patterns=["test"], case_sensitive=True)
assert isinstance(validator, ExcludesValidator)
assert validator.name == "excludes"