diff --git a/snapcraft/internal/build_providers/_multipass/_multipass.py b/snapcraft/internal/build_providers/_multipass/_multipass.py
index 8492fcc0569..88d23049133 100644
--- a/snapcraft/internal/build_providers/_multipass/_multipass.py
+++ b/snapcraft/internal/build_providers/_multipass/_multipass.py
@@ -15,8 +15,6 @@
# along with this program. If not, see .
import shlex
-from contextlib import contextmanager
-from typing import Generator
from .. import errors
from .._base_provider import BaseProvider
@@ -32,38 +30,42 @@ def __init__(self, *, project, echoer) -> None:
super().__init__(project=project, echoer=echoer,
executor=self._multipass_cmd.execute)
- @contextmanager
- def new_instance(self, keep=True) -> Generator:
- """Create the multipass instance."""
- try:
- self.launch_instance(self._multipass_cmd.launch, image='16.04')
- self.setup_snapcraft()
- yield self
- finally:
- try:
- self._try_stop_and_delete(keep=keep)
- except errors.ProviderStopError as stop_error:
- self.echoer.warning('Could not stop {!r}: {}.'.format(
- self.instance_name, stop_error))
- except errors.ProviderDeleteError as delete_error:
- self.echoer.warning('Could not stop {!r}: {}.'.format(
- self.instance_name, delete_error))
-
- def _try_stop_and_delete(self, *, keep: bool) -> None:
+ def __enter__(self):
+ self.create()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.destroy()
+
+ def create(self):
+ """Create the multipass instance and setup the build environment."""
+ self.launch_instance(self._multipass_cmd.launch, image='16.04')
+ self.setup_snapcraft()
+
+ def destroy(self):
+ """Destroy the instance, trying to stop it first."""
try:
instance_info = self._get_instance_info()
except errors.ProviderInfoError as info_error:
- # An error here means this instance may not exist
+ self.echoer.warning(
+ 'Failed to obtain the status of {!r} when trying to '
+ 'delete: {}'.format(self.instance_name, info_error))
return
- if instance_info.is_stopped():
- return
- self._multipass_cmd.stop(instance_name=self.instance_name)
- if not keep:
+ try:
+ if not instance_info.is_stopped():
+ self._multipass_cmd.stop(instance_name=self.instance_name)
self._multipass_cmd.delete(instance_name=self.instance_name)
+ except errors.ProviderStopError as stop_error:
+ self.echoer.warning('Could not stop {!r}: {}.'.format(
+ self.instance_name, stop_error))
+ except errors.ProviderDeleteError as stop_error:
+ self.echoer.warning('Could not stop {!r}: {}.'.format(
+ self.instance_name, stop_error))
def provision_project(self, tarball: str) -> None:
"""Provision the multipass instance with the project to work with."""
+ # TODO add instance check.
# Step 0, sanitize the input
tarball = shlex.quote(tarball)
@@ -81,6 +83,7 @@ def provision_project(self, tarball: str) -> None:
instance_name=self.instance_name)
def build_project(self) -> None:
+ # TODO add instance check.
# Use the full path as /snap/bin is not in PATH.
snapcraft_cmd = 'cd {}; /snap/bin/snapcraft snap --output {}'.format(
self.project_dir, self.snap_filename)
@@ -88,6 +91,7 @@ def build_project(self) -> None:
instance_name=self.instance_name)
def retrieve_snap(self) -> str:
+ # TODO add instance check.
source = '{}:{}/{}'.format(self.instance_name,
self.project_dir,
self.snap_filename)
diff --git a/snapcraft/internal/lifecycle/_containers.py b/snapcraft/internal/lifecycle/_containers.py
index fe662f9754f..8c45a52ba97 100644
--- a/snapcraft/internal/lifecycle/_containers.py
+++ b/snapcraft/internal/lifecycle/_containers.py
@@ -54,8 +54,7 @@ def cleanbuild(*, project, echoer, build_environment, remote='') -> None:
return
build_provider_class = build_providers.get_provider_for('multipass')
- build_provider = build_provider_class(project=project, echoer=echoer)
- with build_provider.new_instance(keep=False) as instance:
+ with build_provider_class(project=project, echoer=echoer) as instance:
instance.provision_project(tar_filename)
instance.build_project()
instance.retrieve_snap()
diff --git a/tests/unit/build_providers/__init__.py b/tests/unit/build_providers/__init__.py
new file mode 100644
index 00000000000..d5b1619bdd5
--- /dev/null
+++ b/tests/unit/build_providers/__init__.py
@@ -0,0 +1,39 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2018 Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from unittest import mock
+
+from snapcraft.project import Project, ProjectInfo
+
+from tests import unit
+
+
+class BaseProviderBaseTest(unit.TestCase):
+
+ def setUp(self):
+ super().setUp()
+
+ self.instance_name = 'ridicoulus-hours'
+ patcher = mock.patch('petname.Generate',
+ return_value=self.instance_name)
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ self.project = Project()
+ self.project.info = ProjectInfo(dict(name='project-name'))
+
+ self.echoer_mock = mock.Mock()
+ self.executor_mock = mock.Mock()
diff --git a/tests/unit/build_providers/multipass/__init__.py b/tests/unit/build_providers/multipass/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/unit/build_providers/multipass/test_multipass.py b/tests/unit/build_providers/multipass/test_multipass.py
new file mode 100644
index 00000000000..2bcac86f373
--- /dev/null
+++ b/tests/unit/build_providers/multipass/test_multipass.py
@@ -0,0 +1,191 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2018 Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from textwrap import dedent
+from unittest import mock
+
+from tests.unit.build_providers import BaseProviderBaseTest
+from snapcraft.internal.build_providers import errors
+from snapcraft.internal.build_providers._multipass import (
+ Multipass, MultipassCommand)
+
+
+_DEFAULT_INSTANCE_INFO = dedent("""\
+ {
+ "errors": [
+ ],
+ "info": {
+ "ridicoulus-hours": {
+ "disks": {
+ "sda1": {
+ "total": "5136297984",
+ "used": "1076072448"
+ }
+ },
+ "image_hash": "bc990872f070249450fc187d668bf65e2f87ffa3d7cf6e2934cb84b62af368e1",
+ "image_release": "16.04 LTS",
+ "ipv4": [
+ "10.74.70.141"
+ ],
+ "load": [
+ 0,
+ 0,
+ 0
+ ],
+ "memory": {
+ "total": 1040318464,
+ "used": 42754048
+ },
+ "mounts": {
+ },
+ "release": "Ubuntu 16.04.4 LTS",
+ "state": "RUNNING"
+ }
+ }
+ }
+ """) # noqa: E501
+
+
+class MultipassTest(BaseProviderBaseTest):
+
+ def setUp(self):
+ super().setUp()
+
+ patcher = mock.patch(
+ 'snapcraft.internal.build_providers._multipass.'
+ '_multipass.MultipassCommand',
+ spec=MultipassCommand)
+ self.multipass_cmd_mock = patcher.start()
+ self.addCleanup(patcher.stop)
+
+ # default data returned for info
+ self.multipass_cmd_mock().info.return_value = \
+ _DEFAULT_INSTANCE_INFO.encode()
+
+ def test_instance_with_contextmanager(self):
+ with Multipass(project=self.project,
+ echoer=self.echoer_mock) as instance:
+ instance.provision_project('source.tar')
+ instance.build_project()
+ instance.retrieve_snap()
+
+ self.multipass_cmd_mock().launch.assert_called_once_with(
+ image='16.04', instance_name=self.instance_name)
+ self.multipass_cmd_mock().execute.assert_has_calls([
+ mock.call(
+ instance_name=self.instance_name,
+ command=['sudo', 'snap', 'install', 'snapcraft',
+ '--classic']),
+ mock.call(
+ instance_name=self.instance_name,
+ command=['mkdir', 'project-name']),
+ mock.call(
+ instance_name=self.instance_name,
+ command=['tar', '-xvf', 'source.tar', '-C', 'project-name']),
+ mock.call(
+ instance_name=self.instance_name,
+ command=['sh', '-c', 'cd project-name; /snap/bin/snapcraft '
+ 'snap --output project-name_{}.snap'.format(
+ self.project.deb_arch)]),
+ ])
+ self.multipass_cmd_mock().info.assert_called_once_with(
+ instance_name=self.instance_name, output_format='json')
+
+ self.multipass_cmd_mock().copy_files.assert_has_calls([
+ mock.call(destination='{}:source.tar'.format(self.instance_name),
+ source='source.tar'),
+ mock.call(destination='project-name_{}.snap'.format(
+ self.project.deb_arch),
+ source='{}:project-name/project-name_{}.snap'.format(
+ self.instance_name, self.project.deb_arch)),
+ ])
+ self.multipass_cmd_mock().stop.assert_called_once_with(
+ instance_name=self.instance_name)
+ self.multipass_cmd_mock().delete.assert_called_once_with(
+ instance_name=self.instance_name)
+
+ def test_provision_project(self):
+ multipass = Multipass(project=self.project, echoer=self.echoer_mock)
+
+ # In the real world, MultipassCommand would return an error when
+ # calling this on an instance that does not exist.
+ multipass.provision_project('source.tar')
+
+ self.multipass_cmd_mock().execute.assert_has_calls([
+ mock.call(
+ instance_name=self.instance_name,
+ command=['mkdir', 'project-name']),
+ mock.call(
+ instance_name=self.instance_name,
+ command=['tar', '-xvf', 'source.tar', '-C', 'project-name']),
+ ])
+ self.multipass_cmd_mock().copy_files.assert_called_once_with(
+ destination='{}:source.tar'.format(self.instance_name),
+ source='source.tar')
+
+ self.multipass_cmd_mock().launch.assert_not_called()
+ self.multipass_cmd_mock().info.assert_not_called()
+ self.multipass_cmd_mock().stop.assert_not_called()
+ self.multipass_cmd_mock().delete.assert_not_called()
+
+ def test_build_project(self):
+ multipass = Multipass(project=self.project, echoer=self.echoer_mock)
+
+ # In the real world, MultipassCommand would return an error when
+ # calling this on an instance that does not exist.
+ multipass.build_project()
+
+ self.multipass_cmd_mock().execute.assert_called_once_with(
+ instance_name=self.instance_name,
+ command=['sh', '-c', 'cd project-name; /snap/bin/snapcraft '
+ 'snap --output project-name_{}.snap'.format(
+ self.project.deb_arch)])
+
+ self.multipass_cmd_mock().copy_files.assert_not_called()
+ self.multipass_cmd_mock().launch.assert_not_called()
+ self.multipass_cmd_mock().info.assert_not_called()
+ self.multipass_cmd_mock().stop.assert_not_called()
+ self.multipass_cmd_mock().delete.assert_not_called()
+
+ def test_retrieve_snap(self):
+ multipass = Multipass(project=self.project, echoer=self.echoer_mock)
+
+ # In the real world, MultipassCommand would return an error when
+ # calling this on an instance that does not exist.
+ multipass.retrieve_snap()
+
+ self.multipass_cmd_mock().copy_files.assert_called_once_with(
+ destination='project-name_{}.snap'.format(self.project.deb_arch),
+ source='{}:project-name/project-name_{}.snap'.format(
+ self.instance_name, self.project.deb_arch))
+
+ self.multipass_cmd_mock().execute.assert_not_called()
+ self.multipass_cmd_mock().launch.assert_not_called()
+ self.multipass_cmd_mock().info.assert_not_called()
+ self.multipass_cmd_mock().stop.assert_not_called()
+ self.multipass_cmd_mock().delete.assert_not_called()
+
+ def test_instance_does_not_exist_on_destroy(self):
+ # An error is raised if the queried image does not exist
+ self.multipass_cmd_mock().info.side_effect = errors.ProviderInfoError(
+ provider_name=self.instance_name)
+
+ multipass = Multipass(project=self.project, echoer=self.echoer_mock)
+
+ multipass.destroy()
+
+ self.multipass_cmd_mock().stop.assert_not_called()
+ self.multipass_cmd_mock().delete.assert_not_called()
diff --git a/tests/unit/build_providers/test_base_provider.py b/tests/unit/build_providers/test_base_provider.py
index 217e9c0fc17..a1a52226dc4 100644
--- a/tests/unit/build_providers/test_base_provider.py
+++ b/tests/unit/build_providers/test_base_provider.py
@@ -18,27 +18,11 @@
from testtools.matchers import Equals
-from snapcraft.project import Project, ProjectInfo
+from . import BaseProviderBaseTest
from snapcraft.internal.build_providers._base_provider import BaseProvider
-from tests import unit
-class BaseProviderTest(unit.TestCase):
-
- def setUp(self):
- super().setUp()
-
- self.instance_name = 'ridicoulus-hours'
- patcher = mock.patch('petname.Generate',
- return_value=self.instance_name)
- patcher.start()
- self.addCleanup(patcher.stop)
-
- self.project = Project()
- self.project.info = ProjectInfo(dict(name='project-name'))
-
- self.echoer_mock = mock.Mock()
- self.executor_mock = mock.Mock()
+class BaseProviderTest(BaseProviderBaseTest):
def test_initialize(self):
base_provider = BaseProvider(project=self.project,