From f46348d7a03747c50945661733505435363f31e9 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Fri, 18 Aug 2023 16:45:28 +0100 Subject: [PATCH] Complete test coverage --- src/operator_repo/checks/__init__.py | 2 + src/operator_repo/classes.py | 7 +- src/operator_repo/cli.py | 17 ++-- tests/conftest.py | 17 ++++ tests/test_cli.py | 122 +++++++++++++++++++++++++++ tests/test_operator.py | 62 ++++++++++++++ 6 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 tests/test_cli.py diff --git a/src/operator_repo/checks/__init__.py b/src/operator_repo/checks/__init__.py index c8c6253..ff6571a 100644 --- a/src/operator_repo/checks/__init__.py +++ b/src/operator_repo/checks/__init__.py @@ -40,6 +40,8 @@ class CheckResult: return self.severity def __eq__(self, other: Any) -> bool: + if not isinstance(other, self.__class__): + return False return (self.kind, self.reason, self.check, self.origin) == ( other.kind, other.reason, diff --git a/src/operator_repo/classes.py b/src/operator_repo/classes.py index 0a85556..3082568 100644 --- a/src/operator_repo/classes.py +++ b/src/operator_repo/classes.py @@ -442,12 +442,11 @@ class Operator: ) if update_strategy == "semver-mode": return {x: {y} for x, y in zip(all_bundles, all_bundles[1:])} - if update_strategy == "semver-skippatch": - # TODO: implement semver-skippatch - raise NotImplementedError("%s: semver-skippatch is not implemented yet") if update_strategy == "replaces-mode": return self._replaces_graph(channel, all_bundles) - raise ValueError(f"{self}: unknown updateGraph value: {update_strategy}") + raise NotImplementedError( + f"{self}: unsupported updateGraph value: {update_strategy}" + ) def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): diff --git a/src/operator_repo/cli.py b/src/operator_repo/cli.py index 9a1de44..c544259 100644 --- a/src/operator_repo/cli.py +++ b/src/operator_repo/cli.py @@ -83,26 +83,21 @@ def action_list(repo: Repo, *what: str, recursive: bool = False) -> None: show(target, recursive) -def _walk( - target: Union[Repo, Operator, Bundle] -) -> Iterator[Union[Repo, Operator, Bundle]]: +def _walk(target: Union[Operator, Bundle]) -> Iterator[Union[Operator, Bundle]]: yield target - if isinstance(target, Repo): - for operator in target: - yield from _walk(operator) - elif isinstance(target, Operator): + if isinstance(target, Operator): yield from target.all_bundles() def action_check(repo: Repo, suite: str, *what: str, recursive: bool = False) -> None: if what: - targets: Iterator[Union[Repo, Operator, Bundle]] = ( + targets: Iterator[Union[Operator, Bundle]] = ( parse_target(repo, x) for x in what ) else: targets = repo.all_operators() if recursive: - all_targets: Iterator[Union[Repo, Operator, Bundle]] = chain.from_iterable( + all_targets: Iterator[Union[Operator, Bundle]] = chain.from_iterable( _walk(x) for x in targets ) else: @@ -119,7 +114,7 @@ def action_check_list(suite: str) -> None: print(f" - {display_name}: {check.__doc__}") -def _get_repo(path: Optional[Path]) -> Repo: +def _get_repo(path: Optional[Path] = None) -> Repo: if not path: path = Path.cwd() try: @@ -201,5 +196,5 @@ def main() -> None: main_parser.print_help() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/tests/conftest.py b/tests/conftest.py index f4cb763..72efebe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,3 +15,20 @@ def mock_bundle(tmp_path: Path) -> Bundle: create_files(tmp_path, bundle_files("hello", "0.0.1")) repo = Repo(tmp_path) return repo.operator("hello").bundle("0.0.1") + + +@pytest.fixture +def mock_repo(tmp_path: Path) -> Repo: + """ + Create a dummy file structure for an operator repo with two operators + and a total of four bundles and return the corresponding Repo object + """ + create_files( + tmp_path, + bundle_files("hello", "0.0.1"), + bundle_files("hello", "0.0.2"), + bundle_files("world", "0.0.1"), + bundle_files("world", "0.0.2"), + ) + repo = Repo(tmp_path) + return repo diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..a34c07e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,122 @@ +from pathlib import Path +from typing import Iterator +from unittest.mock import MagicMock, patch + +import pytest +from _pytest.capture import CaptureFixture + +from operator_repo import Bundle, Operator, Repo +from operator_repo.checks import CheckResult +from operator_repo.cli import _get_repo, main, parse_target + + +def test_cli_parse_target(mock_repo: Repo) -> None: + assert parse_target(mock_repo, "hello") == mock_repo.operator("hello") + assert parse_target(mock_repo, "world/0.0.1") == mock_repo.operator("world").bundle( + "0.0.1" + ) + + +def test_cli_list(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool", "-r", str(mock_repo.root), "list", "hello"]): + main() + captured = capsys.readouterr() + assert "hello/0.0.1" in captured.out + assert "hello/0.0.2" in captured.out + + +def test_cli_list_repo(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool", "-r", str(mock_repo.root), "list"]): + main() + captured = capsys.readouterr() + assert "hello" in captured.out + assert "world" in captured.out + + +def test_cli_list_bundle(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch( + "sys.argv", ["optool", "-r", str(mock_repo.root), "list", "hello/0.0.1"] + ): + main() + captured = capsys.readouterr() + assert "hello/0.0.1" in captured.out + assert "hello/0.0.2" not in captured.out + + +def test_cli_list_recursive(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool", "-r", str(mock_repo.root), "list", "-R"]): + main() + captured = capsys.readouterr() + assert "hello/0.0.1" in captured.out + assert "hello/0.0.2" in captured.out + assert "world/0.0.1" in captured.out + assert "world/0.0.2" in captured.out + + +def test_cli_check(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool", "-r", str(mock_repo.root), "check", "hello"]): + main() + captured = capsys.readouterr() + assert "Channel beta has dangling bundles" in captured.out + + +def test_cli_check_recursive(mock_repo: Repo, capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool", "-r", str(mock_repo.root), "check", "-R"]): + main() + captured = capsys.readouterr() + assert "Channel beta has dangling bundles" in captured.out + assert "CSV doesn't define .metadata.annotations.containerImage" in captured.out + + +def test_cli_check_operator_recursive( + mock_repo: Repo, capsys: CaptureFixture[str] +) -> None: + with patch( + "sys.argv", ["optool", "-r", str(mock_repo.root), "check", "-R", "hello"] + ): + main() + captured = capsys.readouterr() + assert "Channel beta has dangling bundles" in captured.out + assert "CSV doesn't define .metadata.annotations.containerImage" in captured.out + + +@patch("operator_repo.cli.get_checks") +def test_cli_check_list(mock_checks: MagicMock, capsys: CaptureFixture[str]) -> None: + def check_foo(_: Operator) -> Iterator[CheckResult]: + """This is the check_foo description""" + raise StopIteration + + def check_bar(_: Bundle) -> Iterator[CheckResult]: + """This is the check_bar description""" + raise StopIteration + + mock_checks.return_value = {"operator": [check_foo], "bundle": [check_bar]} + with patch("sys.argv", ["optool", "check", "--list"]): + main() + captured = capsys.readouterr() + assert "This is the check_foo description" in captured.out + assert "This is the check_bar description" in captured.out + + +@patch("operator_repo.cli.Path.cwd") +def test_cli_get_repo(mock_cwd: MagicMock, mock_repo: Repo) -> None: + mock_cwd.return_value = mock_repo.root + assert _get_repo() == mock_repo + + +@patch("operator_repo.cli.Path.cwd") +def test_cli_get_repo_invalid( + mock_cwd: MagicMock, tmp_path: Path, capsys: CaptureFixture[str] +) -> None: + mock_cwd.return_value = tmp_path + with pytest.raises(SystemExit): + _ = _get_repo() + captured = capsys.readouterr() + assert "is not a valid operator repository" in captured.out + + +def test_cli_help(capsys: CaptureFixture[str]) -> None: + with patch("sys.argv", ["optool"]): + main() + captured = capsys.readouterr() + assert "usage: optool" in captured.out diff --git a/tests/test_operator.py b/tests/test_operator.py index 2b492d8..1ce2081 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -103,3 +103,65 @@ def test_update_graph(tmp_path: Path) -> None: assert update[bundle1] == {bundle2} assert update[bundle2] == {bundle3, bundle4} assert update[bundle3] == {bundle4} + + +def test_update_graph_invalid_replaces(tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files("hello", "0.0.1"), + bundle_files("hello", "0.0.2", csv={"spec": {"replaces": "other.v0.0.1"}}), + ) + repo = Repo(tmp_path) + operator = repo.operator("hello") + with pytest.raises(ValueError, match="replaces a bundle from a different operator"): + _ = operator.update_graph("beta") + + +def test_update_graph_replaces_missing_bundle(tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files("hello", "0.0.2", csv={"spec": {"replaces": "hello.v0.0.1"}}), + ) + repo = Repo(tmp_path) + operator = repo.operator("hello") + _ = operator.update_graph("beta") + + +def test_update_graph_invalid_operator_name(tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files("hello", "0.0.1", csv={"metadata": {"name": "other.v0.0.1"}}), + bundle_files("hello", "0.0.2", csv={"spec": {"replaces": "hello.v0.0.1"}}), + ) + repo = Repo(tmp_path) + operator = repo.operator("hello") + with pytest.raises(ValueError, match="has bundles with different operator names"): + _ = operator.update_graph("beta") + + +def test_update_graph_semver(tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files("hello", "0.0.1"), + bundle_files("hello", "0.0.2"), + {"operators/hello/ci.yaml": {"updateGraph": "semver-mode"}}, + ) + repo = Repo(tmp_path) + operator = repo.operator("hello") + bundle1 = operator.bundle("0.0.1") + bundle2 = operator.bundle("0.0.2") + update = operator.update_graph("beta") + assert update[bundle1] == {bundle2} + + +def test_update_graph_unsupported(tmp_path: Path) -> None: + create_files( + tmp_path, + bundle_files("hello", "0.0.1"), + bundle_files("hello", "0.0.2"), + {"operators/hello/ci.yaml": {"updateGraph": "semver-skippatch"}}, + ) + repo = Repo(tmp_path) + operator = repo.operator("hello") + with pytest.raises(NotImplementedError, match="unsupported updateGraph value"): + _ = operator.update_graph("beta")