Skip to content

Commit

Permalink
Added unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ag14774 committed Sep 25, 2024
1 parent 6bd68de commit f3f8501
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 12 deletions.
4 changes: 3 additions & 1 deletion poetry_monoranger_plugin/lock_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def execute(self, event: ConsoleCommandEvent):
io.write_line("<info>Running command from monorepo root directory</info>")

monorepo_root = (command.poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
monorepo_root_poetry = Factory().create_poetry(
cwd=monorepo_root, io=io, disable_cache=command.poetry.disable_cache
)

installer = Installer(
io,
Expand Down
2 changes: 1 addition & 1 deletion poetry_monoranger_plugin/monorepo_adder.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def post_execute(self, event: ConsoleTerminateEvent):
return

monorepo_root = (poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io, disable_cache=poetry.disable_cache)

installer = Installer(
io,
Expand Down
23 changes: 14 additions & 9 deletions poetry_monoranger_plugin/path_rewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ def execute(self, event: ConsoleCommandEvent):
main_deps_group.remove_dependency(dependency.name)
main_deps_group.add_dependency(pinned)

def _get_dependency_pyproject(self, poetry: Poetry, dependency: DirectoryDependency) -> PyProjectTOML:
pyproject_file = poetry.pyproject_path.parent / dependency.path / "pyproject.toml"

if not pyproject_file.exists():
raise RuntimeError(f"Could not find pyproject.toml in {dependency.path}")

dep_pyproject: PyProjectTOML = PyProjectTOML(pyproject_file)

if not dep_pyproject.is_poetry_project():
raise RuntimeError(f"Directory {dependency.path} is not a valid poetry project")

return dep_pyproject

def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
"""Pins a directory dependency to a specific version based on the plugin configuration.
Expand All @@ -69,15 +82,7 @@ def _pin_dependency(self, poetry: Poetry, dependency: DirectoryDependency):
RuntimeError: If the pyproject.toml file is not found or is not a valid Poetry project.
ValueError: If the version rewrite rule is invalid.
"""
pyproject_file = poetry.pyproject_path.parent / dependency.path / "pyproject.toml"

if not pyproject_file.exists():
raise RuntimeError(f"Could not find pyproject.toml in {dependency.path}")

dep_pyproject: PyProjectTOML = PyProjectTOML(pyproject_file)

if not dep_pyproject.is_poetry_project():
raise RuntimeError(f"Directory {dependency.path} is not a valid poetry project")
dep_pyproject: PyProjectTOML = self._get_dependency_pyproject(poetry, dependency)

name = cast(str, dep_pyproject.poetry_config["name"])
version = cast(str, dep_pyproject.poetry_config["version"])
Expand Down
2 changes: 1 addition & 1 deletion poetry_monoranger_plugin/venv_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def execute(self, event: ConsoleCommandEvent):
poetry = command.poetry

monorepo_root = (poetry.pyproject_path.parent / self.plugin_conf.monorepo_root).resolve()
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io)
monorepo_root_poetry = Factory().create_poetry(cwd=monorepo_root, io=io, disable_cache=poetry.disable_cache)

io.write_line(f"<info>Using monorepo root venv <fg=green>{monorepo_root.name}</></info>\n")
env_manager = EnvManager(monorepo_root_poetry, io=io)
Expand Down
65 changes: 65 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from pathlib import Path
from typing import Type
from unittest.mock import Mock

import pytest
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import DependencyGroup
from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.poetry import Poetry


@pytest.fixture
def mock_event_gen():
from poetry.console.commands.command import Command

def _factory(command_cls: Type[Command], disable_cache: bool):
from cleo.events.console_command_event import ConsoleCommandEvent

main_grp = DependencyGroup("main")
main_grp.add_dependency(DirectoryDependency("packageB", Path("../packageB"), develop=True))
main_grp.add_dependency(Dependency("numpy", "==1.5.0"))

mock_command = Mock(spec=command_cls)
mock_command.poetry = Mock(spec=Poetry)
mock_command.poetry.pyproject_path = Path("/monorepo_root/packageA/pyproject.toml")
mock_command.poetry.package = Mock()
mock_command.poetry.package.name = "packageA"
mock_command.poetry.package.dependency_group = Mock()
mock_command.poetry.package.dependency_group.return_value = main_grp
mock_command.poetry.locker = Mock()
mock_command.poetry.pool = Mock()
mock_command.poetry.config = Mock()
mock_command.poetry.disable_cache = disable_cache
mock_command.option = Mock(return_value=False)

mock_io = Mock()

mock_event = Mock(spec=ConsoleCommandEvent)
mock_event.command = mock_command
mock_event.io = mock_io

return mock_event

return _factory


@pytest.fixture
def mock_terminate_event_gen(mock_event_gen):
from poetry.console.commands.command import Command

def _factory(command_cls: Type[Command], disable_cache: bool):
from cleo.events.console_terminate_event import ConsoleTerminateEvent

mock_event = mock_event_gen(command_cls, disable_cache)
mock_io = mock_event.io
mock_command = mock_event.command
del mock_event

mock_terminate_event = Mock(spec=ConsoleTerminateEvent)
mock_terminate_event.command = mock_command
mock_terminate_event.io = mock_io

return mock_terminate_event

return _factory
47 changes: 47 additions & 0 deletions tests/test_lock_modifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pathlib import Path
from unittest.mock import Mock, patch

import pytest
from poetry.console.commands.lock import LockCommand
from poetry.installation.installer import Installer
from poetry.poetry import Poetry

from poetry_monoranger_plugin.config import MonorangerConfig
from poetry_monoranger_plugin.lock_modifier import LockModifier


@pytest.mark.parametrize("disable_cache", [True, False])
def test_executes_modifications_for_lock_command(mock_event_gen, disable_cache: bool):
mock_event = mock_event_gen(LockCommand, disable_cache=disable_cache)
mock_command = mock_event.command
config = MonorangerConfig(enabled=True, monorepo_root="../")
lock_modifier = LockModifier(config)

with (
patch("poetry_monoranger_plugin.lock_modifier.Factory.create_poetry", autospec=True) as mock_create_poetry,
patch("poetry_monoranger_plugin.lock_modifier.Installer", autospec=True) as mock_installer_cls,
):
mock_create_poetry.return_value = Mock(spec=Poetry)
mock_installer_cls.return_value = Mock(spec=Installer)

lock_modifier.execute(mock_event)

# A new poetry project object at the monorepo root should be created
mock_create_poetry.assert_called_once()
assert mock_create_poetry.call_args[1]["cwd"] == Path("/monorepo_root").resolve()
assert mock_create_poetry.call_args[1]["io"] == mock_event.io
assert mock_create_poetry.call_args[1]["disable_cache"] == disable_cache

# A new installer should be created with the monorepo root poetry project
mock_installer_cls.assert_called_once()
# Env is remained unchanged as it is the responsibility of venv_modifier.py
assert mock_installer_cls.call_args[0][1] == mock_command.env
assert mock_installer_cls.call_args[0][2] == mock_create_poetry.return_value.package
assert mock_installer_cls.call_args[0][3] == mock_create_poetry.return_value.locker
assert mock_installer_cls.call_args[0][4] == mock_create_poetry.return_value.pool
assert mock_installer_cls.call_args[0][5] == mock_create_poetry.return_value.config
assert mock_installer_cls.call_args[1]["disable_cache"] == mock_create_poetry.return_value.disable_cache

# The new poetry and installer objects should be set on the command
assert mock_command.set_poetry.call_args[0][0] == mock_create_poetry.return_value
assert mock_command.set_installer.call_args[0][0] == mock_installer_cls.return_value
73 changes: 73 additions & 0 deletions tests/test_monorepo_adder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from pathlib import Path
from unittest.mock import Mock, patch

import pytest
from poetry.console.commands.add import AddCommand
from poetry.installation.installer import Installer
from poetry.poetry import Poetry

from poetry_monoranger_plugin.config import MonorangerConfig
from poetry_monoranger_plugin.monorepo_adder import DummyInstaller, MonorepoAdderRemover


@pytest.mark.parametrize("disable_cache", [True, False])
def test_executes_modifications_for_addremove_command(mock_event_gen, disable_cache: bool):
mock_event = mock_event_gen(AddCommand, disable_cache=disable_cache)
mock_command = mock_event.command
config = MonorangerConfig(enabled=True, monorepo_root="../")
adder_remover = MonorepoAdderRemover(config)

with patch("poetry_monoranger_plugin.monorepo_adder.Poetry.__new__", autospec=True) as mock_poetry:
mock_poetry.return_value = Mock(spec=Poetry)

adder_remover.execute(mock_event)

mock_poetry.assert_called_once()
assert mock_command.set_poetry.call_args[0][0] == mock_poetry.return_value
assert isinstance(mock_command.set_installer.call_args[0][0], DummyInstaller)


@pytest.mark.parametrize("disable_cache", [True, False])
def test_executes_modifications_post_addremove_command(mock_terminate_event_gen, disable_cache: bool):
# Here we test the .post_execute command
mock_event = mock_terminate_event_gen(AddCommand, disable_cache=disable_cache)
mock_command = mock_event.command
config = MonorangerConfig(enabled=True, monorepo_root="../")
adder_remover = MonorepoAdderRemover(config)

with (
patch("poetry_monoranger_plugin.monorepo_adder.Factory.create_poetry", autospec=True) as mock_create_poetry,
patch("poetry_monoranger_plugin.monorepo_adder.Installer", autospec=True) as mock_installer_cls,
):
mock_create_poetry.return_value = Mock(spec=Poetry)
mock_installer_cls.return_value = Mock(spec=Installer)

adder_remover.post_execute(mock_event)

# A new poetry project object at the monorepo root should be created
mock_create_poetry.assert_called_once()
assert mock_create_poetry.call_args[1]["cwd"] == Path("/monorepo_root").resolve()
assert mock_create_poetry.call_args[1]["io"] == mock_event.io
assert mock_create_poetry.call_args[1]["disable_cache"] == disable_cache

# A new installer should be created with the monorepo root poetry project
mock_installer_cls.assert_called_once()
# Env is remained unchanged as it is the responsibility of venv_modifier.py
assert mock_installer_cls.call_args[0][1] == mock_command.env
assert mock_installer_cls.call_args[0][2] == mock_create_poetry.return_value.package
assert mock_installer_cls.call_args[0][3] == mock_create_poetry.return_value.locker
assert mock_installer_cls.call_args[0][4] == mock_create_poetry.return_value.pool
assert mock_installer_cls.call_args[0][5] == mock_create_poetry.return_value.config
assert mock_installer_cls.call_args[1]["disable_cache"] == mock_create_poetry.return_value.disable_cache

# Check settings of installer
assert mock_installer_cls.return_value.dry_run.call_args[0][0] == mock_command.option("dry-run")
assert mock_installer_cls.return_value.verbose.call_args[0][0] == mock_event.io.is_verbose()
assert mock_installer_cls.return_value.update.call_args[0][0] is True
assert mock_installer_cls.return_value.execute_operations.call_args[0][0] is not mock_command.option("lock")

# The whitelist should contain the package name
assert mock_installer_cls.return_value.whitelist.call_args[0][0] == [mock_command.poetry.package.name]

# The installer should be run
assert mock_installer_cls.return_value.run.call_count == 1
41 changes: 41 additions & 0 deletions tests/test_path_rewriter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import copy
from unittest.mock import Mock, patch

import pytest
from poetry.console.commands.build import BuildCommand
from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.core.pyproject.toml import PyProjectTOML

from poetry_monoranger_plugin.config import MonorangerConfig
from poetry_monoranger_plugin.path_rewriter import PathRewriter


@pytest.mark.parametrize("disable_cache", [True, False])
def test_executes_path_rewriting_for_build_command(mock_event_gen, disable_cache: bool):
mock_event = mock_event_gen(BuildCommand, disable_cache=disable_cache)
mock_command = mock_event.command
config = MonorangerConfig(enabled=True, monorepo_root="../", version_rewrite_rule="==")
path_rewriter = PathRewriter(config)

original_dependencies = copy.deepcopy(mock_command.poetry.package.dependency_group.return_value.dependencies)

with patch(
"poetry_monoranger_plugin.path_rewriter.PathRewriter._get_dependency_pyproject", autospec=True
) as mock_get_dep:
mock_get_dep.return_value = Mock(spec=PyProjectTOML)
mock_get_dep.return_value.poetry_config = {"version": "0.1.0", "name": "packageB"}

path_rewriter.execute(mock_event)

new_dependencies = mock_command.poetry.package.dependency_group.return_value

assert len(new_dependencies.dependencies) == len(original_dependencies)
# sort the dependencies by name to ensure they are in the same order
original_dependencies = sorted(original_dependencies, key=lambda x: x.name)
new_dependencies = sorted(new_dependencies.dependencies, key=lambda x: x.name)
for i, dep in enumerate(new_dependencies):
assert dep.name == original_dependencies[i].name
if isinstance(original_dependencies[i], DirectoryDependency):
assert dep.pretty_constraint == "0.1.0"
else:
assert dep.pretty_constraint == original_dependencies[i].pretty_constraint
69 changes: 69 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from unittest.mock import Mock, patch

import pytest
from poetry.console.commands.add import AddCommand
from poetry.console.commands.build import BuildCommand
from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand
from poetry.console.commands.install import InstallCommand
from poetry.console.commands.lock import LockCommand
from poetry.console.commands.remove import RemoveCommand
from poetry.console.commands.update import UpdateCommand

from poetry_monoranger_plugin.config import MonorangerConfig
from poetry_monoranger_plugin.plugin import Monoranger


def test_activates_plugin_with_valid_config():
application = Mock()
application.poetry.pyproject.data = {
"tool": {"poetry-monoranger-plugin": {"enabled": True, "monorepo_root": "../"}}
}
application.event_dispatcher = Mock()
plugin = Monoranger()
plugin.activate(application)

assert plugin.plugin_conf.enabled is True
assert plugin.plugin_conf.monorepo_root == "../"
application.event_dispatcher.add_listener.assert_called()


def test_does_not_activate_plugin_with_disabled_config():
application = Mock()
application.poetry.pyproject.data = {}
application.event_dispatcher = Mock()
plugin = Monoranger()
plugin.activate(application)

assert plugin.plugin_conf is None
application.event_dispatcher.add_listener.assert_not_called()


@pytest.mark.parametrize(
"cmd_type",
[
AddCommand,
RemoveCommand,
BuildCommand,
EnvCommand,
LockCommand,
InstallCommand,
UpdateCommand,
],
)
def test_handles_all_command_events(mock_event_gen, cmd_type: type[Command]):
cmd_to_patch: dict[type[Command], str] = {
AddCommand: "poetry_monoranger_plugin.monorepo_adder.MonorepoAdderRemover.execute",
RemoveCommand: "poetry_monoranger_plugin.monorepo_adder.MonorepoAdderRemover.execute",
BuildCommand: "poetry_monoranger_plugin.path_rewriter.PathRewriter.execute",
EnvCommand: "poetry_monoranger_plugin.venv_modifier.VenvModifier.execute",
LockCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
InstallCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
UpdateCommand: "poetry_monoranger_plugin.lock_modifier.LockModifier.execute",
}
event = mock_event_gen(cmd_type, disable_cache=False)
plugin = Monoranger()
plugin.plugin_conf = MonorangerConfig(enabled=True, monorepo_root="../")
with patch(cmd_to_patch[cmd_type]) as mock_execute:
plugin.console_command_event_listener(event, "", Mock())
mock_execute.assert_called_once_with(event)
Loading

0 comments on commit f3f8501

Please sign in to comment.