Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: mock and test provider calls from lifecycle.py #3928

Merged
merged 3 commits into from Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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