188 lines
7.0 KiB
Python
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}"
|
|
)
|