""" Tests for the documentation sync executor. """ import pytest from pathlib import Path from unittest.mock import AsyncMock, patch, MagicMock from src.executors import doc_sync_executor from src.config import Settings @pytest.mark.executor @pytest.mark.unit class TestDocSyncExecutor: """Tests for doc_sync_executor module.""" @pytest.mark.asyncio async def test_executor_requires_all_config_fields(self, test_settings: Settings): """Test that executor validates required config fields.""" incomplete_config = { "project": "test", # Missing other required fields } with pytest.raises(ValueError, match="Missing required config"): await doc_sync_executor.execute(incomplete_config, test_settings) @pytest.mark.asyncio async def test_executor_validates_project_name(self, test_settings: Settings): """Test that executor requires project name.""" config = { "project": "", "upstream_repo": "https://github.com/test/repo.git", "gitea_repo": "library/test" } with pytest.raises(ValueError): await doc_sync_executor.execute(config, test_settings) @pytest.mark.asyncio async def test_executor_successful_sync_entire_repo( self, test_settings: Settings, sample_doc_sync_config: dict, temp_work_dir: Path ): """Test successful documentation sync of entire repository.""" # Modify config to sync entire repo config = {**sample_doc_sync_config, "docs_paths": []} with patch('src.executors.doc_sync_executor._run_command', new_callable=AsyncMock) as mock_run, \ patch('src.executors.doc_sync_executor._get_git_commit', new_callable=AsyncMock) as mock_get_commit, \ patch('src.executors.doc_sync_executor.shutil') as mock_shutil, \ patch('src.executors.doc_sync_executor.Path') as mock_path: # Mock git commit hash mock_get_commit.return_value = "abc123def456" # Mock path operations mock_work_dir = MagicMock() mock_upstream_dir = MagicMock() mock_gitea_dir = MagicMock() # Setup directory mocking mock_upstream_dir.iterdir.return_value = [ MagicMock(name=".git", is_dir=lambda: True), MagicMock(name="README.md", is_dir=lambda: False), MagicMock(name="docs", is_dir=lambda: True), ] mock_gitea_dir.iterdir.return_value = [ MagicMock(name=".git", is_dir=lambda: True) ] mock_path.return_value = mock_work_dir mock_work_dir.__truediv__.side_effect = [mock_upstream_dir, mock_gitea_dir] # Mock git status to show changes mock_run.side_effect = [ "", # git clone upstream "", # git rev-parse HEAD "", # git clone gitea "", # git add "M README.md\n", # git status --porcelain (has changes) "", # git commit "", # git tag "", # git push branch "", # git push tag ] result = await doc_sync_executor.execute(config, test_settings) assert "Successfully synced" in result assert "test-project" in result.lower() assert "entire repository" in result @pytest.mark.asyncio async def test_executor_sync_specific_paths( self, test_settings: Settings, sample_doc_sync_config: dict ): """Test syncing only specific paths.""" config = {**sample_doc_sync_config, "docs_paths": ["/docs", "/examples"]} with patch('src.executors.doc_sync_executor._run_command', new_callable=AsyncMock) as mock_run, \ patch('src.executors.doc_sync_executor._get_git_commit', new_callable=AsyncMock) as mock_get_commit, \ patch('src.executors.doc_sync_executor.shutil') as mock_shutil, \ patch('src.executors.doc_sync_executor.Path') as mock_path: mock_get_commit.return_value = "abc123" # Mock path operations mock_upstream_dir = MagicMock() mock_gitea_dir = MagicMock() mock_docs = MagicMock(name="docs") mock_docs.exists.return_value = True mock_docs.is_dir.return_value = True mock_docs.name = "docs" mock_examples = MagicMock(name="examples") mock_examples.exists.return_value = True mock_examples.is_dir.return_value = True mock_examples.name = "examples" mock_upstream_dir.__truediv__.side_effect = [mock_docs, mock_examples] mock_gitea_dir.iterdir.return_value = [] # Mock git status to show changes async def run_command_side_effect(cmd, **kwargs): if 'status' in cmd: return "M docs/README.md\n" return "" mock_run.side_effect = run_command_side_effect result = await doc_sync_executor.execute(config, test_settings) # Should mention the specific paths assert "docs" in result.lower() or "Successfully synced" in result @pytest.mark.asyncio async def test_executor_handles_no_changes( self, test_settings: Settings, sample_doc_sync_config: dict ): """Test that executor handles case where there are no changes.""" with patch('src.executors.doc_sync_executor._run_command', new_callable=AsyncMock) as mock_run, \ patch('src.executors.doc_sync_executor._get_git_commit', new_callable=AsyncMock) as mock_get_commit, \ patch('src.executors.doc_sync_executor.shutil'), \ patch('src.executors.doc_sync_executor.Path') as mock_path: mock_get_commit.return_value = "abc123" # Mock empty git status (no changes) async def run_command_side_effect(cmd, cwd=None, capture=False): if 'status' in cmd and '--porcelain' in cmd: return "" # No changes return "" mock_run.side_effect = run_command_side_effect # Setup minimal mocking mock_work_dir = MagicMock() mock_upstream_dir = MagicMock() mock_gitea_dir = MagicMock() mock_gitea_dir.iterdir.return_value = [] mock_upstream_dir.iterdir.return_value = [] result = await doc_sync_executor.execute(sample_doc_sync_config, test_settings) assert "already up to date" in result.lower() or "no changes" in result.lower() @pytest.mark.asyncio async def test_executor_cleanup_on_error( self, test_settings: Settings, sample_doc_sync_config: dict ): """Test that executor cleans up on error.""" with patch('src.executors.doc_sync_executor._run_command', new_callable=AsyncMock) as mock_run, \ patch('src.executors.doc_sync_executor.shutil') as mock_shutil, \ patch('src.executors.doc_sync_executor.Path') as mock_path: # Make git clone fail mock_run.side_effect = Exception("Git clone failed") mock_work_dir = MagicMock() mock_work_dir.exists.return_value = True mock_path.return_value = mock_work_dir with pytest.raises(Exception, match="Git clone failed"): await doc_sync_executor.execute(sample_doc_sync_config, test_settings) # Should have attempted cleanup mock_shutil.rmtree.assert_called() @pytest.mark.executor @pytest.mark.unit class TestDocSyncHelpers: """Tests for doc_sync_executor helper functions.""" @pytest.mark.asyncio async def test_run_command_success(self): """Test that _run_command executes successfully.""" with patch('asyncio.create_subprocess_exec', new_callable=AsyncMock) as mock_exec: mock_proc = AsyncMock() mock_proc.returncode = 0 mock_proc.communicate.return_value = (b"output", b"") mock_exec.return_value = mock_proc result = await doc_sync_executor._run_command( ["echo", "test"], capture=True ) assert result == "output" @pytest.mark.asyncio async def test_run_command_failure(self): """Test that _run_command raises on failure.""" with patch('asyncio.create_subprocess_exec', new_callable=AsyncMock) as mock_exec: mock_proc = AsyncMock() mock_proc.returncode = 1 mock_proc.communicate.return_value = (b"", b"error message") mock_exec.return_value = mock_proc with pytest.raises(Exception, match="Command failed"): await doc_sync_executor._run_command(["false"]) @pytest.mark.asyncio async def test_get_git_commit(self, temp_work_dir: Path): """Test getting git commit hash.""" with patch('src.executors.doc_sync_executor._run_command', new_callable=AsyncMock) as mock_run: mock_run.return_value = "abc123def456\n" commit = await doc_sync_executor._get_git_commit(temp_work_dir) assert commit == "abc123def456" mock_run.assert_called_once()