Skip to content

Commit

Permalink
tests: adds unit tests for tasks and bot
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
jpower432 committed Jul 26, 2023
1 parent ba88344 commit 7a03278
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 73 deletions.
29 changes: 29 additions & 0 deletions tests/trestlebot/tasks/test_assemble_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import os
import pathlib
from unittest.mock import Mock, patch

import pytest
from trestle.core.commands.author.catalog import CatalogGenerate
Expand All @@ -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


Expand All @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions tests/trestlebot/tasks/test_regenerate_task .py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
163 changes: 91 additions & 72 deletions tests/trestlebot/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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="[email protected]",
commit_message="Test commit message",
author_name="The Author",
author_email="[email protected]",
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 == "[email protected]"
# Test running the bot
commit_sha = bot.run(
working_dir=repo_path,
branch="main",
commit_name="Test User",
commit_email="[email protected]",
commit_message="Test commit message",
author_name="The Author",
author_email="[email protected]",
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)

Expand All @@ -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="[email protected]",
commit_message="Test commit message",
author_name="The Author",
author_email="[email protected]",
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 == "[email protected]"

# 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
Expand Down Expand Up @@ -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="[email protected]",
commit_message="Test commit message",
author_name="The Author",
author_email="[email protected]",
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
Expand Down
1 change: 0 additions & 1 deletion trestlebot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7a03278

Please sign in to comment.