Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
"""Unit-Tests für den StructureManager.
|
||||
|
||||
Testet die Verwaltung der 3-Ebenen-Ordnerhierarchie:
|
||||
- Namensvalidierung (kebab-case, 2-50 Zeichen)
|
||||
- Projekterstellung in allen vier Kontexten
|
||||
- Fehlermeldung bei existierendem Projektnamen
|
||||
- Edge Cases: Minimum-/Maximum-Länge, Sonderzeichen, Unicode
|
||||
|
||||
Validates: Requirements 1.1, 1.2, 1.3, 1.6
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from monorepo.structure import StructureManager
|
||||
from monorepo.models import ProjectInfo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager(tmp_path: Path) -> StructureManager:
|
||||
"""Erstellt einen StructureManager mit einem temporären Root-Verzeichnis."""
|
||||
return StructureManager(root_path=tmp_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. validate_name: gültige kebab-case Namen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestValidateNameAccepts:
|
||||
"""Testet, dass validate_name gültige Namen akzeptiert."""
|
||||
|
||||
def test_simple_name(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my-project") is True
|
||||
|
||||
def test_minimum_length_two_chars(self, manager: StructureManager) -> None:
|
||||
"""Minimum-Länge: 2 Zeichen (z.B. 'ab')."""
|
||||
assert manager.validate_name("ab") is True
|
||||
|
||||
def test_maximum_length_fifty_chars(self, manager: StructureManager) -> None:
|
||||
"""Maximum-Länge: 50 Zeichen."""
|
||||
name = "a" * 50
|
||||
assert manager.validate_name(name) is True
|
||||
|
||||
def test_starts_with_digit(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("1project") is True
|
||||
|
||||
def test_ends_with_digit(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("project1") is True
|
||||
|
||||
def test_only_digits(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("12") is True
|
||||
|
||||
def test_with_hyphens(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my-cool-project") is True
|
||||
|
||||
def test_mixed_letters_digits_hyphens(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("a1-b2-c3") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. validate_name: Ablehnungen (ungültige Namen)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestValidateNameRejects:
|
||||
"""Testet, dass validate_name ungültige Namen ablehnt."""
|
||||
|
||||
def test_single_char(self, manager: StructureManager) -> None:
|
||||
"""Einzelnes Zeichen ist zu kurz (< 2)."""
|
||||
assert manager.validate_name("a") is False
|
||||
|
||||
def test_fifty_one_chars(self, manager: StructureManager) -> None:
|
||||
"""51 Zeichen überschreitet Maximum."""
|
||||
name = "a" * 51
|
||||
assert manager.validate_name(name) is False
|
||||
|
||||
def test_starts_with_hyphen(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("-project") is False
|
||||
|
||||
def test_ends_with_hyphen(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("project-") is False
|
||||
|
||||
def test_uppercase_letters(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("MyProject") is False
|
||||
|
||||
def test_spaces(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my project") is False
|
||||
|
||||
def test_underscores(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my_project") is False
|
||||
|
||||
def test_unicode_characters(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("mein-prüfprojekt") is False
|
||||
|
||||
def test_empty_string(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("") is False
|
||||
|
||||
def test_only_hyphens(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("---") is False
|
||||
|
||||
def test_special_characters(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my@project") is False
|
||||
|
||||
def test_dots(self, manager: StructureManager) -> None:
|
||||
assert manager.validate_name("my.project") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. create_project: Erstellung in jedem der 4 Kontexte
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateProjectContexts:
|
||||
"""Testet Projekterstellung in allen vier Kontexten."""
|
||||
|
||||
@pytest.mark.parametrize("context", ["privat", "dhive", "bahn", "shared"])
|
||||
def test_create_project_in_each_context(
|
||||
self, manager: StructureManager, context: str
|
||||
) -> None:
|
||||
"""Projekt kann in jedem gültigen Kontext angelegt werden."""
|
||||
result = manager.create_project(context, "test-project")
|
||||
|
||||
assert result.exists()
|
||||
assert result.is_dir()
|
||||
assert result == manager.root_path / context / "test-project"
|
||||
|
||||
@pytest.mark.parametrize("context", ["privat", "dhive", "bahn", "shared"])
|
||||
def test_create_project_returns_correct_path(
|
||||
self, manager: StructureManager, context: str
|
||||
) -> None:
|
||||
"""Der zurückgegebene Pfad zeigt auf den erstellten Ordner."""
|
||||
result = manager.create_project(context, "my-app")
|
||||
|
||||
expected = manager.root_path / context / "my-app"
|
||||
assert result == expected
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. create_project: ValueError bei ungültigem Kontext
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateProjectInvalidContext:
|
||||
"""Testet Fehlermeldung bei ungültigem Kontext."""
|
||||
|
||||
def test_invalid_context_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Kontext"):
|
||||
manager.create_project("invalid", "my-project")
|
||||
|
||||
def test_empty_context_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Kontext"):
|
||||
manager.create_project("", "my-project")
|
||||
|
||||
def test_capitalized_context_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Kontext"):
|
||||
manager.create_project("Privat", "my-project")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. create_project: ValueError bei ungültigem Projektnamen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateProjectInvalidName:
|
||||
"""Testet Fehlermeldung bei ungültigem Projektnamen."""
|
||||
|
||||
def test_invalid_name_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Projektname"):
|
||||
manager.create_project("privat", "My_Project")
|
||||
|
||||
def test_single_char_name_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Projektname"):
|
||||
manager.create_project("privat", "a")
|
||||
|
||||
def test_empty_name_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
with pytest.raises(ValueError, match="Ungültiger Projektname"):
|
||||
manager.create_project("privat", "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. create_project: ValueError bei doppeltem Projektnamen (Namenskonflikt)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCreateProjectDuplicate:
|
||||
"""Testet die Fehlermeldung bei existierendem Projektnamen."""
|
||||
|
||||
def test_duplicate_raises_valueerror(self, manager: StructureManager) -> None:
|
||||
"""Zweites Anlegen des gleichen Projekts schlägt fehl."""
|
||||
manager.create_project("privat", "my-project")
|
||||
|
||||
with pytest.raises(ValueError, match="Namenskonflikt"):
|
||||
manager.create_project("privat", "my-project")
|
||||
|
||||
def test_duplicate_error_mentions_name(self, manager: StructureManager) -> None:
|
||||
"""Die Fehlermeldung nennt den kollidierenden Namen."""
|
||||
manager.create_project("dhive", "existing-app")
|
||||
|
||||
with pytest.raises(ValueError, match="existing-app"):
|
||||
manager.create_project("dhive", "existing-app")
|
||||
|
||||
def test_same_name_different_context_works(self, manager: StructureManager) -> None:
|
||||
"""Gleicher Name in verschiedenen Kontexten ist erlaubt."""
|
||||
path1 = manager.create_project("privat", "my-tool")
|
||||
path2 = manager.create_project("dhive", "my-tool")
|
||||
|
||||
assert path1.exists()
|
||||
assert path2.exists()
|
||||
assert path1 != path2
|
||||
|
||||
def test_duplicate_does_not_modify_existing(self, manager: StructureManager) -> None:
|
||||
"""Ein fehlgeschlagener Duplikat-Versuch verändert nichts."""
|
||||
path = manager.create_project("bahn", "my-service")
|
||||
# Datei im bestehenden Projekt erstellen
|
||||
marker = path / "marker.txt"
|
||||
marker.write_text("original")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
manager.create_project("bahn", "my-service")
|
||||
|
||||
# Bestehender Inhalt bleibt unverändert
|
||||
assert marker.read_text() == "original"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. list_projects: Auflistung und Filterung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListProjects:
|
||||
"""Testet die Projektauflistung und Kontextfilterung."""
|
||||
|
||||
def test_list_all_projects(self, manager: StructureManager) -> None:
|
||||
"""Listet alle Projekte aus allen Kontexten."""
|
||||
manager.create_project("privat", "app-a")
|
||||
manager.create_project("dhive", "app-b")
|
||||
manager.create_project("bahn", "app-c")
|
||||
|
||||
projects = manager.list_projects()
|
||||
|
||||
names = [p.name for p in projects]
|
||||
assert "app-a" in names
|
||||
assert "app-b" in names
|
||||
assert "app-c" in names
|
||||
|
||||
def test_list_filtered_by_context(self, manager: StructureManager) -> None:
|
||||
"""Filterung nach Kontext liefert nur Projekte des Kontexts."""
|
||||
manager.create_project("privat", "priv-app")
|
||||
manager.create_project("dhive", "dhive-app")
|
||||
|
||||
projects = manager.list_projects(context="privat")
|
||||
|
||||
names = [p.name for p in projects]
|
||||
assert "priv-app" in names
|
||||
assert "dhive-app" not in names
|
||||
|
||||
def test_list_returns_project_info(self, manager: StructureManager) -> None:
|
||||
"""Jedes Element enthält name, context und path."""
|
||||
manager.create_project("shared", "common-lib")
|
||||
|
||||
projects = manager.list_projects(context="shared")
|
||||
|
||||
assert len(projects) == 1
|
||||
proj = projects[0]
|
||||
assert proj.name == "common-lib"
|
||||
assert proj.context == "shared"
|
||||
assert proj.path == manager.root_path / "shared" / "common-lib"
|
||||
|
||||
def test_list_empty_when_no_projects(self, manager: StructureManager) -> None:
|
||||
"""Leere Liste wenn keine Projekte existieren."""
|
||||
projects = manager.list_projects()
|
||||
assert projects == []
|
||||
|
||||
def test_list_invalid_context_raises(self, manager: StructureManager) -> None:
|
||||
"""Ungültiger Kontextfilter löst ValueError aus."""
|
||||
with pytest.raises(ValueError, match="Ungültiger Kontext"):
|
||||
manager.list_projects(context="invalid")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. list_projects: Überspringt versteckte Verzeichnisse
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListProjectsSkipsHidden:
|
||||
"""Testet, dass versteckte Verzeichnisse (. prefix) übersprungen werden."""
|
||||
|
||||
def test_skips_dotfiles(self, manager: StructureManager) -> None:
|
||||
"""Verzeichnisse mit . Prefix werden nicht gelistet."""
|
||||
manager.create_project("privat", "visible-app")
|
||||
# Verstecktes Verzeichnis manuell erstellen
|
||||
hidden_dir = manager.root_path / "privat" / ".hidden-dir"
|
||||
hidden_dir.mkdir(parents=True)
|
||||
|
||||
projects = manager.list_projects(context="privat")
|
||||
|
||||
names = [p.name for p in projects]
|
||||
assert "visible-app" in names
|
||||
assert ".hidden-dir" not in names
|
||||
|
||||
def test_skips_git_directory(self, manager: StructureManager) -> None:
|
||||
"""Das .git Verzeichnis wird übersprungen."""
|
||||
manager.create_project("dhive", "real-project")
|
||||
git_dir = manager.root_path / "dhive" / ".git"
|
||||
git_dir.mkdir(parents=True)
|
||||
|
||||
projects = manager.list_projects(context="dhive")
|
||||
|
||||
names = [p.name for p in projects]
|
||||
assert "real-project" in names
|
||||
assert ".git" not in names
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 9. resolve_context: Korrekte Kontexterkennung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveContext:
|
||||
"""Testet die Kontextauflösung anhand eines Pfads."""
|
||||
|
||||
@pytest.mark.parametrize("context", ["privat", "dhive", "bahn", "shared"])
|
||||
def test_resolve_each_context(
|
||||
self, manager: StructureManager, context: str
|
||||
) -> None:
|
||||
"""Erkennt den Kontext für Pfade in jedem Kontextordner."""
|
||||
project_path = manager.create_project(context, "test-proj")
|
||||
|
||||
resolved = manager.resolve_context(project_path)
|
||||
|
||||
assert resolved == context
|
||||
|
||||
def test_resolve_nested_path(self, manager: StructureManager) -> None:
|
||||
"""Erkennt den Kontext auch für tiefere Pfade."""
|
||||
project_path = manager.create_project("bahn", "deep-proj")
|
||||
module_path = project_path / "module-a" / "subdir"
|
||||
module_path.mkdir(parents=True)
|
||||
|
||||
resolved = manager.resolve_context(module_path)
|
||||
|
||||
assert resolved == "bahn"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 10. resolve_context: ValueError bei Pfaden außerhalb des Monorepos
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveContextOutside:
|
||||
"""Testet Fehlermeldung bei Pfaden außerhalb des Monorepos."""
|
||||
|
||||
def test_outside_monorepo_raises(self, manager: StructureManager, tmp_path: Path) -> None:
|
||||
"""Pfad außerhalb des Monorepo-Root löst ValueError aus."""
|
||||
outside_path = tmp_path.parent / "some-other-dir"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
manager.resolve_context(outside_path)
|
||||
|
||||
def test_root_path_itself_raises(self, manager: StructureManager) -> None:
|
||||
"""Das Monorepo-Root selbst ist kein Projekt."""
|
||||
with pytest.raises(ValueError):
|
||||
manager.resolve_context(manager.root_path)
|
||||
|
||||
def test_unknown_context_folder_raises(self, manager: StructureManager) -> None:
|
||||
"""Unbekannter Ordnername auf Ebene 1 löst ValueError aus."""
|
||||
unknown = manager.root_path / "unknown-context" / "some-project"
|
||||
unknown.mkdir(parents=True)
|
||||
|
||||
with pytest.raises(ValueError, match="nicht in einem bekannten"):
|
||||
manager.resolve_context(unknown)
|
||||
Reference in New Issue
Block a user