From 6552f3a4f0def28baa178908f0634c9478878005 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Tue, 2 May 2023 10:28:18 -0400 Subject: [PATCH] Add tests for `Tool` base class implementation --- src/briefcase/commands/upgrade.py | 21 +- src/briefcase/integrations/android_sdk.py | 4 +- src/briefcase/integrations/docker.py | 4 +- src/briefcase/platforms/windows/__init__.py | 4 +- .../platforms/windows/visualstudio.py | 2 +- tests/commands/conftest.py | 11 + tests/commands/upgrade/conftest.py | 182 ++++--- tests/commands/upgrade/test_call.py | 468 ++++++++++-------- tests/conftest.py | 21 +- .../android_sdk/AndroidSDK/test_upgrade.py | 5 + tests/integrations/base/conftest.py | 9 + tests/integrations/base/test_Tool.py | 209 +++++--- tests/integrations/base/test_ToolCache.py | 61 +-- tests/integrations/base/test_tool_registry.py | 60 +++ tests/platforms/android/gradle/test_run.py | 19 +- tests/platforms/iOS/xcode/test_mixin.py | 41 ++ tests/platforms/linux/appimage/test_mixin.py | 74 ++- tests/platforms/linux/flatpak/test_mixin.py | 13 +- .../linux/system/test_mixin__verify.py | 69 ++- .../test_mixin__verify_system_python.py | 2 +- tests/platforms/macOS/app/test_package.py | 27 +- tests/platforms/macOS/xcode/test_package.py | 59 ++- tests/platforms/windows/app/test_build.py | 31 +- tests/platforms/windows/app/test_package.py | 34 +- .../windows/visualstudio/test_build.py | 16 +- 25 files changed, 976 insertions(+), 470 deletions(-) create mode 100644 tests/commands/conftest.py create mode 100644 tests/integrations/base/conftest.py create mode 100644 tests/integrations/base/test_tool_registry.py diff --git a/src/briefcase/commands/upgrade.py b/src/briefcase/commands/upgrade.py index d9199e077..c130d3847 100644 --- a/src/briefcase/commands/upgrade.py +++ b/src/briefcase/commands/upgrade.py @@ -1,3 +1,4 @@ +import sys from operator import attrgetter from typing import List, Set, Type @@ -21,10 +22,10 @@ class UpgradeCommand(BaseCommand): def platform(self): """The upgrade command always reports as the local platform.""" return { - "Darwin": "macOS", - "Linux": "linux", - "Windows": "windows", - }[self.tools.host_os] + "darwin": "macOS", + "linux": "linux", + "win32": "windows", + }[sys.platform] def bundle_path(self, app): """A placeholder; Upgrade command doesn't have a bundle path.""" @@ -61,7 +62,7 @@ def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[ManagedTool]: if tool_list: if invalid_tools := tool_list - set(tool_registry): raise UpgradeToolError( - f"Briefcase does not know how to manage {', '.join(invalid_tools)}." + f"Briefcase does not know how to manage {', '.join(sorted(invalid_tools))}." ) upgrade_list = { tool for name, tool in tool_registry.items() if name in tool_list @@ -73,7 +74,7 @@ def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[ManagedTool]: for tool_klass in upgrade_list: if issubclass(tool_klass, ManagedTool): try: - tool = tool_klass.verify(self.tools, install=False) + tool = tool_klass.verify(tools=self.tools, install=False) except (BriefcaseCommandError, UnsupportedHostError): pass else: @@ -83,7 +84,9 @@ def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[ManagedTool]: # Let the user know if any requested tools are not being managed if tool_list: if unmanaged_tools := tool_list - {tool.name for tool in tools_to_upgrade}: - error_msg = f"Briefcase is not managing {', '.join(unmanaged_tools)}." + error_msg = ( + f"Briefcase is not managing {', '.join(sorted(unmanaged_tools))}." + ) if not tools_to_upgrade: raise UpgradeToolError(error_msg) else: @@ -98,9 +101,9 @@ def __call__(self, tool_list: List[str], list_tools: bool = False, **options): :param list_tools: Boolean to only list upgradeable tools (default False). """ if tools_to_upgrade := self.get_tools_to_upgrade(set(tool_list)): - action = "is managing" if list_tools else "will upgrade" self.logger.info( - f"Briefcase {action} the following tools:", prefix=self.command + f"Briefcase {'is managing' if list_tools else 'will upgrade'} the following tools:", + prefix=self.command, ) for tool in tools_to_upgrade: self.logger.info(f" - {tool.full_name} ({tool.name})") diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index c25e49a51..fc9d9e6b3 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -265,8 +265,8 @@ def managed_install(self) -> bool: return True def uninstall(self): - """Uninstall the Android SDK.""" - # TODO:PR: implement this + """The Android SDK is upgraded in-place instead of being reinstalled.""" + pass def install(self): """Download and install the Android SDK.""" diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py index 17d6d9631..2f68f9097 100644 --- a/src/briefcase/integrations/docker.py +++ b/src/briefcase/integrations/docker.py @@ -189,7 +189,7 @@ def _buildx_installed(cls, tools: ToolCache): except subprocess.CalledProcessError: raise BriefcaseCommandError(cls.BUILDX_PLUGIN_MISSING) - def check_output(self, args: list[SubprocessArgT], image_tag: str): + def check_output(self, args: list[SubprocessArgT], image_tag: str) -> str: """Run a process inside a Docker container, capturing output. This is a bare Docker invocation; it's really only useful for running @@ -470,7 +470,7 @@ def check_output( cwd: Path = None, mounts: dict[str | Path, str | Path] = None, **kwargs, - ): + ) -> str: """Run a process inside a Docker container, capturing output.""" # Any exceptions from running the process are *not* caught. # This ensures that "docker.check_output()" behaves as closely to diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index da5bc8ccc..466eb20c2 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -128,9 +128,9 @@ def default_packaging_format(self): def verify_tools(self): super().verify_tools() - WiX.verify(self.tools) + WiX.verify(tools=self.tools) if self._windows_sdk_needed: - WindowsSDK.verify(self.tools) + WindowsSDK.verify(tools=self.tools) def add_options(self, parser): super().add_options(parser) diff --git a/src/briefcase/platforms/windows/visualstudio.py b/src/briefcase/platforms/windows/visualstudio.py index 638fc85fd..757fb58d9 100644 --- a/src/briefcase/platforms/windows/visualstudio.py +++ b/src/briefcase/platforms/windows/visualstudio.py @@ -38,7 +38,7 @@ class WindowsVisualStudioBuildCommand(WindowsVisualStudioMixin, BuildCommand): def verify_tools(self): super().verify_tools() - VisualStudio.verify(self.tools) + VisualStudio.verify(tools=self.tools) def build_app(self, app: BaseConfig, **kwargs): """Build the Visual Studio project. diff --git a/tests/commands/conftest.py b/tests/commands/conftest.py new file mode 100644 index 000000000..9a7dd26f4 --- /dev/null +++ b/tests/commands/conftest.py @@ -0,0 +1,11 @@ +from unittest.mock import MagicMock + +import git as git_ +import pytest + + +@pytest.fixture +def mock_git(): + git = MagicMock(spec_set=git_) + git.exc = git_.exc + return git diff --git a/tests/commands/upgrade/conftest.py b/tests/commands/upgrade/conftest.py index 74fc9ba35..d9ead2a54 100644 --- a/tests/commands/upgrade/conftest.py +++ b/tests/commands/upgrade/conftest.py @@ -2,9 +2,18 @@ import pytest +import briefcase.commands.upgrade from briefcase.commands import UpgradeCommand from briefcase.console import Console, Log from briefcase.exceptions import MissingToolError +from briefcase.integrations.base import ManagedTool, Tool + + +@pytest.fixture +def upgrade_command(tmp_path): + command = DummyUpgradeCommand(base_path=tmp_path) + command.tools.host_os = "wonky" + return command class DummyUpgradeCommand(UpgradeCommand): @@ -31,88 +40,121 @@ def binary_path(self, app): @pytest.fixture -def ManagedSDK1(): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "managed-1" - sdk.full_name = "Managed 1" - sdk.exists.return_value = True - sdk.managed_install = True - # No plugins defined on SDK1 - sdk.plugins.values.side_effect = AttributeError - return sdk +def mock_tool_registry(monkeypatch): + """Tool registry with all dummy tools.""" + tool_list = [ + DummyTool, + DummyManagedTool1, + DummyManagedTool2, + DummyManagedTool3, + DummyUnManagedManagedTool, + DummyNotInstalledManagedTool, + ] + tool_registry = dict() + for tool in tool_list: + monkeypatch.setattr(tool, "verify", MagicMock(wraps=tool.verify)) + tool_registry[tool.name] = tool -@pytest.fixture -def ManagedSDK2Plugin1(): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "managed-2-plugin-1" - sdk.full_name = "Managed 2 plugin 1" - sdk.exists.return_value = True - sdk.managed_install = True - return sdk + monkeypatch.setattr(briefcase.commands.upgrade, "tool_registry", tool_registry) @pytest.fixture -def ManagedSDK2Plugin2(): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "managed-2-plugin-2" - sdk.full_name = "Managed 2 plugin 2" - sdk.exists.return_value = True - sdk.managed_install = True - return sdk +def mock_no_managed_tool_registry(monkeypatch): + """Tool registry without any installed managed tools.""" + tool_list = [ + DummyTool, + DummyUnManagedManagedTool, + DummyNotInstalledManagedTool, + ] + tool_registry = dict() + for tool in tool_list: + monkeypatch.setattr(tool, "verify", MagicMock(wraps=tool.verify)) + tool_registry[tool.name] = tool -@pytest.fixture -def ManagedSDK2Plugin3(): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "managed-2-plugin-3" - sdk.full_name = "Managed 2 plugin 3" - sdk.exists.return_value = False - sdk.verify.side_effect = MissingToolError("managed-2-plugin-3") - sdk.managed_install = True - return sdk + monkeypatch.setattr(briefcase.commands.upgrade, "tool_registry", tool_registry) -@pytest.fixture -def ManagedSDK2(ManagedSDK2Plugin1, ManagedSDK2Plugin2, ManagedSDK2Plugin3): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "managed-2" - sdk.full_name = "Managed 2" - sdk.exists.return_value = True - sdk.managed_install = True - sdk.plugins = { - "managed2-plugin1": ManagedSDK2Plugin1, - "managed2-plugin2": ManagedSDK2Plugin2, - "managed2-plugin3": ManagedSDK2Plugin3, - } - return sdk +class DummyToolBase(Tool): + name = "dummy_tool_base" + supported_host_os = {"wonky"} + @classmethod + def verify_install(cls, tools, **kwargs): + return cls(tools=tools) -@pytest.fixture -def NonManagedSDK(): - sdk = MagicMock() - sdk.verify.return_value = sdk - sdk.name = "non-managed" - sdk.full_name = "Non Managed" - sdk.exists.return_value = True - sdk.managed_install = False - return sdk +class DummyManagedToolBase(ManagedTool): + name = "dummy_managed_tool_base" + supported_host_os = {"wonky"} -@pytest.fixture -def NonInstalledSDK(): - sdk = MagicMock() - sdk.name = "non-installed" - sdk.full_name = "Non Installed" - sdk.verify.side_effect = MissingToolError("non-installed") - return sdk + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.actions = [] + @classmethod + def verify_install(cls, tools, **kwargs): + # add to ToolCache so accessible after upgrade + setattr(tools, cls.name, cls(tools=tools)) + return getattr(tools, cls.name) -@pytest.fixture -def upgrade_command(tmp_path, ManagedSDK1, ManagedSDK2, NonManagedSDK, NonInstalledSDK): - return DummyUpgradeCommand(base_path=tmp_path) + def exists(self) -> bool: + self.actions.append("exists") + return True + + def install(self, *args, **kwargs): + self.actions.append("install") + + def uninstall(self, *args, **kwargs): + self.actions.append("uninstall") + + +class DummyTool(DummyToolBase): + """Unmanaged Tool testing class.""" + + name = "unmanaged" + full_name = "Unmanaged Dummy Tool" + + +class DummyUnManagedManagedTool(DummyManagedToolBase): + """Managed Tool without a managed install testing class.""" + + name = "unmanaged_managed" + full_name = "Unmanaged Managed Dummy Tool" + + @property + def managed_install(self) -> bool: + return False + + +class DummyNotInstalledManagedTool(DummyManagedToolBase): + """Managed Tool without a managed install testing class.""" + + name = "not_installed" + full_name = "Not Installed Managed Dummy Tool" + + @classmethod + def verify_install(cls, tools, **kwargs): + raise MissingToolError(cls.full_name) + + +class DummyManagedTool1(DummyManagedToolBase): + """Managed Tool testing class.""" + + name = "managed_1" + full_name = "Managed Dummy Tool 1" + + +class DummyManagedTool2(DummyManagedToolBase): + """Managed Tool testing class.""" + + name = "managed_2" + full_name = "Managed Dummy Tool 2" + + +class DummyManagedTool3(DummyManagedToolBase): + """Managed Tool testing class.""" + + name = "managed_3" + full_name = "Managed Dummy Tool 3" diff --git a/tests/commands/upgrade/test_call.py b/tests/commands/upgrade/test_call.py index 4207f8bec..af7884bf5 100644 --- a/tests/commands/upgrade/test_call.py +++ b/tests/commands/upgrade/test_call.py @@ -1,200 +1,268 @@ -# import pytest -# -# from briefcase.exceptions import BriefcaseCommandError -# -# -# def test_list_tools( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# ManagedSDK2Plugin1, -# ManagedSDK2Plugin2, -# ManagedSDK2Plugin3, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """The tools for upgrade can be listed.""" -# -# upgrade_command(tool_list=[], list_tools=True) -# -# # The tools are all verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2Plugin1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2Plugin2.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2Plugin3.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# -# # The console contains the lines we expect, but not the non-managed and -# # non-installed tools. -# out = capsys.readouterr().out -# assert " - managed-1" in out -# assert " - managed-2" in out -# assert " - managed-2-plugin-1" in out -# assert " - managed-2-plugin-2" in out -# assert " - managed-2-plugin-3" not in out -# assert " - non-managed" not in out -# assert " - non-installed" not in out -# -# -# def test_list_specific_tools( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """If a list of tools is provided, only those are listed.""" -# -# upgrade_command( -# tool_list=["managed-1", "non-managed", "non-installed"], list_tools=True -# ) -# -# # All tools are verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# -# # The console contains the lines we expect, but not the non-requested, -# # non-managed, and non-installed tools. -# out = capsys.readouterr().out -# assert " - managed-1" in out -# assert " - managed-2" not in out -# assert " - non-managed" not in out -# assert " - non-installed" not in out -# -# -# def test_upgrade_tools( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """All managed tools can be upgraded.""" -# upgrade_command(tool_list=[]) -# -# # All tools are verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# -# # The console contains the lines we expect, but not the non-managed and -# # non-installed tools. -# out = capsys.readouterr().out -# assert " - managed-1" in out -# assert " - managed-2" in out -# assert " - non-managed" not in out -# assert " - non-installed" not in out -# -# # There is also an upgrade message for each tool -# assert "[managed-1] Upgrading Managed 1..." in out -# assert "[managed-2] Upgrading Managed 2..." in out -# -# # The managed tools are upgraded -# ManagedSDK1.upgrade.assert_called_with() -# ManagedSDK2.upgrade.assert_called_with() -# assert NonManagedSDK.upgrade.call_count == 0 -# assert NonInstalledSDK.upgrade.call_count == 0 -# -# -# def test_upgrade_specific_tools( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """If a list of tools is provided, only those are listed.""" -# -# upgrade_command( -# tool_list=["managed-1", "non-managed", "non-installed"], -# ) -# -# # All tools are verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# -# # The console contains the lines we expect, but not the non-requested, -# # non-managed, and non-installed tools. -# out = capsys.readouterr().out -# assert " - managed-1" in out -# assert " - managed-2" not in out -# assert " - non-managed" not in out -# assert " - non-installed" not in out -# -# # There is also an upgrade message for each tool -# assert "[managed-1] Upgrading Managed 1..." in out -# -# # The requested managed tools are upgraded -# ManagedSDK1.upgrade.assert_called_with() -# assert ManagedSDK2.upgrade.call_count == 0 -# assert NonManagedSDK.upgrade.call_count == 0 -# assert NonInstalledSDK.upgrade.call_count == 0 -# -# -# def test_upgrade_no__tools( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """If there is nothing up upgrade, a message is returned.""" -# -# upgrade_command( -# tool_list=["non-managed", "non-installed"], -# ) -# -# # All tools are verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# -# # The console contains no mention of tools... -# out = capsys.readouterr().out -# assert " - managed-1" not in out -# assert " - managed-2" not in out -# assert " - non-managed" not in out -# assert " - non-installed" not in out -# -# # ...but it *does* say there's nothing being managed. -# assert "Briefcase is not managing any tools." in out -# -# # Nothing is upgraded -# assert ManagedSDK1.upgrade.call_count == 0 -# assert ManagedSDK2.upgrade.call_count == 0 -# assert NonManagedSDK.upgrade.call_count == 0 -# assert NonInstalledSDK.upgrade.call_count == 0 -# -# -# def test_unknown_tool( -# upgrade_command, -# ManagedSDK1, -# ManagedSDK2, -# NonManagedSDK, -# NonInstalledSDK, -# capsys, -# ): -# """If a list of tools is provided, only those are listed.""" -# -# # Requesting an unknown tool raises an error -# with pytest.raises(BriefcaseCommandError): -# upgrade_command(tool_list=["managed-1", "unknown-tool"]) -# -# # All tools are still verified -# ManagedSDK1.verify.assert_called_with(upgrade_command.tools, install=False) -# ManagedSDK2.verify.assert_called_with(upgrade_command.tools, install=False) -# NonManagedSDK.verify.assert_called_with(upgrade_command.tools, install=False) -# NonInstalledSDK.verify.assert_called_with(upgrade_command.tools, install=False) +import pytest + +from briefcase.exceptions import UpgradeToolError + +from .conftest import ( + DummyManagedTool1, + DummyManagedTool2, + DummyManagedTool3, + DummyNotInstalledManagedTool, + DummyTool, + DummyUnManagedManagedTool, +) + + +def test_list_tools(upgrade_command, mock_tool_registry, capsys): + """The tools for upgrade can be listed.""" + upgrade_command(tool_list=[], list_tools=True) + + DummyTool.verify.assert_not_called() + + DummyUnManagedManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyNotInstalledManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool1.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool2.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool3.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert capsys.readouterr().out == ( + "\n" + "[upgrade] Briefcase is managing the following tools:\n" + " - Managed Dummy Tool 1 (managed_1)\n" + " - Managed Dummy Tool 2 (managed_2)\n" + " - Managed Dummy Tool 3 (managed_3)\n" + ) + + +def test_list_specific_tools(upgrade_command, mock_tool_registry, capsys): + """If a list of tools is provided, only those are listed.""" + upgrade_command(tool_list=["managed_1", "managed_2"], list_tools=True) + + DummyTool.verify.assert_not_called() + DummyUnManagedManagedTool.verify.assert_not_called() + DummyNotInstalledManagedTool.verify.assert_not_called() + DummyManagedTool3.verify.assert_not_called() + + DummyManagedTool1.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool2.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert capsys.readouterr().out == ( + "\n" + "[upgrade] Briefcase is managing the following tools:\n" + " - Managed Dummy Tool 1 (managed_1)\n" + " - Managed Dummy Tool 2 (managed_2)\n" + ) + + +def test_upgrade_tools(upgrade_command, mock_tool_registry, capsys): + """All managed tools can be upgraded.""" + upgrade_command(tool_list=[]) + + DummyTool.verify.assert_not_called() + + DummyUnManagedManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyNotInstalledManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool1.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool2.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool3.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert upgrade_command.tools.unmanaged_managed.actions == [] + + assert upgrade_command.tools.managed_1.actions == [ + "exists", + "uninstall", + "install", + ] + assert upgrade_command.tools.managed_2.actions == [ + "exists", + "uninstall", + "install", + ] + assert upgrade_command.tools.managed_3.actions == [ + "exists", + "uninstall", + "install", + ] + + assert capsys.readouterr().out == ( + "\n" + "[upgrade] Briefcase will upgrade the following tools:\n" + " - Managed Dummy Tool 1 (managed_1)\n" + " - Managed Dummy Tool 2 (managed_2)\n" + " - Managed Dummy Tool 3 (managed_3)\n" + "\n" + "[managed_1] Upgrading Managed Dummy Tool 1...\n" + "\n" + "[managed_2] Upgrading Managed Dummy Tool 2...\n" + "\n" + "[managed_3] Upgrading Managed Dummy Tool 3...\n" + ) + + +def test_upgrade_specific_tools(upgrade_command, mock_tool_registry, capsys): + """If a list of tools is provided, only those are listed.""" + upgrade_command(tool_list=["managed_1", "managed_2"]) + + DummyTool.verify.assert_not_called() + DummyUnManagedManagedTool.verify.assert_not_called() + DummyNotInstalledManagedTool.verify.assert_not_called() + DummyManagedTool3.verify.assert_not_called() + + DummyManagedTool1.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyManagedTool2.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert upgrade_command.tools.managed_1.actions == [ + "exists", + "uninstall", + "install", + ] + assert upgrade_command.tools.managed_2.actions == [ + "exists", + "uninstall", + "install", + ] + + assert capsys.readouterr().out == ( + "\n" + "[upgrade] Briefcase will upgrade the following tools:\n" + " - Managed Dummy Tool 1 (managed_1)\n" + " - Managed Dummy Tool 2 (managed_2)\n" + "\n" + "[managed_1] Upgrading Managed Dummy Tool 1...\n" + "\n" + "[managed_2] Upgrading Managed Dummy Tool 2...\n" + ) + + +def test_upgrade_no_tools(upgrade_command, mock_no_managed_tool_registry, capsys): + """If no tools are being managed, a message is returned.""" + upgrade_command(tool_list=[]) + + DummyTool.verify.assert_not_called() + + DummyUnManagedManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyNotInstalledManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert capsys.readouterr().out == "Briefcase is not managing any tools.\n" + + +def test_upgrade_unmanaged_tools(upgrade_command, mock_tool_registry, capsys): + """If only unmanaged tools are requested to upgrade, error is raised.""" + with pytest.raises( + UpgradeToolError, + match="Briefcase is not managing not_installed, unmanaged, unmanaged_managed.", + ): + upgrade_command(tool_list=["unmanaged", "unmanaged_managed", "not_installed"]) + + DummyTool.verify.assert_not_called() + DummyManagedTool1.verify.assert_not_called() + DummyManagedTool2.verify.assert_not_called() + DummyManagedTool3.verify.assert_not_called() + + DummyUnManagedManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyNotInstalledManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert capsys.readouterr().out == "" + + +def test_upgrade_mixed_tools(upgrade_command, mock_tool_registry, capsys): + """If managed and unmanaged tools are requested to upgrade, a warning is shown and + the upgrade continues.""" + upgrade_command( + tool_list=[ + "managed_1", + "managed_2", + "unmanaged", + "unmanaged_managed", + "not_installed", + ] + ) + + DummyTool.verify.assert_not_called() + DummyManagedTool3.verify.assert_not_called() + + DummyUnManagedManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + DummyNotInstalledManagedTool.verify.assert_called_once_with( + tools=upgrade_command.tools, install=False + ) + + assert upgrade_command.tools.unmanaged_managed.actions == [] + assert upgrade_command.tools.managed_1.actions == [ + "exists", + "uninstall", + "install", + ] + assert upgrade_command.tools.managed_2.actions == [ + "exists", + "uninstall", + "install", + ] + + assert capsys.readouterr().out == ( + "Briefcase is not managing not_installed, unmanaged, unmanaged_managed.\n" + "\n" + "[upgrade] Briefcase will upgrade the following tools:\n" + " - Managed Dummy Tool 1 (managed_1)\n" + " - Managed Dummy Tool 2 (managed_2)\n" + "\n" + "[managed_1] Upgrading Managed Dummy Tool 1...\n" + "\n" + "[managed_2] Upgrading Managed Dummy Tool 2...\n" + ) + + +def test_unknown_tool(upgrade_command, mock_tool_registry, capsys): + """If a list of tools is provided, only those are listed.""" + + # Requesting an unknown tool raises an error + with pytest.raises( + UpgradeToolError, + match="Briefcase does not know how to manage unknown_tool_1, unknown_tool_2.", + ): + upgrade_command(tool_list=["managed_1", "unknown_tool_1", "unknown_tool_2"]) + + DummyTool.verify.assert_not_called() + DummyManagedTool1.verify.assert_not_called() + DummyManagedTool2.verify.assert_not_called() + DummyManagedTool3.verify.assert_not_called() + DummyUnManagedManagedTool.verify.assert_not_called() + DummyNotInstalledManagedTool.verify.assert_not_called() + + assert capsys.readouterr().out == "" diff --git a/tests/conftest.py b/tests/conftest.py index 7bacd6765..4f91dcd4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,9 @@ import inspect import subprocess import time -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY -import git as git_ import pytest -from git import exc as git_exceptions from briefcase.config import AppConfig from briefcase.console import Printer @@ -19,15 +17,9 @@ def pytest_sessionfinish(session, exitstatus): Printer.dev_null.close() -@pytest.fixture -def mock_git(): - git = MagicMock(spec_set=git_) - git.exc = git_exceptions - return git - - -# alias print() to allow non-briefcase code to use it +# alias so fixtures can still use them _print = print +_sleep = time.sleep def monkeypatched_print(*args, **kwargs): @@ -46,16 +38,13 @@ def monkeypatched_print(*args, **kwargs): @pytest.fixture(autouse=True) def no_print(monkeypatch): - """Replace builtin print function for ALL tests.""" + """Replace builtin ``print()`` for ALL tests.""" monkeypatch.setattr("builtins.print", monkeypatched_print) -_sleep = time.sleep - - @pytest.fixture def sleep_zero(monkeypatch): - """Replace all calls to ``time.sleep(x)`` with ``time.sleep(0).""" + """Replace all calls to ``time.sleep(x)`` with ``time.sleep(0)``.""" monkeypatch.setattr(time, "sleep", lambda x: _sleep(0)) diff --git a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py index 1b560a4de..d4161b12d 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py +++ b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py @@ -30,3 +30,8 @@ def test_upgrade_failure(mock_tools, android_sdk): check=True, stream_output=False, ) + + +def test_uninstall_does_nothing(mock_tools, android_sdk): + """Uninstalling the Android SDK does nothing since it is upgraded in place.""" + android_sdk.uninstall() diff --git a/tests/integrations/base/conftest.py b/tests/integrations/base/conftest.py new file mode 100644 index 000000000..75a1e2082 --- /dev/null +++ b/tests/integrations/base/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from briefcase.console import Console, Log +from briefcase.integrations.base import ToolCache + + +@pytest.fixture +def simple_tools(tmp_path): + return ToolCache(logger=Log(), console=Console(), base_path=tmp_path) diff --git a/tests/integrations/base/test_Tool.py b/tests/integrations/base/test_Tool.py index 576ae5f05..d1711900f 100644 --- a/tests/integrations/base/test_Tool.py +++ b/tests/integrations/base/test_Tool.py @@ -1,85 +1,164 @@ +from unittest.mock import MagicMock + import pytest +from briefcase.exceptions import ( + MissingToolError, + NonManagedToolError, + UnsupportedHostError, +) from briefcase.integrations.base import ManagedTool, Tool -@pytest.fixture -def unmanaged_tool(mock_tools) -> Tool: - class DummyTool(Tool): - """Unmanaged Tool testing class.""" +class DummyTool(Tool): + """Unmanaged Tool testing class.""" + + name = "UnmanagedDummyTool" + full_name = "Unmanaged Dummy Tool" + supported_host_os = {"wonky"} + + @classmethod + def verify_install(cls, tools, **kwargs): + return f"i'm a {cls.name}" + + +class DummyManagedTool(ManagedTool): + """Managed Tool testing class.""" + + name = "ManagedDummyTool" + full_name = "Managed Dummy Tool" + supported_host_os = {"wonky"} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.actions = [] + + @classmethod + def verify_install(cls, tools, **kwargs): + return f"i'm a {cls.name}" - name = "UnmanagedDummyTool" - full_name = "Unmanaged Dummy Tool" + def exists(self) -> bool: + self.actions.append("exists") + return True - def verify_install(self, tools, **kwargs): - pass + def install(self, *args, **kwargs): + self.actions.append("install") + def uninstall(self, *args, **kwargs): + self.actions.append("uninstall") + + +@pytest.fixture +def unmanaged_tool(mock_tools) -> DummyTool: return DummyTool(tools=mock_tools) @pytest.fixture -def managed_tool(mock_tools) -> ManagedTool: - class DummyTool(ManagedTool): - """Managed Tool testing class.""" +def managed_tool(mock_tools) -> DummyManagedTool: + return DummyManagedTool(tools=mock_tools) - name = "ManagedDummyTool" - full_name = "Managed Dummy Tool" - managed_install = True - def verify_install(self, tools, **kwargs): - pass +@pytest.mark.parametrize( + "klass, kwargs", + [ + (DummyTool, {}), + (DummyManagedTool, {}), + (DummyTool, {"one": "two", "three": "four"}), + (DummyManagedTool, {"one": "two", "three": "four"}), + ], +) +def test_tool_verify(mock_tools, klass, kwargs, monkeypatch): + """Tool verification checks host OS and tool install.""" + mock_verify_host = MagicMock(wraps=klass.verify_host) + mock_verify_install = MagicMock(wraps=klass.verify_install) + monkeypatch.setattr(klass, "verify_host", mock_verify_host) + monkeypatch.setattr(klass, "verify_install", mock_verify_install) - def exists(self) -> bool: - pass + mock_tools.host_os = "wonky" - def install(self, *args, **kwargs): - pass + tool = klass.verify(tools=mock_tools, **kwargs) - def uninstall(self, *args, **kwargs): - pass + mock_verify_host.assert_called_once_with(tools=mock_tools) + mock_verify_install.assert_called_once_with(tools=mock_tools, app=None, **kwargs) + assert tool == f"i'm a {klass.name}" - return DummyTool(tools=mock_tools) +@pytest.mark.parametrize( + "klass, kwargs", + [ + (DummyTool, {}), + (DummyManagedTool, {}), + (DummyTool, {"one": "two", "three": "four"}), + (DummyManagedTool, {"one": "two", "three": "four"}), + ], +) +def test_tool_verify_with_app(mock_tools, first_app_config, klass, kwargs, monkeypatch): + """App-bound Tool verification checks host OS and tool install.""" + mock_verify_host = MagicMock(wraps=klass.verify_host) + mock_verify_install = MagicMock(wraps=klass.verify_install) + monkeypatch.setattr(klass, "verify_host", mock_verify_host) + monkeypatch.setattr(klass, "verify_install", mock_verify_install) + + mock_tools.host_os = "wonky" + + tool = klass.verify(tools=mock_tools, app=first_app_config, **kwargs) + + mock_verify_host.assert_called_once_with(tools=mock_tools) + mock_verify_install.assert_called_once_with( + tools=mock_tools, + app=first_app_config, + **kwargs, + ) + assert tool == f"i'm a {klass.name}" + + +@pytest.mark.parametrize("klass", [DummyTool, DummyManagedTool]) +def test_tool_unsupported_host_os(mock_tools, klass): + """Tool verification fails for unsupported Host OS.""" + mock_tools.host_os = "not wonky" + + with pytest.raises( + UnsupportedHostError, + match=f"{klass.name} is not supported on not wonky", + ): + klass.verify(tools=mock_tools) + + +def test_unmanaged_install_is_false(unmanaged_tool): + """Tool.managed_install defaults False.""" + assert unmanaged_tool.managed_install is False + + +def test_managed_install_is_true(managed_tool): + """Tool.managed_install defaults False.""" + assert managed_tool.managed_install is True + + +def test_managed_upgrade(managed_tool): + """Order of operations is correct for upgrade.""" + managed_tool.upgrade() + + assert managed_tool.actions == ["exists", "uninstall", "install"] + + +def test_managed_raises_if_unmanaged(mock_tools): + """If a ManagedTool is unmanaged, upgrade raises.""" + + class NonManagedManagedTool(DummyManagedTool): + @property + def managed_install(self) -> bool: + return False + + with pytest.raises(NonManagedToolError): + NonManagedManagedTool(tools=mock_tools).upgrade() + + +def test_managed_raises_if_not_exists(mock_tools): + """If a ManagedTool doesn't exist, upgrade raises.""" + + class NonExistsManagedTool(DummyManagedTool): + def exists(self) -> bool: + return False -# def test_managed_install_defaults_false(unmanaged_tool): -# """Tool.managed_install defaults False.""" -# assert unmanaged_tool.managed_install is False -# -# -# def test_install_raises_for_managed_tool(managed_tool): -# """Tool.install() raises NotImplementedError for managed tool.""" -# with pytest.raises( -# NotImplementedError, -# match="Missing implementation for Tool 'DummyTool'", -# ): -# managed_tool.install() -# -# -# def test_uninstall_raises_for_managed_tool(managed_tool): -# """Tool.uninstall() raises NotImplementedError for managed tool.""" -# with pytest.raises( -# NotImplementedError, -# match="Missing implementation for Tool 'DummyTool'", -# ): -# managed_tool.uninstall() -# -# -# def test_upgrade_raises_for_managed_tool_when_exists(managed_tool): -# """Tool.upgrade() raises NotImplementedError for managed tool when it exists.""" -# managed_tool.exists = lambda: True -# with pytest.raises( -# NotImplementedError, -# match="Missing implementation for Tool 'DummyTool'", -# ): -# managed_tool.upgrade() -# -# -# def test_upgrade_raises_for_managed_tool_when_not_exists(managed_tool): -# """Tool.upgrade() raises MissingToolError for managed tool when it does not -# exist.""" -# managed_tool.exists = lambda: False -# with pytest.raises( -# MissingToolError, -# match="Unable to locate 'Managed Dummy Tool'. Has it been installed?", -# ): -# managed_tool.upgrade() + with pytest.raises(MissingToolError): + NonExistsManagedTool(tools=mock_tools).upgrade() diff --git a/tests/integrations/base/test_ToolCache.py b/tests/integrations/base/test_ToolCache.py index 7886befdd..a4464e03c 100644 --- a/tests/integrations/base/test_ToolCache.py +++ b/tests/integrations/base/test_ToolCache.py @@ -1,12 +1,9 @@ import importlib -import inspect import os -import pkgutil import platform import shutil import sys from pathlib import Path -from typing import Dict, Set, Type import pytest import requests @@ -14,60 +11,13 @@ import briefcase.integrations from briefcase.console import Console, Log -from briefcase.integrations.base import ManagedTool, Tool, ToolCache, tool_registry +from briefcase.integrations.base import ToolCache - -@pytest.fixture -def simple_tools(tmp_path): - return ToolCache(logger=Log(), console=Console(), base_path=tmp_path) - - -@pytest.fixture -def all_defined_tools() -> Set[Type[Tool]]: - """All classes that subclass Tool.""" - return { - tool - for toolset in map(tools_for_module, briefcase.integrations.__all__) - for tool in toolset.values() - } - - -def tools_for_module(tool_module_name: str) -> Dict[str, Type[Tool]]: - """Return classes that subclass Tool in a module in - ``briefcase.integrations``, e.g. {"android_sdk": AndroidSDK}.""" - return { - klass_name: klass - for klass_name, klass in inspect.getmembers( - sys.modules[f"briefcase.integrations.{tool_module_name}"], - lambda klass: ( - inspect.isclass(klass) - and issubclass(klass, (Tool, ManagedTool)) - and klass not in {Tool, ManagedTool} - ), - ) - } - - -def test_tool_registry(all_defined_tools, simple_tools): - """The Tool Registry must contain all defined Tools.""" - # test uses subset since registry will contain dummy testing tools - assert all_defined_tools.issubset(tool_registry.values()) - - -def test_unique_tool_names(all_defined_tools): - """All tools must have a unique name.""" - assert len(all_defined_tools) == len({t.name for t in all_defined_tools}) - - -def test_valid_tool_names(all_defined_tools): - """All tools must have a valid name.""" - assert all(" " not in t.name for t in all_defined_tools) +from .test_tool_registry import integrations_modules, tools_for_module def test_toolcache_typing(): """Tool typing for ToolCache is correct.""" - # Modules in ``briefcase.integrations`` that do not contain tools. - nontool_modules = {"base"} # Tools that are intentionally not annotated in ToolCache. tools_unannotated = {"cookiecutter"} # Tool names to exclude from the dynamic annotation checks; they are manually checked. @@ -89,12 +39,7 @@ def test_toolcache_typing(): } # Ensure all modules containing Tools are exported in ``briefcase.integrations``. - tool_modules = { - module.name - for module in pkgutil.iter_modules(briefcase.integrations.__path__) - if module.name not in nontool_modules - } - assert sorted(tool_modules) == sorted(briefcase.integrations.__all__) + assert sorted(integrations_modules()) == sorted(briefcase.integrations.__all__) # Ensure defined Tool modules/classes are annotated in ToolCache. for tool_module_name in briefcase.integrations.__all__: diff --git a/tests/integrations/base/test_tool_registry.py b/tests/integrations/base/test_tool_registry.py new file mode 100644 index 000000000..298396849 --- /dev/null +++ b/tests/integrations/base/test_tool_registry.py @@ -0,0 +1,60 @@ +import inspect +import pkgutil +import sys +from typing import Dict, Set, Type + +import pytest + +import briefcase.integrations +from briefcase.integrations.base import ManagedTool, Tool, tool_registry + + +def integrations_modules() -> Set[str]: + """All modules in ``briefcase.integrations`` irrespective of whether they are + defined in ``briefcase.integrations.__all__``""" + return { + module.name + for module in pkgutil.iter_modules(briefcase.integrations.__path__) + if module.name not in {"base"} + } + + +def tools_for_module(tool_module_name: str) -> Dict[str, Type[Tool]]: + """Return classes that subclass Tool in a module in + ``briefcase.integrations``, e.g. {"android_sdk": AndroidSDK}.""" + return dict( + inspect.getmembers( + sys.modules[f"briefcase.integrations.{tool_module_name}"], + lambda klass: ( + inspect.isclass(klass) + and issubclass(klass, (Tool, ManagedTool)) + and klass not in {Tool, ManagedTool} + ), + ) + ) + + +@pytest.fixture +def all_defined_tools() -> Set[Type[Tool]]: + """All classes under ``briefcase.integrations`` that subclass Tool.""" + return { + tool + for toolset in map(tools_for_module, integrations_modules()) + for tool in toolset.values() + } + + +def test_tool_registry(all_defined_tools, simple_tools): + """The Tool Registry must contain all defined Tools.""" + # test uses subset since registry will contain dummy testing tools + assert all_defined_tools.issubset(tool_registry.values()) + + +def test_unique_tool_names(all_defined_tools): + """All tools must have a unique name.""" + assert len(all_defined_tools) == len({t.name for t in all_defined_tools}) + + +def test_valid_tool_names(all_defined_tools): + """All tools must have a name without spaces.""" + assert all(" " not in t.name for t in all_defined_tools) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index d95c1272d..b9719e940 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -487,21 +487,28 @@ def test_run_idle_device(run_command, first_app_config): def test_log_file_extra(run_command, monkeypatch): """Android commands register a log file extra to list SDK packages.""" - verify = mock.MagicMock(return_value=run_command.tools.android_sdk) - monkeypatch.setattr(AndroidSDK, "verify", verify) + mock_android_sdk_verify = mock.MagicMock(return_value=run_command.tools.android_sdk) + monkeypatch.setattr(AndroidSDK, "verify", mock_android_sdk_verify) monkeypatch.setattr(AndroidSDK, "verify_emulator", mock.MagicMock()) # Even if one command triggers another, the sdkmanager should only be run once. run_command.update_command.verify_tools() run_command.verify_tools() - sdk_manager = "/path/to/android_sdk/cmdline-tools/latest/bin/sdkmanager" - if platform.system() == "Windows": - sdk_manager += ".bat" + # Android SDK tool was verified + mock_android_sdk_verify.assert_has_calls([mock.call(tools=run_command.tools)] * 2) + assert isinstance(run_command.tools.android_sdk, AndroidSDK) - run_command.tools.logger.save_log = True + # list_packages() was not called run_command.tools.subprocess.check_output.assert_not_called() + + # list_packages() is called when saving the log + run_command.tools.logger.save_log = True run_command.tools.logger.save_log_to_file(run_command) + + sdk_manager = "/path/to/android_sdk/cmdline-tools/latest/bin/sdkmanager" + if platform.system() == "Windows": + sdk_manager += ".bat" run_command.tools.subprocess.check_output.assert_called_once_with( [normpath(sdk_manager), "--list_installed"], env={ diff --git a/tests/platforms/iOS/xcode/test_mixin.py b/tests/platforms/iOS/xcode/test_mixin.py index 6d0b3f6a5..245917590 100644 --- a/tests/platforms/iOS/xcode/test_mixin.py +++ b/tests/platforms/iOS/xcode/test_mixin.py @@ -1,5 +1,8 @@ +from unittest.mock import MagicMock + import pytest +import briefcase.integrations.xcode from briefcase.console import Console, Log from briefcase.exceptions import NoDistributionArtefact from briefcase.platforms.iOS.xcode import iOSXcodeCreateCommand @@ -37,3 +40,41 @@ def test_distribution_path(create_command, first_app_config, tmp_path): match=r"WARNING: No distributable artefact has been generated", ): create_command.distribution_path(first_app_config) + + +def test_verify(create_command, monkeypatch): + """If you're on macOS, you can verify tools.""" + create_command.tools.host_os = "Darwin" + + mock_ensure_xcode_is_installed = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_xcode_is_installed", + mock_ensure_xcode_is_installed, + ) + mock_ensure_command_line_tools_are_installed = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) + + create_command.verify_tools() + + assert create_command.tools.xcode_cli is not None + mock_ensure_xcode_is_installed.assert_called_once_with( + tools=create_command.tools, + min_version=(13, 0, 0), + ) + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + tools=create_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with( + tools=create_command.tools + ) diff --git a/tests/platforms/linux/appimage/test_mixin.py b/tests/platforms/linux/appimage/test_mixin.py index 2d09a4d17..cfa81c16b 100644 --- a/tests/platforms/linux/appimage/test_mixin.py +++ b/tests/platforms/linux/appimage/test_mixin.py @@ -3,6 +3,7 @@ import pytest +import briefcase.platforms.linux.appimage from briefcase.console import Console, Log from briefcase.integrations.docker import Docker, DockerAppContext from briefcase.integrations.subprocess import Subprocess @@ -98,16 +99,41 @@ def test_verify_linux_docker(create_command, tmp_path, first_app_config, monkeyp create_command.use_docker = True # Mock Docker tool verification - Docker.verify = MagicMock() - DockerAppContext.verify = MagicMock() + mock__version_compat = MagicMock(spec=Docker._version_compat) + mock__user_access = MagicMock(spec=Docker._user_access) + mock__buildx_installed = MagicMock(spec=Docker._buildx_installed) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_version_compat", + mock__version_compat, + ) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_user_access", + mock__user_access, + ) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_buildx_installed", + mock__buildx_installed, + ) + mock_docker_app_context_verify = MagicMock(spec=DockerAppContext.verify) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.DockerAppContext, + "verify", + mock_docker_app_context_verify, + ) # Verify the tools create_command.verify_tools() create_command.verify_app_tools(app=first_app_config) # Docker and Docker app context are verified - Docker.verify.assert_called_with(tools=create_command.tools) - DockerAppContext.verify.assert_called_with( + mock__version_compat.assert_called_with(tools=create_command.tools) + mock__user_access.assert_called_with(tools=create_command.tools) + mock__buildx_installed.assert_called_with(tools=create_command.tools) + assert isinstance(create_command.tools.docker, Docker) + mock_docker_app_context_verify.assert_called_with( tools=create_command.tools, app=first_app_config, image_tag="briefcase/com.example.first-app:appimage", @@ -130,22 +156,52 @@ def test_verify_linux_docker(create_command, tmp_path, first_app_config, monkeyp ) -def test_verify_non_linux_docker(create_command, tmp_path, first_app_config): +def test_verify_non_linux_docker( + create_command, + first_app_config, + monkeypatch, + tmp_path, +): """If Docker is enabled on non-Linux, the Docker alias is set.""" create_command.tools.host_os = "Darwin" create_command.use_docker = True # Mock Docker tool verification - Docker.verify = MagicMock() - DockerAppContext.verify = MagicMock() + mock__version_compat = MagicMock(spec=Docker._version_compat) + mock__user_access = MagicMock(spec=Docker._user_access) + mock__buildx_installed = MagicMock(spec=Docker._buildx_installed) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_version_compat", + mock__version_compat, + ) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_user_access", + mock__user_access, + ) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.Docker, + "_buildx_installed", + mock__buildx_installed, + ) + mock_docker_app_context_verify = MagicMock(spec=DockerAppContext.verify) + monkeypatch.setattr( + briefcase.platforms.linux.appimage.DockerAppContext, + "verify", + mock_docker_app_context_verify, + ) # Verify the tools create_command.verify_tools() create_command.verify_app_tools(app=first_app_config) # Docker and Docker app context are verified - Docker.verify.assert_called_with(tools=create_command.tools) - DockerAppContext.verify.assert_called_with( + mock__version_compat.assert_called_with(tools=create_command.tools) + mock__user_access.assert_called_with(tools=create_command.tools) + mock__buildx_installed.assert_called_with(tools=create_command.tools) + assert isinstance(create_command.tools.docker, Docker) + mock_docker_app_context_verify.assert_called_with( tools=create_command.tools, app=first_app_config, image_tag="briefcase/com.example.first-app:appimage", diff --git a/tests/platforms/linux/flatpak/test_mixin.py b/tests/platforms/linux/flatpak/test_mixin.py index 379534c9d..a9b42aac9 100644 --- a/tests/platforms/linux/flatpak/test_mixin.py +++ b/tests/platforms/linux/flatpak/test_mixin.py @@ -2,6 +2,7 @@ import pytest +import briefcase.platforms.linux.flatpak from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseConfigError from briefcase.integrations.flatpak import Flatpak @@ -125,13 +126,21 @@ def test_custom_runtime_sdk_only(create_command, first_app_config, tmp_path): create_command.flatpak_sdk(first_app_config) -def test_verify_linux(create_command, tmp_path): +def test_verify_linux(create_command, monkeypatch, tmp_path): """Verifying on Linux creates an SDK wrapper.""" create_command.tools.host_os = "Linux" create_command.tools.subprocess = MagicMock(spec_set=Subprocess) + mock_flatpak_verify = MagicMock(wraps=Flatpak.verify) + monkeypatch.setattr( + briefcase.platforms.linux.flatpak.Flatpak, + "verify", + mock_flatpak_verify, + ) + # Verify the tools create_command.verify_tools() - # No error and an SDK wrapper is created + # Flatpak tool was verified + mock_flatpak_verify.assert_called_once_with(tools=create_command.tools) assert isinstance(create_command.tools.flatpak, Flatpak) diff --git a/tests/platforms/linux/system/test_mixin__verify.py b/tests/platforms/linux/system/test_mixin__verify.py index fad3299bc..88fb0ab3d 100644 --- a/tests/platforms/linux/system/test_mixin__verify.py +++ b/tests/platforms/linux/system/test_mixin__verify.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock +import briefcase.platforms.linux.system from briefcase.integrations.docker import Docker, DockerAppContext from briefcase.integrations.subprocess import Subprocess @@ -48,8 +49,30 @@ def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch): first_app_config.python_version_tag = "3" # Mock Docker tool verification - Docker.verify = MagicMock() - DockerAppContext.verify = MagicMock() + mock__version_compat = MagicMock(spec=Docker._version_compat) + mock__user_access = MagicMock(spec=Docker._user_access) + mock__buildx_installed = MagicMock(spec=Docker._buildx_installed) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_version_compat", + mock__version_compat, + ) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_user_access", + mock__user_access, + ) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_buildx_installed", + mock__buildx_installed, + ) + mock_docker_app_context_verify = MagicMock(spec=DockerAppContext.verify) + monkeypatch.setattr( + briefcase.platforms.linux.system.DockerAppContext, + "verify", + mock_docker_app_context_verify, + ) create_command.verify_python = MagicMock() # Verify the tools @@ -57,8 +80,11 @@ def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch): create_command.verify_app_tools(app=first_app_config) # Docker and Docker app context are verified - Docker.verify.assert_called_with(tools=create_command.tools) - DockerAppContext.verify.assert_called_with( + mock__version_compat.assert_called_with(tools=create_command.tools) + mock__user_access.assert_called_with(tools=create_command.tools) + mock__buildx_installed.assert_called_with(tools=create_command.tools) + assert isinstance(create_command.tools.docker, Docker) + mock_docker_app_context_verify.assert_called_with( tools=create_command.tools, app=first_app_config, image_tag="briefcase/com.example.first-app:somevendor-surprising", @@ -91,7 +117,7 @@ def test_linux_docker(create_command, tmp_path, first_app_config, monkeypatch): create_command.verify_python.assert_not_called() -def test_non_linux_docker(create_command, tmp_path, first_app_config): +def test_non_linux_docker(create_command, first_app_config, monkeypatch, tmp_path): """If Docker is enabled on non-Linux, the Docker alias is set.""" create_command.tools.host_os = "Darwin" create_command.target_image = "somevendor:surprising" @@ -103,8 +129,30 @@ def test_non_linux_docker(create_command, tmp_path, first_app_config): first_app_config.python_version_tag = "3" # Mock Docker tool verification - Docker.verify = MagicMock() - DockerAppContext.verify = MagicMock() + mock__version_compat = MagicMock(spec=Docker._version_compat) + mock__user_access = MagicMock(spec=Docker._user_access) + mock__buildx_installed = MagicMock(spec=Docker._buildx_installed) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_version_compat", + mock__version_compat, + ) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_user_access", + mock__user_access, + ) + monkeypatch.setattr( + briefcase.platforms.linux.system.Docker, + "_buildx_installed", + mock__buildx_installed, + ) + mock_docker_app_context_verify = MagicMock(spec=DockerAppContext.verify) + monkeypatch.setattr( + briefcase.platforms.linux.system.DockerAppContext, + "verify", + mock_docker_app_context_verify, + ) create_command.verify_python = MagicMock() # Verify the tools @@ -112,8 +160,11 @@ def test_non_linux_docker(create_command, tmp_path, first_app_config): create_command.verify_app_tools(app=first_app_config) # Docker and Docker app context are verified - Docker.verify.assert_called_with(tools=create_command.tools) - DockerAppContext.verify.assert_called_with( + mock__version_compat.assert_called_with(tools=create_command.tools) + mock__user_access.assert_called_with(tools=create_command.tools) + mock__buildx_installed.assert_called_with(tools=create_command.tools) + assert isinstance(create_command.tools.docker, Docker) + mock_docker_app_context_verify.assert_called_with( tools=create_command.tools, app=first_app_config, image_tag="briefcase/com.example.first-app:somevendor-surprising", diff --git a/tests/platforms/linux/system/test_mixin__verify_system_python.py b/tests/platforms/linux/system/test_mixin__verify_system_python.py index b45b2319f..6bdb88074 100644 --- a/tests/platforms/linux/system/test_mixin__verify_system_python.py +++ b/tests/platforms/linux/system/test_mixin__verify_system_python.py @@ -37,7 +37,7 @@ def test_valid_python3(monkeypatch, create_command): ) def test_bad_python3(monkeypatch, create_command, resolved_path, expected_error): """If the system Python3 isn't obviously a Python3, an error is raised.""" - # Mock a Python3 symlink that isn'tthe existence of a valid non-docker system Python + # Mock a Python3 symlink that isn't the existence of a valid non-docker system Python # with the same major/minor as the current Python python3 = MagicMock() python3.resolve.return_value = Path(resolved_path) diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py index 06f41145b..baf446228 100644 --- a/tests/platforms/macOS/app/test_package.py +++ b/tests/platforms/macOS/app/test_package.py @@ -5,6 +5,7 @@ import pytest +import briefcase.integrations.xcode from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseCommandError from briefcase.platforms.macOS.app import macOSAppPackageCommand @@ -834,14 +835,30 @@ def test_dmg_with_missing_installer_background( ) -def test_verify(package_command): +def test_verify(package_command, monkeypatch): """If you're on macOS, you can verify tools.""" + package_command.tools.host_os = "Darwin" + # Mock the existence of the command line tools - package_command.tools.subprocess.check_output.side_effect = [ - subprocess.CalledProcessError(cmd=["xcode-select", "--install"], returncode=1), - "clang 37.42", # clang --version - ] + mock_ensure_command_line_tools_are_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) package_command.verify_tools() assert package_command.tools.xcode_cli is not None + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + tools=package_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with( + tools=package_command.tools + ) diff --git a/tests/platforms/macOS/xcode/test_package.py b/tests/platforms/macOS/xcode/test_package.py index aa8eef7d1..a5a07714c 100644 --- a/tests/platforms/macOS/xcode/test_package.py +++ b/tests/platforms/macOS/xcode/test_package.py @@ -1 +1,58 @@ -# skip since packaging uses the same code as app command +from unittest import mock + +import pytest + +import briefcase.integrations.xcode +from briefcase.console import Console, Log +from briefcase.platforms.macOS.xcode import macOSXcodePackageCommand + +# skip most tests since packaging uses the same code as app command + + +@pytest.fixture +def package_command(tmp_path): + command = macOSXcodePackageCommand( + logger=Log(), + console=Console(), + base_path=tmp_path / "base_path", + data_path=tmp_path / "briefcase", + ) + return command + + +def test_verify(package_command, monkeypatch): + """If you're on macOS, you can verify tools.""" + package_command.tools.host_os = "Darwin" + + mock_ensure_xcode_is_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_xcode_is_installed", + mock_ensure_xcode_is_installed, + ) + mock_ensure_command_line_tools_are_installed = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "ensure_command_line_tools_are_installed", + mock_ensure_command_line_tools_are_installed, + ) + mock_confirm_xcode_license_accepted = mock.MagicMock() + monkeypatch.setattr( + briefcase.integrations.xcode, + "confirm_xcode_license_accepted", + mock_confirm_xcode_license_accepted, + ) + + package_command.verify_tools() + + assert package_command.tools.xcode_cli is not None + mock_ensure_xcode_is_installed.assert_called_once_with( + tools=package_command.tools, + min_version=(13, 0, 0), + ) + mock_ensure_command_line_tools_are_installed.assert_called_once_with( + tools=package_command.tools + ) + mock_confirm_xcode_license_accepted.assert_called_once_with( + tools=package_command.tools + ) diff --git a/tests/platforms/windows/app/test_build.py b/tests/platforms/windows/app/test_build.py index 2d29f36a2..86ef0700c 100644 --- a/tests/platforms/windows/app/test_build.py +++ b/tests/platforms/windows/app/test_build.py @@ -44,22 +44,47 @@ def test_verify_without_windows_sdk(build_command, monkeypatch): monkeypatch.setattr(briefcase.platforms.windows.app, "WindowsSDK", mock_sdk) mock_sdk.verify.side_effect = BriefcaseCommandError("Windows SDK") + mock_rcedit_verify = mock.MagicMock(wraps=RCEdit.verify) + monkeypatch.setattr( + briefcase.platforms.windows.app.RCEdit, + "verify", + mock_rcedit_verify, + ) + build_command.verify_tools() - # No error and an SDK wrapper is created + # RCEdit tool was verified + mock_rcedit_verify.assert_called_once_with(tools=build_command.tools) assert isinstance(build_command.tools.rcedit, RCEdit) # Windows SDK tool not created assert not hasattr(build_command.tools, "windows_sdk") -def test_verify_with_windows_sdk(build_command, windows_sdk, tmp_path): +def test_verify_with_windows_sdk(build_command, windows_sdk, monkeypatch, tmp_path): """Verifying on Windows creates an RCEdit and Windows SDK wrapper.""" build_command.tools.windows_sdk = windows_sdk + mock_windows_sdk_verify = mock.MagicMock(wraps=WindowsSDK.verify) + monkeypatch.setattr( + briefcase.platforms.windows.app.WindowsSDK, + "verify", + mock_windows_sdk_verify, + ) + + mock_rcedit_verify = mock.MagicMock(wraps=RCEdit.verify) + monkeypatch.setattr( + briefcase.platforms.windows.app.RCEdit, + "verify", + mock_rcedit_verify, + ) + build_command.verify_tools() - # No error and SDK wrappers are created + # RCEdit tool was verified + mock_rcedit_verify.assert_called_once_with(tools=build_command.tools) assert isinstance(build_command.tools.rcedit, RCEdit) + # WindowsSDK tool was verified + mock_windows_sdk_verify.assert_called_once_with(tools=build_command.tools) assert isinstance(build_command.tools.windows_sdk, WindowsSDK) diff --git a/tests/platforms/windows/app/test_package.py b/tests/platforms/windows/app/test_package.py index b5a012f9d..4c894a98b 100644 --- a/tests/platforms/windows/app/test_package.py +++ b/tests/platforms/windows/app/test_package.py @@ -4,6 +4,7 @@ import pytest +import briefcase.platforms.windows from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.subprocess import Subprocess @@ -68,26 +69,51 @@ def test_package_formats(package_command): assert package_command.default_packaging_format == "msi" -def test_verify(package_command): +def test_verify(package_command, monkeypatch): """Verifying on Windows creates a WiX wrapper.""" # prime Command to _not_ need Windows SDK package_command._windows_sdk_needed = False + mock_wix_verify = mock.MagicMock(wraps=WiX.verify) + monkeypatch.setattr( + briefcase.platforms.windows.WiX, + "verify", + mock_wix_verify, + ) + package_command.verify_tools() - # No error and an SDK wrapper is created + # WiX tool was verified + mock_wix_verify.assert_called_once_with(tools=package_command.tools) assert isinstance(package_command.tools.wix, WiX) -def test_verify_with_signing(package_command): +def test_verify_with_signing(package_command, monkeypatch): """Verifying on Windows creates WiX and WindowsSDK wrappers when code signing.""" # prime Command to need Windows SDK package_command._windows_sdk_needed = True + mock_windows_sdk_verify = mock.MagicMock(wraps=WindowsSDK.verify) + monkeypatch.setattr( + briefcase.platforms.windows.WindowsSDK, + "verify", + mock_windows_sdk_verify, + ) + + mock_wix_verify = mock.MagicMock(wraps=WiX.verify) + monkeypatch.setattr( + briefcase.platforms.windows.WiX, + "verify", + mock_wix_verify, + ) + package_command.verify_tools() - # No error and SDK wrappers are created + # WiX tool was verified + mock_wix_verify.assert_called_once_with(tools=package_command.tools) assert isinstance(package_command.tools.wix, WiX) + # WindowsSDK tool was verified + mock_windows_sdk_verify.assert_called_once_with(tools=package_command.tools) assert isinstance(package_command.tools.windows_sdk, WindowsSDK) diff --git a/tests/platforms/windows/visualstudio/test_build.py b/tests/platforms/windows/visualstudio/test_build.py index 2d438ce1f..6184fb09c 100644 --- a/tests/platforms/windows/visualstudio/test_build.py +++ b/tests/platforms/windows/visualstudio/test_build.py @@ -1,10 +1,10 @@ -import platform import subprocess from pathlib import Path from unittest import mock import pytest +import briefcase.platforms.windows.visualstudio from briefcase.console import Console, Log from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.subprocess import Subprocess @@ -28,15 +28,21 @@ def build_command(tmp_path): return command -@pytest.mark.skipif(platform.system() != "Windows", reason="Windows specific tests") -def test_verify(build_command): +def test_verify(build_command, monkeypatch): """Verifying on Windows creates a VisualStudio wrapper.""" + build_command.tools.host_os = "Windows" - build_command.tools.subprocess = mock.MagicMock(spec_set=Subprocess) + mock_visualstudio_verify = mock.MagicMock(wraps=VisualStudio.verify) + monkeypatch.setattr( + briefcase.platforms.windows.visualstudio.VisualStudio, + "verify", + mock_visualstudio_verify, + ) build_command.verify_tools() - # No error and an SDK wrapper is created + # VisualStudio tool was verified + mock_visualstudio_verify.assert_called_once_with(tools=build_command.tools) assert isinstance(build_command.tools.visualstudio, VisualStudio)