diff --git a/tests/trestlebot/tasks/test_assemble_task.py b/tests/trestlebot/tasks/test_assemble_task.py index ce314851..717c8719 100644 --- a/tests/trestlebot/tasks/test_assemble_task.py +++ b/tests/trestlebot/tasks/test_assemble_task.py @@ -18,6 +18,7 @@ import os import pathlib +from unittest.mock import Mock, patch import pytest from trestle.core.commands.author.catalog import CatalogGenerate @@ -27,6 +28,7 @@ from tests import testutils from trestlebot.tasks.assemble_task import AssembleTask +from trestlebot.tasks.authored.base_authored import AuthorObjectBase from trestlebot.tasks.authored.types import AuthoredType @@ -41,6 +43,33 @@ ssp_md_dir = "md_ssp" +def test_assemble_task_isolated(tmp_trestle_dir: str) -> None: + """Test the assemble task isolated from AuthoredObject implementation""" + trestle_root = pathlib.Path(tmp_trestle_dir) + md_path = os.path.join(cat_md_dir, test_cat) + args = testutils.setup_for_catalog(trestle_root, test_cat, md_path) + cat_generate = CatalogGenerate() + assert cat_generate._run(args) == 0 + + mock = Mock(spec=AuthorObjectBase) + + assemble_task = AssembleTask( + tmp_trestle_dir, + AuthoredType.CATALOG.value, + cat_md_dir, + "", + ) + + with patch( + "trestlebot.tasks.authored.types.get_authored_object" + ) as mock_get_authored_object: + mock_get_authored_object.return_value = mock + + assert assemble_task.execute() == 0 + + mock.assemble.assert_called_once_with(markdown_path=md_path) + + def test_catalog_assemble_task(tmp_trestle_dir: str) -> None: """Test catalog assemble at the task level""" trestle_root = pathlib.Path(tmp_trestle_dir) diff --git a/tests/trestlebot/tasks/test_regenerate_task .py b/tests/trestlebot/tasks/test_regenerate_task .py index b6983df7..db0bfd0e 100644 --- a/tests/trestlebot/tasks/test_regenerate_task .py +++ b/tests/trestlebot/tasks/test_regenerate_task .py @@ -19,11 +19,13 @@ import argparse import os import pathlib +from unittest.mock import Mock, patch import pytest from trestle.core.commands.author.ssp import SSPAssemble, SSPGenerate from tests import testutils +from trestlebot.tasks.authored.base_authored import AuthorObjectBase from trestlebot.tasks.authored.types import AuthoredType from trestlebot.tasks.regenerate_task import RegenerateTask @@ -39,6 +41,33 @@ ssp_md_dir = "md_ssp" +def test_regenerate_task_isolated(tmp_trestle_dir: str) -> None: + """Test the regenerate task isolated from AuthoredObject implementation""" + trestle_root = pathlib.Path(tmp_trestle_dir) + md_path = os.path.join(cat_md_dir, test_cat) + _ = testutils.setup_for_catalog(trestle_root, test_cat, md_path) + + mock = Mock(spec=AuthorObjectBase) + + regenerate_task = RegenerateTask( + tmp_trestle_dir, + AuthoredType.CATALOG.value, + cat_md_dir, + "", + ) + + with patch( + "trestlebot.tasks.authored.types.get_authored_object" + ) as mock_get_authored_object: + mock_get_authored_object.return_value = mock + + assert regenerate_task.execute() == 0 + + mock.regenerate.assert_called_once_with( + model_path=f"catalogs/{test_cat}", markdown_path=cat_md_dir + ) + + def test_catalog_regenerate_task(tmp_trestle_dir: str) -> None: """Test catalog regenerate at the task level""" trestle_root = pathlib.Path(tmp_trestle_dir) diff --git a/tests/trestlebot/test_bot.py b/tests/trestlebot/test_bot.py index abf90f3c..99868eab 100644 --- a/tests/trestlebot/test_bot.py +++ b/tests/trestlebot/test_bot.py @@ -16,20 +16,38 @@ """Test for top-level Trestle Bot logic.""" -import json import os -from typing import Tuple +from typing import Callable, List, Tuple from unittest.mock import Mock, patch import pytest +from git import GitCommandError from git.repo import Repo import trestlebot.bot as bot from tests.testutils import clean -from trestlebot.provider import GitProvider - - -def test_stage_files(tmp_repo: Tuple[str, Repo]) -> None: +from trestlebot.provider import GitProvider, GitProviderException + + +def check_arrays_equal(arr1: List[str], arr2: List[str]) -> bool: + return sorted(arr1) == sorted(arr2) + + +@pytest.mark.parametrize( + "file_patterns, expected_files", + [ + (["*.txt"], ["file1.txt", "file2.txt"]), + (["file*.txt"], ["file1.txt", "file2.txt"]), + (["*.csv"], ["file3.csv"]), + (["file*.csv"], ["file3.csv"]), + (["*.txt", "*.csv"], ["file1.txt", "file2.txt", "file3.csv"]), + (["."], ["file1.txt", "file2.txt", "file3.csv"]), + ([], []), + ], +) +def test_stage_files( + tmp_repo: Tuple[str, Repo], file_patterns: List[str], expected_files: List[str] +) -> None: """Test staging files by patterns""" repo_path, repo = tmp_repo @@ -38,16 +56,16 @@ def test_stage_files(tmp_repo: Tuple[str, Repo]) -> None: f.write("Test file 1 content") with open(os.path.join(repo_path, "file2.txt"), "w") as f: f.write("Test file 2 content") + with open(os.path.join(repo_path, "file3.csv"), "w") as f: + f.write("test,") # Stage the files - bot._stage_files(repo, ["*.txt"]) + bot._stage_files(repo, file_patterns) # Verify that files are staged staged_files = [item.a_path for item in repo.index.diff(repo.head.commit)] - assert len(staged_files) == 2 - assert "file1.txt" in staged_files - assert "file2.txt" in staged_files + assert check_arrays_equal(staged_files, expected_files) is True clean(repo_path, repo) @@ -201,28 +219,24 @@ def test_run_dry_run(tmp_repo: Tuple[str, Repo]) -> None: with open(test_file_path, "w") as f: f.write("Test content") - # Test running the bot - commit_sha = bot.run( - working_dir=repo_path, - branch="main", - commit_name="Test User", - commit_email="test@example.com", - commit_message="Test commit message", - author_name="The Author", - author_email="author@test.com", - patterns=["*.txt"], - dry_run=True, - ) - assert commit_sha != "" + with patch("git.remote.Remote.push") as mock_push: + mock_push.return_value = "Mocked result" - # Verify that the commit is made - commit = next(repo.iter_commits()) - assert commit.message.strip() == "Test commit message" - assert commit.author.name == "The Author" - assert commit.author.email == "author@test.com" + # Test running the bot + commit_sha = bot.run( + working_dir=repo_path, + branch="main", + commit_name="Test User", + commit_email="test@example.com", + commit_message="Test commit message", + author_name="The Author", + author_email="author@test.com", + patterns=["*.txt"], + dry_run=True, + ) + assert commit_sha != "" - # Verify that the file is tracked by the commit - assert os.path.basename(test_file_path) in commit.stats.files + mock_push.assert_not_called() clean(repo_path, repo) @@ -248,48 +262,6 @@ def test_empty_commit(tmp_repo: Tuple[str, Repo]) -> None: clean(repo_path, repo) -def test_non_matching_files(tmp_repo: Tuple[str, Repo]) -> None: - """Test that non-matching files are ignored""" - repo_path, repo = tmp_repo - - # Create a test file - test_file_path = os.path.join(repo_path, "test.txt") - with open(test_file_path, "w") as f: - f.write("Test content") - - # Create a test file - data = {"test": "file"} - test_json_path = os.path.join(repo_path, "test.json") - with open(test_json_path, "w") as f: - json.dump(data, f, indent=4) - - # Test running the bot - commit_sha = bot.run( - working_dir=repo_path, - branch="main", - commit_name="Test User", - commit_email="test@example.com", - commit_message="Test commit message", - author_name="The Author", - author_email="author@test.com", - patterns=["*.json"], - dry_run=True, - ) - assert commit_sha != "" - - # Verify that the commit is made - commit = next(repo.iter_commits()) - assert commit.message.strip() == "Test commit message" - assert commit.author.name == "The Author" - assert commit.author.email == "author@test.com" - - # Verify that only the JSON file is tracked in the commits - assert os.path.basename(test_file_path) not in commit.stats.files - assert os.path.basename(test_json_path) in commit.stats.files - - clean(repo_path, repo) - - def test_run_check_only(tmp_repo: Tuple[str, Repo]) -> None: """Test bot run with check_only""" repo_path, repo = tmp_repo @@ -319,6 +291,53 @@ def test_run_check_only(tmp_repo: Tuple[str, Repo]) -> None: clean(repo_path, repo) +def push_side_effect(refspec: str) -> None: + raise GitCommandError("example") + + +def pull_side_effect(refspec: str) -> None: + raise GitProviderException("example") + + +@pytest.mark.parametrize( + "side_effect, msg", + [ + (push_side_effect, "Git push to .* failed: .*"), + (pull_side_effect, "Git pull request to .* failed: example"), + ], +) +def test_run_with_exception( + tmp_repo: Tuple[str, Repo], side_effect: Callable[[str], None], msg: str +) -> None: + """Test bot run with mocked push with side effects that throw exceptions""" + repo_path, repo = tmp_repo + + # Create a test file + test_file_path = os.path.join(repo_path, "test.txt") + with open(test_file_path, "w") as f: + f.write("Test content") + + repo.create_remote("origin", url="git.test.com/test/repo.git") + + with patch("git.remote.Remote.push") as mock_push: + mock_push.side_effect = side_effect + + with pytest.raises(bot.RepoException, match=msg): + _ = bot.run( + working_dir=repo_path, + branch="main", + commit_name="Test User", + commit_email="test@example.com", + commit_message="Test commit message", + author_name="The Author", + author_email="author@test.com", + patterns=["*.txt"], + dry_run=False, + ) + + clean(repo_path, repo) + + def test_run_with_provider(tmp_repo: Tuple[str, Repo]) -> None: """Test bot run with mock git provider""" repo_path, repo = tmp_repo diff --git a/trestlebot/bot.py b/trestlebot/bot.py index b13f63fa..2522bc94 100644 --- a/trestlebot/bot.py +++ b/trestlebot/bot.py @@ -39,7 +39,6 @@ class RepoException(Exception): def _stage_files(gitwd: Repo, patterns: List[str]) -> None: """Stages files in git based on file patterns""" for pattern in patterns: - gitwd.index.add(pattern) if pattern == ".": logger.info("Staging all repository changes") # Using check to avoid adding git directory