Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions documentdb_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

import pytest
from documentdb_tests.framework import fixtures
from documentdb_tests.framework.test_structure_validator import (
validate_test_file_location,
validate_python_files_in_tests
)
from pathlib import Path


def pytest_addoption(parser):
Expand Down Expand Up @@ -131,3 +136,40 @@ def collection(database_client, request, worker_id):

# Cleanup: drop collection
fixtures.cleanup_collection(database_client, collection_name)


def pytest_collection_modifyitems(session, config, items):
"""
Combined pytest hook to validate test structure.
"""
errors = []
seen_files = set()

# Validate structure for collected test files
for item in items:
file_path = str(item.fspath)

if file_path in seen_files:
continue
seen_files.add(file_path)

is_valid, error_msg = validate_test_file_location(file_path)
if not is_valid:
errors.append(f"\n {file_path}\n → {error_msg}")

# Validate all Python files in tests directory
if items:
first_item_path = Path(items[0].fspath)
if "tests" in first_item_path.parts:
tests_idx = first_item_path.parts.index("tests")
tests_dir = Path(*first_item_path.parts[:tests_idx + 1])
errors.extend(validate_python_files_in_tests(tests_dir))

if errors:
import sys

print("\n\n❌ Folder Structure Violations:", file=sys.stderr)
print("".join(errors), file=sys.stderr)
print("\nSee docs/testing/FOLDER_STRUCTURE.md for rules.\n", file=sys.stderr)

pytest.exit("Test validation failed", returncode=1)
79 changes: 79 additions & 0 deletions documentdb_tests/framework/test_structure_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Test structure validator to enforce folder organization rules.
"""
from pathlib import Path


def validate_test_file_location(file_path: str) -> tuple[bool, str]:
"""
Validate that a test file follows naming conventions.

Returns:
(is_valid, error_message) - error_message is empty if valid
"""
path = Path(file_path)

# Skip if not in tests directory
if "tests" not in path.parts:
return True, ""

# Get path relative to tests directory
try:
tests_idx = path.parts.index("tests")
rel_parts = path.parts[tests_idx + 1:]
except (ValueError, IndexError):
return True, ""

if not rel_parts or len(rel_parts) < 2:
return True, ""

# Extract test file name and parent folder
test_file = path.stem # filename without .py
parent_folder = rel_parts[-2]

# Skip validation for certain folders
skip_folders = {"operators", "tests"}
if parent_folder in skip_folders:
return True, ""

# Rule: Test files in feature subfolders should include feature name in filename
# Pattern: test_{feature}_*.py or test_pipeline_*.py (for integration tests)
if not test_file.startswith("test_pipeline") and parent_folder not in test_file:
return False, (
f"Test file in /{parent_folder}/ should include feature name in filename. "
f"Expected pattern: test_{parent_folder}_*.py, got: {path.name}"
)

return True, ""


def validate_python_files_in_tests(tests_dir: Path) -> list[str]:
"""
Find Python files in tests directory that don't follow test_*.py pattern.

Returns:
List of error messages for invalid files
"""
errors = []

# Folders where non-test Python files are allowed
allowed_folders = {"utils", "fixtures", "__pycache__"}

for py_file in tests_dir.rglob("*.py"):
# Skip __init__.py files
if py_file.name == "__init__.py":
continue

# Check if file is in an allowed folder
if any(folder in py_file.parts for folder in allowed_folders):
continue

# Check if file follows test_*.py pattern
if not py_file.stem.startswith("test_"):
rel_path = py_file.relative_to(tests_dir.parent)
errors.append(
f"\n {rel_path}\n → Python file in tests directory must follow test_*.py pattern. "
f"Got: {py_file.name}. If this is a utility file, move it to a utils/ or fixtures/ folder."
)

return errors
Loading