1
0
Fork 0

Compare commits

...

2 Commits

Author SHA1 Message Date
Maurizio Porrato 6c109d1875 More tests 2023-08-17 18:57:49 +01:00
Maurizio Porrato a3bc96efa5 Fix pyproject.toml 2023-08-17 14:46:51 +01:00
12 changed files with 371 additions and 21 deletions

View File

@ -1,5 +1,5 @@
[project]
name = "operator_repo"
name = "operator-repo"
version = "0.1.0"
description = "Library and utilities to handle repositories of kubernetes operators"
authors = [

View File

@ -176,10 +176,7 @@ class Bundle:
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
raise TypeError(
f"== not supported between instances of '{self.__class__.__name__}'"
f" and '{other.__class__.__name__}"
)
return False
if self.csv_operator_name != other.csv_operator_name:
return False
try:
@ -204,7 +201,6 @@ class Bundle:
f" and '{other.__class__.__name__}"
)
if self.csv_operator_name != other.csv_operator_name:
# raise ValueError("Can't compare bundles from different operators")
return self.csv_operator_name < other.csv_operator_name
try:
return Version.parse(self.csv_operator_version.lstrip("v")) < Version.parse(
@ -434,21 +430,15 @@ class Operator:
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
raise NotImplementedError(
f"Can't compare {self.__class__.__name__} to {other.__class__.__name__}"
)
return False
return self.operator_name == other.operator_name
def __ne__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
raise NotImplementedError(
f"Can't compare {self.__class__.__name__} to {other.__class__.__name__}"
)
return self.operator_name != other.operator_name
return not self == other
def __lt__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
raise NotImplementedError(
raise TypeError(
f"Can't compare {self.__class__.__name__} to {other.__class__.__name__}"
)
return self.operator_name < other.operator_name
@ -551,9 +541,7 @@ class Repo:
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
raise NotImplementedError(
f"Can't compare {self.__class__.__name__} to {other.__class__.__name__}"
)
return False
return self._repo_path == other._repo_path
def __repr__(self) -> str:

View File

@ -9,7 +9,7 @@ import sys
from collections.abc import Iterator
from itertools import chain
from pathlib import Path
from typing import Union, Optional
from typing import Optional, Union
from .checks import get_checks, run_suite
from .classes import Bundle, Operator, Repo

View File

@ -1,5 +1,5 @@
from pathlib import Path
from typing import Optional, Union
from typing import Any, Optional, Union
import yaml
@ -153,3 +153,19 @@ def bundle_files(
},
other_files or {},
)
def make_nested_dict(items: dict[str, Any]) -> dict:
"""
_make_nested_dict({"foo.bar": "baz"}) -> {"foo": {"bar": "baz"}}
"""
result = {}
for path, value in items.items():
current = result
keys = path.split(".")
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
current[keys[-1]] = value
return result

0
tests/checks/__init__.py Normal file
View File

183
tests/checks/test_bundle.py Normal file
View File

@ -0,0 +1,183 @@
import re
from pathlib import Path
from typing import Union
import pytest
from operator_repo import Repo
from operator_repo.checks.bundle import check_image, check_operator_name, check_semver
from tests import bundle_files, create_files, make_nested_dict
@pytest.mark.parametrize(
"bundle, extra_files, expected_results",
[
(
bundle_files("hello", "0.0.1"),
{},
set(),
),
(
bundle_files(
"hello",
"0.0.1",
annotations={"operators.operatorframework.io.bundle.package.v1": "foo"},
),
{},
{
"Operator name from annotations.yaml (foo) does not match the operator's directory name (hello)",
"Operator name from annotations.yaml (foo) does not match the name defined in the CSV (hello)",
},
),
(
bundle_files("hello", "0.0.1"),
{"operators/hello/0.0.1/metadata/annotations.yaml": {"annotations": {}}},
{
"Bundle does not define the operator name in annotations.yaml",
},
),
],
False,
["Names ok", "Wrong annotations.yaml", "Empty annotations.yaml"],
)
def test_operator_name(
tmp_path: Path, bundle: dict, extra_files: dict, expected_results: set[str]
) -> None:
create_files(tmp_path, bundle, extra_files)
repo = Repo(tmp_path)
operator = next(repo.all_operators())
bundle = next(operator.all_bundles())
assert {x.reason for x in check_operator_name(bundle)} == expected_results
@pytest.mark.parametrize(
"bundle, extra_files, expected_results",
[
(
bundle_files("hello", "0.0.1"),
{},
{"CSV doesn't define .metadata.annotations.containerImage"},
),
(
bundle_files(
"hello",
"0.0.1",
csv=make_nested_dict(
{
"metadata.annotations.containerImage": "example.com/namespace/image:tag",
}
),
),
{},
{"CSV doesn't define .spec.install.spec.deployments"},
),
(
bundle_files(
"hello",
"0.0.1",
csv=make_nested_dict(
{
"metadata.annotations.containerImage": "example.com/namespace/image:tag",
"spec.install.spec.deployments": [
make_nested_dict(
{
"spec.template.spec.containers": [
{"image": "example.com/namespace/image:tag"}
]
}
),
],
}
),
),
{},
set(),
),
(
bundle_files(
"hello",
"0.0.1",
csv=make_nested_dict(
{
"metadata.annotations.containerImage": "example.com/namespace/image:tag",
"spec.install.spec.deployments": [
make_nested_dict(
{
"spec.template.spec.containers": [
{
"image": "example.com/namespace/image:othertag"
}
]
}
),
],
}
),
),
{},
{
"container image example.com/namespace/image:tag not used by any deployment"
},
),
(
bundle_files("hello", "0.0.1"),
{"operators/hello/0.0.1/manifests/hello.clusterserviceversion.yaml": ""},
re.compile("Invalid CSV contents "),
),
],
False,
[
"Missing containerImage",
"Missing deployments",
"Matching images",
"Mismatched images",
"Empty CSV",
],
)
def test_image(
tmp_path: Path,
bundle: dict,
extra_files: dict,
expected_results: Union[set[str], re.Pattern],
) -> None:
create_files(tmp_path, bundle, extra_files)
repo = Repo(tmp_path)
operator = next(repo.all_operators())
bundle = next(operator.all_bundles())
reasons = {x.reason for x in check_image(bundle)}
if isinstance(expected_results, re.Pattern):
assert len(reasons) == 1
reason = reasons.pop()
assert expected_results.match(reason)
else:
assert reasons == expected_results
@pytest.mark.parametrize(
"bundle, extra_files, expected_results",
[
(
bundle_files("hello", "0.0.1"),
{},
set(),
),
(
bundle_files("hello", "latest"),
{},
{
"Version from CSV (latest) is not valid semver",
"Version from filesystem (latest) is not valid semver",
},
),
],
False,
["All versions ok", "Both versions invalid"],
)
def test_semver(
tmp_path: Path, bundle: dict, extra_files: dict, expected_results: set[str]
) -> None:
create_files(tmp_path, bundle, extra_files)
repo = Repo(tmp_path)
operator = next(repo.all_operators())
bundle = next(operator.all_bundles())
assert {x.reason for x in check_semver(bundle)} == expected_results

View File

@ -0,0 +1,75 @@
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
from operator_repo import Bundle, Repo
from operator_repo.checks import Fail, Warn, get_checks, run_check, run_suite
from tests import bundle_files, create_files
def test_check_result() -> None:
result1 = Warn("foo")
result2 = Fail("bar")
result3 = Warn("foo")
assert result1 != result2
assert result1 < result2
assert result1 == result3
assert "foo" in str(result1)
assert "bar" in str(result2)
assert "foo" in repr(result1)
assert "bar" in repr(result2)
assert "warning" in str(result1)
assert "failure" in str(result2)
assert {result1, result2, result3} == {result1, result2}
@patch("importlib.import_module")
def test_get_checks(mock_import_module: MagicMock) -> None:
fake_module = MagicMock()
def check_fake(x):
pass
fake_module.check_fake = check_fake
fake_module.non_check_bar = lambda x: None
mock_import_module.return_value = fake_module
assert get_checks("suite.name") == {
"operator": [check_fake],
"bundle": [check_fake],
}
mock_import_module.assert_has_calls(
[call("suite.name.operator"), call("suite.name.bundle")], any_order=True
)
@patch("importlib.import_module")
def test_get_checks_missing_modules(mock_import_module: MagicMock) -> None:
mock_import_module.side_effect = ModuleNotFoundError()
assert get_checks("suite.name") == {
"operator": [],
"bundle": [],
}
mock_import_module.assert_has_calls(
[call("suite.name.operator"), call("suite.name.bundle")], any_order=True
)
def test_run_check(mock_bundle: Bundle) -> None:
def check_fake(x):
yield Warn("foo")
assert list(run_check(check_fake, mock_bundle)) == [
Warn("foo", "check_fake", mock_bundle)
]
@patch("operator_repo.checks.get_checks")
def test_run_suite(mock_get_checks: MagicMock, mock_bundle: Bundle) -> None:
def check_fake(x):
yield Warn("foo")
mock_get_checks.return_value = {"bundle": [check_fake], "operator": []}
assert list(run_suite([mock_bundle], "fake.suite")) == [
Warn("foo", "check_fake", mock_bundle)
]

View File

@ -0,0 +1,59 @@
from pathlib import Path
import pytest
from operator_repo import Repo
from operator_repo.checks.operator import check_upgrade
from tests import bundle_files, create_files
@pytest.mark.parametrize(
"bundles, operator_name, expected_results",
[
(
[bundle_files("hello", "0.0.1")],
"hello",
set(),
),
(
[
bundle_files("hello", "0.0.1"),
bundle_files("hello", "0.0.2"),
],
"hello",
{"Channel beta has dangling bundles: {Bundle(hello/0.0.1)}"},
),
(
[
bundle_files("hello", "0.0.1"),
bundle_files(
"hello", "0.0.2", csv={"spec": {"replaces": "hello.v0.0.1"}}
),
],
"hello",
set(),
),
(
[
bundle_files("hello", "0.0.1"),
bundle_files("hello", "0.0.2", csv={"spec": {"replaces": "rubbish"}}),
],
"hello",
{"Bundle(hello/0.0.2) has invalid 'replaces' field: 'rubbish'"},
),
],
False,
[
"Single bundle",
"Two bundles, no replaces",
"Two bundles",
"Two bundles, invalid replaces",
],
)
def test_upgrade(
tmp_path: Path, bundles: list[dict], operator_name: str, expected_results: set[str]
) -> None:
create_files(tmp_path, *bundles)
repo = Repo(tmp_path)
operator = repo.operator(operator_name)
assert {x.reason for x in check_upgrade(operator)} == expected_results

13
tests/conftest.py Normal file
View File

@ -0,0 +1,13 @@
from pathlib import Path
import pytest
from operator_repo import Bundle, Repo
from tests import bundle_files, create_files
@pytest.fixture
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")

View File

@ -2,7 +2,7 @@ from pathlib import Path
import pytest
from operator_repo import Bundle, Repo
from operator_repo import Bundle, Operator, Repo
from operator_repo.exceptions import InvalidBundleException, InvalidOperatorException
from tests import bundle_files, create_files
@ -23,6 +23,9 @@ def test_bundle(tmp_path: Path) -> None:
== "hello"
)
assert bundle.dependencies == []
assert bundle != "foo"
with pytest.raises(TypeError):
_ = bundle < "foo"
def test_bundle_compare(tmp_path: Path) -> None:
@ -118,3 +121,8 @@ def test_bundle_invalid(tmp_path: Path) -> None:
assert list(repo.operator("one_empty_bundle")) == [
repo.operator("one_empty_bundle").bundle("0.0.1")
]
def test_bundle_caching(mock_bundle: Bundle) -> None:
assert Bundle(mock_bundle.root).operator == mock_bundle.operator
assert Bundle(mock_bundle.root).operator is not mock_bundle.operator

View File

@ -1,5 +1,7 @@
from pathlib import Path
import pytest
from operator_repo import Operator, Repo
from tests import bundle_files, create_files
@ -25,6 +27,9 @@ def test_operator_one_bundle(tmp_path: Path) -> None:
assert bundle.dependencies == []
assert operator.root == repo.root / "operators" / "hello"
assert "hello" in repr(operator)
assert operator != "foo"
with pytest.raises(TypeError):
_ = operator < "foo"
def test_channels(tmp_path: Path) -> None:

View File

@ -39,3 +39,6 @@ def test_repo_one_bundle(tmp_path: Path) -> None:
assert len(list(repo)) == 1
assert set(repo) == {repo.operator("hello")}
assert repo.has("hello")
assert repo != "foo"
with pytest.raises(TypeError):
_ = repo < "foo"