Files
2026-06-30 20:37:40 +02:00

188 lines
7.0 KiB
Python

"""Property-basierte Tests für den StructureManager.
**Validates: Requirements 1.3**
Property 1: Namensvalidierung akzeptiert nur gültiges kebab-case
- For any String, die validate_name-Funktion soll genau dann true zurückgeben,
wenn der String ausschließlich aus Kleinbuchstaben, Ziffern und Bindestrichen besteht,
zwischen 2 und 50 Zeichen lang ist und nicht mit einem Bindestrich beginnt oder endet.
"""
from __future__ import annotations
import string
from pathlib import Path
from hypothesis import given, assume, settings
from hypothesis import strategies as st
from monorepo.structure import StructureManager
# --- Strategies ---
# Valid kebab-case characters
VALID_CHARS = string.ascii_lowercase + string.digits + "-"
VALID_START_END_CHARS = string.ascii_lowercase + string.digits
@st.composite
def valid_kebab_names(draw: st.DrawFn) -> str:
"""Generates valid kebab-case names: 2-50 chars, [a-z0-9-], no leading/trailing dash."""
length = draw(st.integers(min_value=2, max_value=50))
# First and last char: no dash allowed
first = draw(st.sampled_from(VALID_START_END_CHARS))
if length == 2:
last = draw(st.sampled_from(VALID_START_END_CHARS))
return first + last
last = draw(st.sampled_from(VALID_START_END_CHARS))
# Middle chars can include dashes
middle = draw(st.text(alphabet=VALID_CHARS, min_size=length - 2, max_size=length - 2))
return first + middle + last
@st.composite
def invalid_names_wrong_chars(draw: st.DrawFn) -> str:
"""Generates names containing at least one invalid character (not [a-z0-9-])."""
# Generate a base valid-ish name, then inject an invalid char
invalid_char = draw(
st.sampled_from(
list(set(string.printable) - set(VALID_CHARS) - set(string.whitespace))
+ ["Ä", "ö", "ü", "€", " ", "_", ".", "A", "Z"]
)
)
# Place the invalid char in a string of valid length
length = draw(st.integers(min_value=2, max_value=50))
position = draw(st.integers(min_value=0, max_value=length - 1))
chars = []
for i in range(length):
if i == position:
chars.append(invalid_char)
else:
chars.append(draw(st.sampled_from(VALID_START_END_CHARS)))
return "".join(chars)
@st.composite
def invalid_names_too_short(draw: st.DrawFn) -> str:
"""Generates names that are too short (0 or 1 character)."""
length = draw(st.integers(min_value=0, max_value=1))
return draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length, max_size=length))
@st.composite
def invalid_names_too_long(draw: st.DrawFn) -> str:
"""Generates names that are too long (>50 characters)."""
length = draw(st.integers(min_value=51, max_value=100))
first = draw(st.sampled_from(VALID_START_END_CHARS))
last = draw(st.sampled_from(VALID_START_END_CHARS))
middle = draw(st.text(alphabet=VALID_CHARS, min_size=length - 2, max_size=length - 2))
return first + middle + last
@st.composite
def invalid_names_leading_dash(draw: st.DrawFn) -> str:
"""Generates names that start with a dash."""
length = draw(st.integers(min_value=2, max_value=50))
rest = draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length - 1, max_size=length - 1))
return "-" + rest
@st.composite
def invalid_names_trailing_dash(draw: st.DrawFn) -> str:
"""Generates names that end with a dash."""
length = draw(st.integers(min_value=2, max_value=50))
rest = draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length - 1, max_size=length - 1))
return rest + "-"
# --- Tests ---
manager = StructureManager(root_path=Path("/tmp/test-monorepo"))
class TestProperty1NameValidation:
"""Property 1: Namensvalidierung akzeptiert nur gültiges kebab-case.
**Validates: Requirements 1.3**
"""
@given(name=valid_kebab_names())
@settings(max_examples=200)
def test_valid_names_are_accepted(self, name: str) -> None:
"""Valid kebab-case names (2-50 chars, [a-z0-9-], no leading/trailing dash)
must be accepted by validate_name."""
assert manager.validate_name(name) is True, (
f"validate_name rejected valid name: '{name}'"
)
@given(name=invalid_names_wrong_chars())
@settings(max_examples=200)
def test_names_with_invalid_chars_are_rejected(self, name: str) -> None:
"""Names containing characters outside [a-z0-9-] must be rejected."""
assert manager.validate_name(name) is False, (
f"validate_name accepted name with invalid chars: '{name}'"
)
@given(name=invalid_names_too_short())
@settings(max_examples=50)
def test_names_too_short_are_rejected(self, name: str) -> None:
"""Names shorter than 2 characters must be rejected."""
assert manager.validate_name(name) is False, (
f"validate_name accepted too-short name: '{name}'"
)
@given(name=invalid_names_too_long())
@settings(max_examples=200)
def test_names_too_long_are_rejected(self, name: str) -> None:
"""Names longer than 50 characters must be rejected."""
assert manager.validate_name(name) is False, (
f"validate_name accepted too-long name: '{name}'"
)
@given(name=invalid_names_leading_dash())
@settings(max_examples=100)
def test_names_starting_with_dash_are_rejected(self, name: str) -> None:
"""Names starting with a dash must be rejected."""
assert manager.validate_name(name) is False, (
f"validate_name accepted name starting with dash: '{name}'"
)
@given(name=invalid_names_trailing_dash())
@settings(max_examples=100)
def test_names_ending_with_dash_are_rejected(self, name: str) -> None:
"""Names ending with a dash must be rejected."""
assert manager.validate_name(name) is False, (
f"validate_name accepted name ending with dash: '{name}'"
)
@given(name=st.text(min_size=0, max_size=100))
@settings(max_examples=500)
def test_validate_name_biconditional(self, name: str) -> None:
"""For any string, validate_name returns True if and only if:
- The string consists exclusively of [a-z0-9-]
- Has length between 2 and 50 (inclusive)
- Does not start or end with a dash
This is the full biconditional property check.
"""
# Compute expected result from first principles
is_valid_chars = all(c in VALID_CHARS for c in name)
is_valid_length = 2 <= len(name) <= 50
no_leading_dash = len(name) > 0 and name[0] != "-"
no_trailing_dash = len(name) > 0 and name[-1] != "-"
expected = (
is_valid_chars
and is_valid_length
and no_leading_dash
and no_trailing_dash
)
result = manager.validate_name(name)
assert result == expected, (
f"validate_name('{name}') returned {result}, expected {expected}. "
f"chars_valid={is_valid_chars}, length_valid={is_valid_length}, "
f"no_lead_dash={no_leading_dash}, no_trail_dash={no_trailing_dash}"
)