mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-24 12:29:08 +00:00
Migrate images from dockerpy to aiodocker (#6252)
* Migrate images from dockerpy to aiodocker * Add missing coverage and fix bug in repair * Bind libraries to different files and refactor images.pull * Use the same socket again Try using the same socket again. * Fix pytest --------- Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""Test Home Assistant core."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import ANY, MagicMock, Mock, PropertyMock, call, patch
|
||||
|
||||
import aiodocker
|
||||
from awesomeversion import AwesomeVersion
|
||||
from docker.errors import APIError, DockerException, ImageNotFound, NotFound
|
||||
from docker.errors import APIError, DockerException, NotFound
|
||||
import pytest
|
||||
from requests import RequestException
|
||||
from time_machine import travel
|
||||
|
||||
from supervisor.const import CpuArch
|
||||
@@ -23,8 +26,12 @@ from supervisor.exceptions import (
|
||||
from supervisor.homeassistant.api import APIState
|
||||
from supervisor.homeassistant.core import HomeAssistantCore
|
||||
from supervisor.homeassistant.module import HomeAssistant
|
||||
from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.data import Issue
|
||||
from supervisor.updater import Updater
|
||||
|
||||
from tests.common import AsyncIterator
|
||||
|
||||
|
||||
async def test_update_fails_if_out_of_date(coresys: CoreSys):
|
||||
"""Test update of Home Assistant fails when supervisor or plugin is out of date."""
|
||||
@@ -52,11 +59,23 @@ async def test_update_fails_if_out_of_date(coresys: CoreSys):
|
||||
await coresys.homeassistant.core.update()
|
||||
|
||||
|
||||
async def test_install_landingpage_docker_error(
|
||||
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
|
||||
@pytest.mark.parametrize(
|
||||
"err",
|
||||
[
|
||||
aiodocker.DockerError(HTTPStatus.TOO_MANY_REQUESTS, {"message": "ratelimit"}),
|
||||
APIError("ratelimit", MagicMock(status_code=HTTPStatus.TOO_MANY_REQUESTS)),
|
||||
],
|
||||
)
|
||||
async def test_install_landingpage_docker_ratelimit_error(
|
||||
coresys: CoreSys,
|
||||
capture_exception: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
err: Exception,
|
||||
):
|
||||
"""Test install landing page fails due to docker error."""
|
||||
"""Test install landing page fails due to docker ratelimit error."""
|
||||
coresys.security.force = True
|
||||
coresys.docker.images.pull.side_effect = [err, AsyncIterator([{}])]
|
||||
|
||||
with (
|
||||
patch.object(DockerHomeAssistant, "attach", side_effect=DockerError),
|
||||
patch.object(
|
||||
@@ -69,19 +88,35 @@ async def test_install_landingpage_docker_error(
|
||||
),
|
||||
patch("supervisor.homeassistant.core.asyncio.sleep") as sleep,
|
||||
):
|
||||
coresys.docker.images.get.side_effect = [APIError("fail"), MagicMock()]
|
||||
await coresys.homeassistant.core.install_landingpage()
|
||||
sleep.assert_awaited_once_with(30)
|
||||
|
||||
assert "Failed to install landingpage, retrying after 30sec" in caplog.text
|
||||
capture_exception.assert_not_called()
|
||||
assert (
|
||||
Issue(IssueType.DOCKER_RATELIMIT, ContextType.SYSTEM)
|
||||
in coresys.resolution.issues
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"err",
|
||||
[
|
||||
aiodocker.DockerError(HTTPStatus.INTERNAL_SERVER_ERROR, {"message": "fail"}),
|
||||
APIError("fail"),
|
||||
DockerException(),
|
||||
RequestException(),
|
||||
OSError(),
|
||||
],
|
||||
)
|
||||
async def test_install_landingpage_other_error(
|
||||
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
|
||||
coresys: CoreSys,
|
||||
capture_exception: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
err: Exception,
|
||||
):
|
||||
"""Test install landing page fails due to other error."""
|
||||
coresys.docker.images.get.side_effect = [(err := OSError()), MagicMock()]
|
||||
coresys.docker.images.inspect.side_effect = [err, MagicMock()]
|
||||
|
||||
with (
|
||||
patch.object(DockerHomeAssistant, "attach", side_effect=DockerError),
|
||||
@@ -102,11 +137,23 @@ async def test_install_landingpage_other_error(
|
||||
capture_exception.assert_called_once_with(err)
|
||||
|
||||
|
||||
async def test_install_docker_error(
|
||||
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
|
||||
@pytest.mark.parametrize(
|
||||
"err",
|
||||
[
|
||||
aiodocker.DockerError(HTTPStatus.TOO_MANY_REQUESTS, {"message": "ratelimit"}),
|
||||
APIError("ratelimit", MagicMock(status_code=HTTPStatus.TOO_MANY_REQUESTS)),
|
||||
],
|
||||
)
|
||||
async def test_install_docker_ratelimit_error(
|
||||
coresys: CoreSys,
|
||||
capture_exception: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
err: Exception,
|
||||
):
|
||||
"""Test install fails due to docker error."""
|
||||
"""Test install fails due to docker ratelimit error."""
|
||||
coresys.security.force = True
|
||||
coresys.docker.images.pull.side_effect = [err, AsyncIterator([{}])]
|
||||
|
||||
with (
|
||||
patch.object(HomeAssistantCore, "start"),
|
||||
patch.object(DockerHomeAssistant, "cleanup"),
|
||||
@@ -123,19 +170,35 @@ async def test_install_docker_error(
|
||||
),
|
||||
patch("supervisor.homeassistant.core.asyncio.sleep") as sleep,
|
||||
):
|
||||
coresys.docker.images.get.side_effect = [APIError("fail"), MagicMock()]
|
||||
await coresys.homeassistant.core.install()
|
||||
sleep.assert_awaited_once_with(30)
|
||||
|
||||
assert "Error on Home Assistant installation. Retrying in 30sec" in caplog.text
|
||||
capture_exception.assert_not_called()
|
||||
assert (
|
||||
Issue(IssueType.DOCKER_RATELIMIT, ContextType.SYSTEM)
|
||||
in coresys.resolution.issues
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"err",
|
||||
[
|
||||
aiodocker.DockerError(HTTPStatus.INTERNAL_SERVER_ERROR, {"message": "fail"}),
|
||||
APIError("fail"),
|
||||
DockerException(),
|
||||
RequestException(),
|
||||
OSError(),
|
||||
],
|
||||
)
|
||||
async def test_install_other_error(
|
||||
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
|
||||
coresys: CoreSys,
|
||||
capture_exception: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
err: Exception,
|
||||
):
|
||||
"""Test install fails due to other error."""
|
||||
coresys.docker.images.get.side_effect = [(err := OSError()), MagicMock()]
|
||||
coresys.docker.images.inspect.side_effect = [err, MagicMock()]
|
||||
|
||||
with (
|
||||
patch.object(HomeAssistantCore, "start"),
|
||||
@@ -161,21 +224,29 @@ async def test_install_other_error(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"container_exists,image_exists", [(False, True), (True, False), (True, True)]
|
||||
("container_exc", "image_exc", "remove_calls"),
|
||||
[
|
||||
(NotFound("missing"), None, []),
|
||||
(
|
||||
None,
|
||||
aiodocker.DockerError(404, {"message": "missing"}),
|
||||
[call(force=True, v=True)],
|
||||
),
|
||||
(None, None, [call(force=True, v=True)]),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("path_extern")
|
||||
async def test_start(
|
||||
coresys: CoreSys, container_exists: bool, image_exists: bool, path_extern
|
||||
coresys: CoreSys,
|
||||
container_exc: DockerException | None,
|
||||
image_exc: aiodocker.DockerError | None,
|
||||
remove_calls: list[call],
|
||||
):
|
||||
"""Test starting Home Assistant."""
|
||||
if image_exists:
|
||||
coresys.docker.images.get.return_value.id = "123"
|
||||
else:
|
||||
coresys.docker.images.get.side_effect = ImageNotFound("missing")
|
||||
|
||||
if container_exists:
|
||||
coresys.docker.containers.get.return_value.image.id = "123"
|
||||
else:
|
||||
coresys.docker.containers.get.side_effect = NotFound("missing")
|
||||
coresys.docker.images.inspect.return_value = {"Id": "123"}
|
||||
coresys.docker.images.inspect.side_effect = image_exc
|
||||
coresys.docker.containers.get.return_value.id = "123"
|
||||
coresys.docker.containers.get.side_effect = container_exc
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
@@ -198,18 +269,14 @@ async def test_start(
|
||||
assert run.call_args.kwargs["hostname"] == "homeassistant"
|
||||
|
||||
coresys.docker.containers.get.return_value.stop.assert_not_called()
|
||||
if container_exists:
|
||||
coresys.docker.containers.get.return_value.remove.assert_called_once_with(
|
||||
force=True,
|
||||
v=True,
|
||||
)
|
||||
else:
|
||||
coresys.docker.containers.get.return_value.remove.assert_not_called()
|
||||
assert (
|
||||
coresys.docker.containers.get.return_value.remove.call_args_list == remove_calls
|
||||
)
|
||||
|
||||
|
||||
async def test_start_existing_container(coresys: CoreSys, path_extern):
|
||||
"""Test starting Home Assistant when container exists and is viable."""
|
||||
coresys.docker.images.get.return_value.id = "123"
|
||||
coresys.docker.images.inspect.return_value = {"Id": "123"}
|
||||
coresys.docker.containers.get.return_value.image.id = "123"
|
||||
coresys.docker.containers.get.return_value.status = "exited"
|
||||
|
||||
@@ -394,24 +461,32 @@ async def test_core_loads_wrong_image_for_machine(
|
||||
"""Test core is loaded with wrong image for machine."""
|
||||
coresys.homeassistant.set_image("ghcr.io/home-assistant/odroid-n2-homeassistant")
|
||||
coresys.homeassistant.version = AwesomeVersion("2024.4.0")
|
||||
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||
|
||||
await coresys.homeassistant.core.load()
|
||||
with patch.object(
|
||||
DockerAPI,
|
||||
"pull_image",
|
||||
return_value={
|
||||
"Id": "abc123",
|
||||
"Config": {"Labels": {"io.hass.version": "2024.4.0"}},
|
||||
},
|
||||
) as pull_image:
|
||||
container.attrs |= pull_image.return_value
|
||||
await coresys.homeassistant.core.load()
|
||||
pull_image.assert_called_once_with(
|
||||
ANY,
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant",
|
||||
"2024.4.0",
|
||||
platform="linux/amd64",
|
||||
)
|
||||
|
||||
container.remove.assert_called_once_with(force=True, v=True)
|
||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
|
||||
"force": True,
|
||||
}
|
||||
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:2024.4.0",
|
||||
"force": True,
|
||||
}
|
||||
coresys.docker.pull_image.assert_called_once_with(
|
||||
ANY,
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant",
|
||||
"2024.4.0",
|
||||
platform="linux/amd64",
|
||||
assert coresys.docker.images.delete.call_args_list[0] == call(
|
||||
"ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
|
||||
force=True,
|
||||
)
|
||||
assert coresys.docker.images.delete.call_args_list[1] == call(
|
||||
"ghcr.io/home-assistant/odroid-n2-homeassistant:2024.4.0",
|
||||
force=True,
|
||||
)
|
||||
assert (
|
||||
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||
@@ -428,8 +503,8 @@ async def test_core_load_allows_image_override(coresys: CoreSys, container: Magi
|
||||
await coresys.homeassistant.core.load()
|
||||
|
||||
container.remove.assert_not_called()
|
||||
coresys.docker.images.remove.assert_not_called()
|
||||
coresys.docker.images.get.assert_not_called()
|
||||
coresys.docker.images.delete.assert_not_called()
|
||||
coresys.docker.images.inspect.assert_not_called()
|
||||
assert (
|
||||
coresys.homeassistant.image == "ghcr.io/home-assistant/odroid-n2-homeassistant"
|
||||
)
|
||||
@@ -440,27 +515,36 @@ async def test_core_loads_wrong_image_for_architecture(
|
||||
):
|
||||
"""Test core is loaded with wrong image for architecture."""
|
||||
coresys.homeassistant.version = AwesomeVersion("2024.4.0")
|
||||
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||
coresys.docker.images.get("ghcr.io/home-assistant/qemux86-64-homeassistant").attrs[
|
||||
"Architecture"
|
||||
] = "arm64"
|
||||
coresys.docker.images.inspect.return_value = img_data = (
|
||||
coresys.docker.images.inspect.return_value
|
||||
| {
|
||||
"Architecture": "arm64",
|
||||
"Config": {"Labels": {"io.hass.version": "2024.4.0"}},
|
||||
}
|
||||
)
|
||||
container.attrs |= img_data
|
||||
|
||||
await coresys.homeassistant.core.load()
|
||||
with patch.object(
|
||||
DockerAPI,
|
||||
"pull_image",
|
||||
return_value=img_data | {"Architecture": "amd64"},
|
||||
) as pull_image:
|
||||
await coresys.homeassistant.core.load()
|
||||
pull_image.assert_called_once_with(
|
||||
ANY,
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant",
|
||||
"2024.4.0",
|
||||
platform="linux/amd64",
|
||||
)
|
||||
|
||||
container.remove.assert_called_once_with(force=True, v=True)
|
||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
|
||||
"force": True,
|
||||
}
|
||||
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:2024.4.0",
|
||||
"force": True,
|
||||
}
|
||||
coresys.docker.pull_image.assert_called_once_with(
|
||||
ANY,
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant",
|
||||
"2024.4.0",
|
||||
platform="linux/amd64",
|
||||
assert coresys.docker.images.delete.call_args_list[0] == call(
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
|
||||
force=True,
|
||||
)
|
||||
assert coresys.docker.images.delete.call_args_list[1] == call(
|
||||
"ghcr.io/home-assistant/qemux86-64-homeassistant:2024.4.0",
|
||||
force=True,
|
||||
)
|
||||
assert (
|
||||
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||
|
||||
Reference in New Issue
Block a user