Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""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}"
|
||||
)
|
||||
Reference in New Issue
Block a user