""" Global test configuration and fixtures for MarkiTect project. Provides shared fixtures, utilities, and configuration for all test types. """ import pytest import tempfile import shutil import asyncio from pathlib import Path from unittest.mock import Mock, AsyncMock from typing import Generator, Dict, Any import sqlite3 import os # Import async test utilities from tests.utils.assertions import cleanup_async_mocks, create_async_mock_that_returns # Note: event_loop fixture is now handled by pytest-asyncio with asyncio_mode=auto # This replaces the manual event loop management for better async test support @pytest.fixture(scope="session") def test_workspace() -> Generator[Path, None, None]: """Create isolated test workspace for file operations.""" temp_dir = tempfile.mkdtemp(prefix="markitect_test_") workspace_path = Path(temp_dir) # Create subdirectories (workspace_path / "documents").mkdir() (workspace_path / "cache").mkdir() (workspace_path / "workspaces").mkdir() yield workspace_path # Cleanup shutil.rmtree(temp_dir, ignore_errors=True) @pytest.fixture def test_database_path(test_workspace) -> Path: """Provide path for test database.""" return test_workspace / "test.db" @pytest.fixture def mock_database(): """Provide mocked database for testing.""" mock_db = Mock() mock_cursor = Mock() mock_db.cursor.return_value = mock_cursor mock_db.execute.return_value = mock_cursor mock_cursor.fetchone.return_value = None mock_cursor.fetchall.return_value = [] mock_cursor.lastrowid = 1 return mock_db @pytest.fixture def mock_http_client(): """Provide mocked HTTP client for API tests.""" mock_client = Mock() mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "success"} mock_response.text = '{"status": "success"}' mock_client.get.return_value = mock_response mock_client.post.return_value = mock_response mock_client.put.return_value = mock_response mock_client.delete.return_value = mock_response return mock_client @pytest.fixture def mock_async_http_client(): """Provide mocked async HTTP client for API tests.""" mock_client = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value={"status": "success"}) mock_response.text = AsyncMock(return_value='{"status": "success"}') mock_client.get.return_value = mock_response mock_client.post.return_value = mock_response mock_client.put.return_value = mock_response mock_client.delete.return_value = mock_response return mock_client @pytest.fixture def test_config(test_workspace) -> Dict[str, Any]: """Provide test configuration dictionary.""" return { "workspace_dir": str(test_workspace / "workspaces"), "database_path": str(test_workspace / "test.db"), "cache_dir": str(test_workspace / "cache"), "gitea_url": "http://test-gitea.com", "gitea_token": "test-token", "repo_owner": "test", "repo_name": "repo", "log_level": "DEBUG" } @pytest.fixture def clean_environment(): """Provide clean environment variables for testing.""" original_env = dict(os.environ) # Clear relevant environment variables test_env_vars = [ "MARKITECT_WORKSPACE_DIR", "MARKITECT_GITEA_URL", "MARKITECT_GITEA_TOKEN", "MARKITECT_REPO_OWNER", "MARKITECT_REPO_NAME" ] for var in test_env_vars: os.environ.pop(var, None) yield # Restore original environment os.environ.clear() os.environ.update(original_env) @pytest.fixture def isolated_environment(test_workspace, clean_environment): """Set up isolated environment for CLI testing.""" env = { "MARKITECT_WORKSPACE_DIR": str(test_workspace / "workspaces"), "MARKITECT_GITEA_URL": "http://test-gitea.com", "MARKITECT_GITEA_TOKEN": "test-token", "MARKITECT_REPO_OWNER": "test", "MARKITECT_REPO_NAME": "repo", "PYTHONPATH": "." } # Update current process environment for key, value in env.items(): os.environ[key] = value yield env @pytest.fixture def sample_markdown_content(): """Provide sample markdown content for testing.""" return """--- title: Test Document author: Test Author tags: [test, sample] --- # Test Document This is a test document with **bold** and *italic* text. ## Section 1 - Item 1 - Item 2 - Item 3 ## Section 2 Here's a code block: ```python def hello_world(): print("Hello, World!") ``` And a link: [Test Link](https://example.com) """ @pytest.fixture def sample_issue_data(): """Provide sample issue data for testing.""" return { "number": 123, "title": "Test Issue", "body": "This is a test issue description", "state": "open", "labels": [ {"name": "bug"}, {"name": "priority:high"}, {"name": "status:in-progress"} ], "milestone": { "id": 1, "title": "Version 1.0", "description": "First release" }, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T12:00:00Z" } @pytest.fixture def sample_project_data(): """Provide sample project data for testing.""" return { "name": "Test Project", "description": "A test project for testing", "state": "active", "milestones": [ { "id": 1, "title": "Version 1.0", "description": "First release", "due_date": "2025-12-31T23:59:59Z", "state": "open", "open_issues": 5, "closed_issues": 3 } ], "kanban_columns": ["Todo", "In Progress", "Review", "Done"], "created_at": "2024-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } # Performance testing fixtures @pytest.fixture def performance_timer(): """Timer fixture for performance testing.""" import time class Timer: def __init__(self): self.start_time = None self.end_time = None def start(self): self.start_time = time.time() def stop(self): self.end_time = time.time() @property def elapsed(self) -> float: if self.start_time is None: raise ValueError("Timer not started") if self.end_time is None: return time.time() - self.start_time return self.end_time - self.start_time return Timer() # Async test helpers @pytest.fixture def async_test_timeout(): """Default timeout for async tests.""" return 30.0 # 30 seconds @pytest.fixture def async_cleanup(): """Fixture to help with async test cleanup and prevent coroutine warnings.""" mocks_to_cleanup = [] def register_mock(mock): """Register a mock for cleanup.""" mocks_to_cleanup.append(mock) return mock yield register_mock # Cleanup all registered mocks cleanup_async_mocks(*mocks_to_cleanup) @pytest.fixture def async_mock_client(async_cleanup): """Provide a properly configured async HTTP client mock.""" mock_client = AsyncMock() mock_response = AsyncMock() mock_response.status = 200 mock_response.json = create_async_mock_that_returns({"status": "success"}) mock_response.text = create_async_mock_that_returns('{"status": "success"}') # Configure the mock client methods mock_client.get = create_async_mock_that_returns(mock_response) mock_client.post = create_async_mock_that_returns(mock_response) mock_client.put = create_async_mock_that_returns(mock_response) mock_client.delete = create_async_mock_that_returns(mock_response) # Register for cleanup async_cleanup(mock_client) async_cleanup(mock_response) return mock_client # Test markers configuration def pytest_configure(config): """Configure pytest markers.""" config.addinivalue_line( "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" ) config.addinivalue_line( "markers", "integration: marks tests as integration tests" ) config.addinivalue_line( "markers", "e2e: marks tests as end-to-end tests" ) config.addinivalue_line( "markers", "performance: marks tests as performance tests" ) config.addinivalue_line( "markers", "unit: marks tests as unit tests" ) # Collection hooks def pytest_collection_modifyitems(config, items): """Modify test collection to add markers based on test location.""" for item in items: # Add markers based on test file location if "unit" in str(item.fspath): item.add_marker(pytest.mark.unit) elif "integration" in str(item.fspath): item.add_marker(pytest.mark.integration) elif "e2e" in str(item.fspath): item.add_marker(pytest.mark.e2e) elif "performance" in str(item.fspath): item.add_marker(pytest.mark.performance)