Skip to content

Commit

Permalink
tests: mock and test provider calls from lifecycle.py (#3928)
Browse files Browse the repository at this point in the history
tests: mock and test provider calls from `lifecycle.py`

Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal committed Oct 3, 2022
1 parent 4e23e8f commit f0173be
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 37 deletions.
22 changes: 5 additions & 17 deletions snapcraft/parts/lifecycle.py
Expand Up @@ -26,7 +26,7 @@
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

import craft_parts
from craft_cli import EmitterMode, emit
from craft_cli import emit
from craft_parts import ProjectInfo, StepInfo, callbacks

from snapcraft import errors, extensions, linters, pack, providers, ua_manager, utils
Expand Down Expand Up @@ -462,9 +462,7 @@ def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None
emit.progress("Cleaned build provider", permanent=True)


# pylint: disable=too-many-branches


# pylint: disable-next=too-many-branches
def _run_in_provider(
project: Project, command_name: str, parsed_args: "argparse.Namespace"
) -> None:
Expand All @@ -482,14 +480,8 @@ def _run_in_provider(
if getattr(parsed_args, "output", None):
cmd.extend(["--output", parsed_args.output])

if emit.get_mode() == EmitterMode.VERBOSE:
cmd.append("--verbose")
elif emit.get_mode() == EmitterMode.QUIET:
cmd.append("--quiet")
elif emit.get_mode() == EmitterMode.DEBUG:
cmd.append("--verbosity=debug")
elif emit.get_mode() == EmitterMode.TRACE:
cmd.append("--verbosity=trace")
mode = emit.get_mode().name.lower()
cmd.append(f"--verbosity={mode}")

if parsed_args.debug:
cmd.append("--debug")
Expand All @@ -502,8 +494,7 @@ def _run_in_provider(
cmd.append("--enable-manifest")
build_information = getattr(parsed_args, "manifest_build_information", None)
if build_information:
cmd.append("--manifest-build-information")
cmd.append(build_information)
cmd.extend(["--manifest-build-information", build_information])

cmd.append("--build-for")
cmd.append(project.get_build_for())
Expand Down Expand Up @@ -543,9 +534,6 @@ def _run_in_provider(
capture_logs_from_instance(instance)


# pylint: enable=too-many-branches


def _set_global_environment(info: ProjectInfo) -> None:
"""Set global environment variables."""
info.global_environment.update(
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/conftest.py
Expand Up @@ -15,15 +15,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import base64
import contextlib
import textwrap
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from unittest.mock import Mock

import pytest
import yaml
from craft_providers import Executor
from pymacaroons import Caveat, Macaroon

from snapcraft.extensions import extension, register, unregister
from snapcraft.providers import Provider


@pytest.fixture
Expand Down Expand Up @@ -304,3 +308,55 @@ def legacy_config_path(
config_file.write_text(legacy_config_credentials)

return config_file


@pytest.fixture
def mock_instance():
"""Provide a mock instance (Executor)."""
yield Mock(spec=Executor)


@pytest.fixture(autouse=True)
def fake_provider(mock_instance):
"""Fixture to provide a minimal fake provider."""

class FakeProvider(Provider):
"""Fake provider."""

def clean_project_environments(
self,
*,
project_name: str,
project_path: Path,
build_on: str,
build_for: str,
):
pass

@classmethod
def ensure_provider_is_available(cls) -> None:
pass

@classmethod
def is_provider_available(cls) -> bool:
return True

def create_environment(self, *, instance_name: str):
yield mock_instance

@contextlib.contextmanager
def launched_environment(
self,
*,
project_name: str,
project_path: Path,
base: str,
bind_ssh: bool,
build_on: str,
build_for: str,
http_proxy: Optional[str] = None,
https_proxy: Optional[str] = None,
):
yield mock_instance

return FakeProvider()
147 changes: 127 additions & 20 deletions tests/unit/parts/test_lifecycle.py
Expand Up @@ -18,9 +18,10 @@
import textwrap
from datetime import datetime
from pathlib import Path
from unittest.mock import ANY, PropertyMock, call, patch
from unittest.mock import ANY, Mock, PropertyMock, call

import pytest
from craft_cli import EmitterMode, emit
from craft_parts import Action, Step, callbacks

from snapcraft import errors
Expand Down Expand Up @@ -62,10 +63,11 @@ def project_vars(mocker):
)


@pytest.fixture
def mock_provider():
with patch("snapcraft.providers.Provider", autospec=True) as _mock_provider:
yield _mock_provider
@pytest.fixture()
def mock_provider(mocker, mock_instance, fake_provider):
_mock_provider = Mock(wraps=fake_provider)
mocker.patch("snapcraft.providers.get_provider", return_value=_mock_provider)
yield _mock_provider


def test_config_not_found(new_dir):
Expand Down Expand Up @@ -1100,29 +1102,128 @@ def test_lifecycle_run_permission_denied(new_dir):
)


@pytest.mark.parametrize("http_proxy", (None, "1.2.3.4"))
@pytest.mark.parametrize("https_proxy", (None, "1.2.3.4"))
def test_lifecycle_run_in_provider_http_https_proxy(
http_proxy, https_proxy, mock_provider, mocker, snapcraft_yaml
def test_lifecycle_run_in_provider_default(
mock_instance, mock_provider, mocker, snapcraft_yaml
):
"""Verify _run_in_provider passes http[s] proxy to the instance."""
"""Verify default calls made in `run_in_provider()`"""
mock_capture_logs_from_instance = mocker.patch(
"snapcraft.parts.lifecycle.capture_logs_from_instance"
)

expected_command = [
"snapcraft",
"test",
"--verbosity=quiet",
"--build-for",
get_host_architecture(),
]

project = Project.unmarshal(snapcraft_yaml(base="core22"))
mocker.patch("snapcraft.providers.get_provider", return_value=mock_provider)
parts_lifecycle._run_in_provider(
project=project,
command_name="test",
parsed_args=argparse.Namespace(
use_lxd=False,
debug=False,
bind_ssh=False,
http_proxy=None,
https_proxy=None,
),
)

mock_provider.launched_environment.assert_called_with(
project_name="mytest",
project_path=ANY,
base="core22",
bind_ssh=False,
build_on=get_host_architecture(),
build_for=get_host_architecture(),
http_proxy=None,
https_proxy=None,
)

mock_provider.ensure_provider_is_available.assert_called_once()
mock_instance.execute_run.assert_called_once_with(
expected_command, check=True, cwd=Path("/root/project")
)
mock_capture_logs_from_instance.assert_called_once()


@pytest.mark.parametrize(
"emit_mode,verbosity",
[
(EmitterMode.VERBOSE, "--verbosity=verbose"),
(EmitterMode.QUIET, "--verbosity=quiet"),
(EmitterMode.BRIEF, "--verbosity=brief"),
(EmitterMode.DEBUG, "--verbosity=debug"),
(EmitterMode.TRACE, "--verbosity=trace"),
],
)
def test_lifecycle_run_in_provider_all_options(
mock_instance,
mock_provider,
mocker,
snapcraft_yaml,
emit_mode,
verbosity,
):
"""Verify all project options are parsed in `run_in_provider()`."""
mock_capture_logs_from_instance = mocker.patch(
"snapcraft.parts.lifecycle.capture_logs_from_instance"
)
mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-1")
mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-2")

# build the expected command to be executed in the provider
parts = ["test-part-1", "test-part-2"]
output = "test-output"
manifest_build_information = "test-build-info"
ua_token = "test-ua-token"
http_proxy = "1.2.3.4"
https_proxy = "1.2.3.4"
expected_command = (
["snapcraft", "test"]
+ parts
+ [
"--output",
output,
verbosity,
"--debug",
"--shell",
"--shell-after",
"--enable-manifest",
"--manifest-build-information",
manifest_build_information,
"--build-for",
"test-arch-1",
"--ua-token",
ua_token,
"--enable-experimental-ua-services",
]
)

# set emitter mode
emit.set_mode(emit_mode)

project = Project.unmarshal(snapcraft_yaml(base="core22"))
parts_lifecycle._run_in_provider(
project=project,
command_name="test",
parsed_args=argparse.Namespace(
parts=[],
parts=parts,
output=output,
destructive_mode=False,
use_lxd=False,
provider=None,
enable_manifest=False,
manifest_image_information=None,
bind_ssh=False,
ua_token=None,
enable_manifest=True,
manifest_build_information=manifest_build_information,
bind_ssh=True,
ua_token=ua_token,
enable_experimental_ua_services=True,
build_for=None,
debug=False,
debug=True,
shell=True,
shell_after=True,
http_proxy=http_proxy,
https_proxy=https_proxy,
),
Expand All @@ -1132,13 +1233,19 @@ def test_lifecycle_run_in_provider_http_https_proxy(
project_name="mytest",
project_path=ANY,
base="core22",
bind_ssh=False,
build_on=get_host_architecture(),
build_for=get_host_architecture(),
bind_ssh=True,
build_on="test-arch-2",
build_for="test-arch-1",
http_proxy=http_proxy,
https_proxy=https_proxy,
)

mock_provider.ensure_provider_is_available.assert_called_once()
mock_instance.execute_run.assert_called_once_with(
expected_command, check=True, cwd=Path("/root/project")
)
mock_capture_logs_from_instance.assert_called_once()


@pytest.fixture
def minimal_yaml_data():
Expand Down

0 comments on commit f0173be

Please sign in to comment.