Skip to content

Commit

Permalink
test: Add unit tests
Browse files Browse the repository at this point in the history
Unit tests with coverage for current code base were added. Along with
that, an option to run unit tests via `tox` was added to
pyproject.tom.

`poetry run tox -e unit`

Signed-off-by: Martin Kalcok <martin.kalcok@gmail.com>
  • Loading branch information
mkalcok committed Dec 25, 2024
1 parent 8217ad8 commit ed89805
Show file tree
Hide file tree
Showing 11 changed files with 546 additions and 60 deletions.
4 changes: 2 additions & 2 deletions microovn_rebuilder/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def watch(
break


def parse_args() -> argparse.Namespace:
def parse_args() -> argparse.Namespace: # pragma: no cover
parser = argparse.ArgumentParser(description="Monitor file changes.")
parser.add_argument("-c", "--config", required=True, help="Path to config file")
parser.add_argument(
Expand Down Expand Up @@ -115,5 +115,5 @@ def main() -> None:
watch(targets, connector, args.ovn_src, args.jobs)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
4 changes: 2 additions & 2 deletions microovn_rebuilder/remote/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def __init__(self, remotes: List[str]) -> None:

@abstractmethod
def check_remote(self, remote_dst: str) -> None:
pass
pass # pragma: no cover

@abstractmethod
def update(self, target: Target) -> None:
pass
pass # pragma: no cover
2 changes: 1 addition & 1 deletion microovn_rebuilder/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def parse_config(
)
except KeyError as exc:
raise ConfigException(
f"One of the 'targets' in config file '{cfg_path}' is missing key: {exc.args[1]}"
f"One of the 'targets' in config file '{cfg_path}' is missing key: {exc.args[0]}"
) from exc

if not targets:
Expand Down
170 changes: 131 additions & 39 deletions poetry.lock

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ isort = "^5.13.2"
mypy = "^1.13.0"
pytest = "^8.3.4"
pytest-mock = "^3.14.0"
pytest-cov = "^6.0.0"
coverage = "^7.6.9"

[tool.black]
line-length = 89
Expand All @@ -46,17 +48,23 @@ commands_pre = [["poetry", "install"]]
[tool.tox.env.lint]
description = "Run static code check and lints on the code"
commands = [
["poetry", "run", "black", "--check", "microovn_rebuilder"],
["poetry", "run", "isort", "--check", "microovn_rebuilder"],
["poetry", "run", "mypy", "--install-types", "--non-interactive"],
["poetry", "run", "mypy", "microovn_rebuilder"],
["poetry", "run", "black", "--check", "microovn_rebuilder/", "tests/unit/"],
["poetry", "run", "isort", "--check", "microovn_rebuilder/", "tests/unit/"],
["poetry", "run", "mypy", "--install-types", "--non-interactive", "microovn_rebuilder/"],
]

[tool.tox.env.format]
description = "Run code formatters"
commands = [
["poetry", "run", "black", "microovn_rebuilder"],
["poetry", "run", "isort", "microovn_rebuilder"],
["poetry", "run", "black", "microovn_rebuilder/", "tests/unit/"],
["poetry", "run", "isort", "microovn_rebuilder/", "tests/unit/"],
]

[tool.tox.env.unit]
description = "Run unit tests (including required coverage)"
commands = [
["poetry", "run", "pytest", "--cov=microovn_rebuilder/", "--cov-report=json", "tests/unit/"],
["poetry", "run", "coverage", "report", "--fail-under=100"],
]

[build-system]
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,34 @@
from typing import Set

import pytest

import microovn_rebuilder
from microovn_rebuilder.remote import lxd
from microovn_rebuilder.target import Target, parse_config


@pytest.fixture(scope="session")
def config_file() -> str:
return str(Path(microovn_rebuilder.__file__).parent.parent / "default_config.yaml")


@pytest.fixture(scope="session")
def local_ovn_path() -> str:
return "/tmp/foo/ovn"


@pytest.fixture(scope="session")
def remote_deployment_path() -> str:
return "/tmp/foo/squashfs-root"


@pytest.fixture(scope="session")
def default_targets(config_file: str, local_ovn_path: str, remote_deployment_path: str) -> Set[Target]:
def default_targets(
config_file: str, local_ovn_path: str, remote_deployment_path: str
) -> Set[Target]:
return parse_config(config_file, local_ovn_path, remote_deployment_path)


@pytest.fixture(scope="session")
def lxd_connector() -> lxd.LXDConnector:
return lxd.LXDConnector(["vm1", "vm2"])
20 changes: 20 additions & 0 deletions tests/unit/remote/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest

from microovn_rebuilder.remote import _CONNECTORS, ConnectorException, create_connector


@pytest.mark.parametrize(
"bad_spec", ["foo", "foo,bar", "lxd:vm1,ssh:vm2", "foo:vm1,foo:vm2"]
)
def test_create_connector_invalid_spec(bad_spec):
with pytest.raises(ConnectorException):
create_connector(bad_spec)


def test_create_connector():
connector = create_connector("lxd:vm1,lxd:vm2")
expected_remotes = ["vm1", "vm2"]
expected_type = _CONNECTORS["lxd"]

assert isinstance(connector, expected_type)
assert connector.remotes == expected_remotes
99 changes: 99 additions & 0 deletions tests/unit/remote/test_lxd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import subprocess
from unittest import mock
from unittest.mock import MagicMock, call

import pytest

from microovn_rebuilder.remote import ConnectorException, lxd


def test_update(mocker, lxd_connector, default_targets):
target = list(default_targets)[0]
mock_run_result = MagicMock(spec=subprocess.CompletedProcess)
mock_run_cmd = mocker.patch.object(
lxd_connector, "_run_command", return_value=mock_run_result
)
mock_check_cmd = mocker.patch.object(lxd_connector, "_check_cmd_result")

expected_run_calls = []
expected_check_calls = []
for remote in lxd_connector.remotes:
expected_run_calls.append(
call("lxc", "file", "delete", f"{remote}{target.remote_path}")
)
expected_check_calls.append(
call(mock_run_result, f"[{remote}] Failed to remove remote file")
)

expected_run_calls.append(
call(
"lxc", "file", "push", target.local_path, f"{remote}{target.remote_path}"
)
)
expected_check_calls.append(
call(mock_run_result, f"[{remote}] Failed to upload file")
)

expected_run_calls.append(
call("lxc", "exec", remote, "snap", "restart", target.service)
)
expected_check_calls.append(
call(mock_run_result, f"[{remote}] Failed to restart service")
)

lxd_connector.update(target)

mock_run_cmd.assert_has_calls(expected_run_calls)
mock_check_cmd.assert_has_calls(expected_check_calls)


def test_check_remote(mocker, lxd_connector, remote_deployment_path):
mock_run_result = MagicMock(spec=subprocess.CompletedProcess)
mock_run_command = mocker.patch.object(
lxd_connector, "_run_command", return_value=mock_run_result
)
mock_check_cmd_result = mocker.patch.object(lxd_connector, "_check_cmd_result")

expected_run_calls = []
expected_check_calls = []
for remote in lxd_connector.remotes:
expected_run_calls.append(
call("lxc", "exec", remote, "--", "test", "-d", remote_deployment_path)
)
expected_check_calls.append(
call(
mock_run_result,
f"[{remote}] Remote directory '{remote_deployment_path}' does not exist on LXC instance {remote}",
)
)

lxd_connector.check_remote(remote_deployment_path)

mock_run_command.assert_has_calls(expected_run_calls)
mock_check_cmd_result.assert_has_calls(expected_check_calls)


def test_run_command(mocker, lxd_connector):
mock_run = mocker.patch.object(lxd.subprocess, "run")
cmd = ["/bin/foo", "bar"]
lxd_connector._run_command(*cmd)

mock_run.assert_called_once_with(tuple(cmd), capture_output=True)


def test_check_cmd_result_no_error(lxd_connector):
result = MagicMock(autospec=subprocess.CompletedProcess)
result.returncode = 0

assert lxd_connector._check_cmd_result(result, "extra message") is None


def test_check_cmd_result_error(lxd_connector):
result = MagicMock(autospec=subprocess.CompletedProcess)
result.returncode = 1

with pytest.raises(ConnectorException) as exc:
extra_msg = "error details foo"
lxd_connector._check_cmd_result(result, extra_msg)

assert extra_msg in str(exc.value)
Loading

0 comments on commit ed89805

Please sign in to comment.