mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-08 18:39:33 +00:00
Send progress updates during image pull for install/update (#6102)
* Send progress updates during image pull for install/update * Add extra to tests about job APIs * Sent out of date progress to sentry and combine done event * Pulling container image layer
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock, call, patch
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, PropertyMock, call, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from docker.errors import DockerException, NotFound
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
from requests import RequestException
|
||||
|
||||
from supervisor.addons.manager import Addon
|
||||
from supervisor.const import BusEvent, CpuArch
|
||||
from supervisor.const import BusEvent, CoreState, CpuArch
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.const import ContainerState
|
||||
from supervisor.docker.interface import DockerInterface
|
||||
@@ -25,6 +25,10 @@ from supervisor.exceptions import (
|
||||
DockerNotFound,
|
||||
DockerRequestError,
|
||||
)
|
||||
from supervisor.homeassistant.const import WSEvent
|
||||
from supervisor.jobs import JobSchedulerOptions, SupervisorJob
|
||||
|
||||
from tests.common import load_json_fixture
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -346,58 +350,256 @@ async def test_install_fires_progress_events(
|
||||
await asyncio.sleep(1)
|
||||
assert events == [
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Pulling from home-assistant/odroid-n2-homeassistant",
|
||||
id="2025.7.2",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Already exists",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="6e771e15690e",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Pulling fs layer",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="1578b14a573c",
|
||||
),
|
||||
PullLogEntry(
|
||||
status="Waiting", progress_detail=PullProgressDetail(), id="2488d0e401e1"
|
||||
job_id=ANY,
|
||||
status="Waiting",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="2488d0e401e1",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Downloading",
|
||||
progress_detail=PullProgressDetail(current=1378, total=1486),
|
||||
progress="[==============================================> ] 1.378kB/1.486kB",
|
||||
id="1578b14a573c",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Download complete",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="1578b14a573c",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Extracting",
|
||||
progress_detail=PullProgressDetail(current=1486, total=1486),
|
||||
progress="[==================================================>] 1.486kB/1.486kB",
|
||||
id="1578b14a573c",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Pull complete",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="1578b14a573c",
|
||||
),
|
||||
PullLogEntry(
|
||||
job_id=ANY,
|
||||
status="Verifying Checksum",
|
||||
progress_detail=PullProgressDetail(),
|
||||
id="6a1e931d8f88",
|
||||
),
|
||||
PullLogEntry(
|
||||
status="Digest: sha256:490080d7da0f385928022927990e04f604615f7b8c622ef3e58253d0f089881d"
|
||||
job_id=ANY,
|
||||
status="Digest: sha256:490080d7da0f385928022927990e04f604615f7b8c622ef3e58253d0f089881d",
|
||||
),
|
||||
PullLogEntry(
|
||||
status="Status: Downloaded newer image for ghcr.io/home-assistant/odroid-n2-homeassistant:2025.7.2"
|
||||
job_id=ANY,
|
||||
status="Status: Downloaded newer image for ghcr.io/home-assistant/odroid-n2-homeassistant:2025.7.2",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def test_install_sends_progress_to_home_assistant(
|
||||
coresys: CoreSys, test_docker_interface: DockerInterface, ha_ws_client: AsyncMock
|
||||
):
|
||||
"""Test progress events are sent as job updates to Home Assistant."""
|
||||
coresys.core.set_state(CoreState.RUNNING)
|
||||
coresys.docker.docker.api.pull.return_value = load_json_fixture(
|
||||
"docker_pull_image_log.json"
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.supervisor), "arch", PropertyMock(return_value="i386")
|
||||
),
|
||||
):
|
||||
# Schedule job so we can listen for the end. Then we can assert against the WS mock
|
||||
event = asyncio.Event()
|
||||
job, install_task = coresys.jobs.schedule_job(
|
||||
test_docker_interface.install,
|
||||
JobSchedulerOptions(),
|
||||
AwesomeVersion("1.2.3"),
|
||||
"test",
|
||||
)
|
||||
|
||||
async def listen_for_job_end(reference: SupervisorJob):
|
||||
if reference.uuid != job.uuid:
|
||||
return
|
||||
event.set()
|
||||
|
||||
coresys.bus.register_event(BusEvent.SUPERVISOR_JOB_END, listen_for_job_end)
|
||||
await install_task
|
||||
await event.wait()
|
||||
|
||||
events = [
|
||||
evt.args[0]["data"]["data"]
|
||||
for evt in ha_ws_client.async_send_command.call_args_list
|
||||
if "data" in evt.args[0] and evt.args[0]["data"]["event"] == WSEvent.JOB
|
||||
]
|
||||
assert events[0]["name"] == "docker_interface_install"
|
||||
assert events[0]["uuid"] == job.uuid
|
||||
assert events[0]["done"] is None
|
||||
assert events[1]["name"] == "docker_interface_install"
|
||||
assert events[1]["uuid"] == job.uuid
|
||||
assert events[1]["done"] is False
|
||||
assert events[-1]["name"] == "docker_interface_install"
|
||||
assert events[-1]["uuid"] == job.uuid
|
||||
assert events[-1]["done"] is True
|
||||
|
||||
def make_sub_log(layer_id: str):
|
||||
return [
|
||||
{
|
||||
"stage": evt["stage"],
|
||||
"progress": evt["progress"],
|
||||
"done": evt["done"],
|
||||
"extra": evt["extra"],
|
||||
}
|
||||
for evt in events
|
||||
if evt["name"] == "Pulling container image layer"
|
||||
and evt["reference"] == layer_id
|
||||
and evt["parent_id"] == job.uuid
|
||||
]
|
||||
|
||||
layer_1_log = make_sub_log("1e214cd6d7d0")
|
||||
layer_2_log = make_sub_log("1a38e1d5e18d")
|
||||
assert len(layer_1_log) == 20
|
||||
assert len(layer_2_log) == 19
|
||||
assert len(events) == 42
|
||||
assert layer_1_log == [
|
||||
{"stage": "Pulling fs layer", "progress": 0, "done": False, "extra": None},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 0.1,
|
||||
"done": False,
|
||||
"extra": {"current": 539462, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 0.6,
|
||||
"done": False,
|
||||
"extra": {"current": 4864838, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 0.9,
|
||||
"done": False,
|
||||
"extra": {"current": 7552896, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 1.2,
|
||||
"done": False,
|
||||
"extra": {"current": 10252544, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 2.9,
|
||||
"done": False,
|
||||
"extra": {"current": 25369792, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 11.9,
|
||||
"done": False,
|
||||
"extra": {"current": 103619904, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 26.1,
|
||||
"done": False,
|
||||
"extra": {"current": 227726144, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Downloading",
|
||||
"progress": 49.6,
|
||||
"done": False,
|
||||
"extra": {"current": 433170048, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Verifying Checksum",
|
||||
"progress": 50,
|
||||
"done": False,
|
||||
"extra": {"current": 433170048, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Download complete",
|
||||
"progress": 50,
|
||||
"done": False,
|
||||
"extra": {"current": 433170048, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 50.1,
|
||||
"done": False,
|
||||
"extra": {"current": 557056, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 60.3,
|
||||
"done": False,
|
||||
"extra": {"current": 89686016, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 70.0,
|
||||
"done": False,
|
||||
"extra": {"current": 174358528, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 80.0,
|
||||
"done": False,
|
||||
"extra": {"current": 261816320, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 88.4,
|
||||
"done": False,
|
||||
"extra": {"current": 334790656, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 94.0,
|
||||
"done": False,
|
||||
"extra": {"current": 383811584, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 99.9,
|
||||
"done": False,
|
||||
"extra": {"current": 435617792, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Extracting",
|
||||
"progress": 100.0,
|
||||
"done": False,
|
||||
"extra": {"current": 436480882, "total": 436480882},
|
||||
},
|
||||
{
|
||||
"stage": "Pull complete",
|
||||
"progress": 100.0,
|
||||
"done": True,
|
||||
"extra": {"current": 436480882, "total": 436480882},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error_log", "exc_type", "exc_msg"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user