diff --git a/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections.py b/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections.py rename to documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py b/documentdb_tests/compatibility/tests/core/operator/stages/group/test_aggregate_group_stage.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py rename to documentdb_tests/compatibility/tests/core/operator/stages/group/test_aggregate_group_stage.py diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py b/documentdb_tests/compatibility/tests/core/operator/stages/match/test_aggregate_match_stage.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py rename to documentdb_tests/compatibility/tests/core/operator/stages/match/test_aggregate_match_stage.py diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_basic_queries.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_basic_queries.py rename to documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_projections.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_projections.py rename to documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_query_operators.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_query_operators.py rename to documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index 131b582..e53f405 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -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): @@ -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) diff --git a/documentdb_tests/framework/test_structure_validator.py b/documentdb_tests/framework/test_structure_validator.py new file mode 100644 index 0000000..7821e36 --- /dev/null +++ b/documentdb_tests/framework/test_structure_validator.py @@ -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