From abf0c8392f130e25332b8c7511869191ef470b0c Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Wed, 26 Jul 2023 14:44:21 -0400 Subject: [PATCH] tests: adds unit tests for tasks and bot Adds unit tests to isolate tasks from AuthoredObject Adds unit tests to test different combinations for staged files Adds unit test to text exception handling for git failures Signed-off-by: Jennifer Power --- tests/trestlebot/tasks/test_assemble_task.py | 29 ++++ .../trestlebot/tasks/test_regenerate_task .py | 29 ++++ tests/trestlebot/test_bot.py | 163 ++++++++++-------- trestlebot/bot.py | 1 - 4 files changed, 149 insertions(+), 73 deletions(-) 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..5f929bff 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_lists_equal(list1: List[str], list2: List[str]) -> bool: + return sorted(list1) == sorted(list2) + + +@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_lists_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