diff --git a/supervisor/docker/pull_progress.py b/supervisor/docker/pull_progress.py index 69e967ed8..58649d0af 100644 --- a/supervisor/docker/pull_progress.py +++ b/supervisor/docker/pull_progress.py @@ -237,9 +237,8 @@ class ImagePullProgress: def calculate_progress(self) -> float: """Calculate overall progress 0-100. - Uses count-based progress where each layer contributes equally. - Each layer's individual progress (0-100) is weighted by 1/total_layers. - This ensures progress never goes backwards when large layers appear late. + Uses count-based progress where each layer that needs pulling contributes + equally. Layers that already exist locally are excluded from the calculation. Returns 0 until we've seen the first "Downloading" event, since Docker reports "Already exists" and "Pulling fs layer" events before we know @@ -250,11 +249,18 @@ class ImagePullProgress: if not self._seen_downloading or not self.layers: return 0.0 + # Only count layers that need pulling (exclude already_exists) + layers_to_pull = [ + layer for layer in self.layers.values() if not layer.already_exists + ] + + if not layers_to_pull: + # All layers already exist, nothing to download + return 100.0 + # Each layer contributes equally: sum of layer progresses / total layers - total_progress = sum( - layer.calculate_progress() for layer in self.layers.values() - ) - return total_progress / len(self.layers) + total_progress = sum(layer.calculate_progress() for layer in layers_to_pull) + return total_progress / len(layers_to_pull) def get_stage(self) -> str | None: """Get current stage based on layer states.""" diff --git a/tests/api/test_homeassistant.py b/tests/api/test_homeassistant.py index 7f8731421..85b870401 100644 --- a/tests/api/test_homeassistant.py +++ b/tests/api/test_homeassistant.py @@ -305,8 +305,8 @@ async def test_api_progress_updates_home_assistant_update( and evt.args[0]["data"]["event"] == WSEvent.JOB and evt.args[0]["data"]["data"]["name"] == "home_assistant_core_update" ] - # Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each) - # Cached layers contribute immediately when downloading starts + # Count-based progress: 2 layers need pulling (each worth 50%) + # Layers that already exist are excluded from progress calculation assert events[:5] == [ { "stage": None, @@ -320,34 +320,34 @@ async def test_api_progress_updates_home_assistant_update( }, { "stage": None, - "progress": 50.0, + "progress": 9.2, "done": False, }, { "stage": None, - "progress": 54.6, + "progress": 25.6, "done": False, }, { "stage": None, - "progress": 62.8, + "progress": 35.4, "done": False, }, ] assert events[-5:] == [ { "stage": None, - "progress": 95.7, + "progress": 95.5, "done": False, }, { "stage": None, - "progress": 97.1, + "progress": 96.9, "done": False, }, { "stage": None, - "progress": 98.4, + "progress": 98.2, "done": False, }, { diff --git a/tests/api/test_store.py b/tests/api/test_store.py index 8b8203ab8..4e934f7b5 100644 --- a/tests/api/test_store.py +++ b/tests/api/test_store.py @@ -761,8 +761,8 @@ async def test_api_progress_updates_addon_install_update( and evt.args[0]["data"]["data"]["name"] == job_name and evt.args[0]["data"]["data"]["reference"] == addon_slug ] - # Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each) - # Cached layers contribute immediately when downloading starts + # Count-based progress: 2 layers need pulling (each worth 50%) + # Layers that already exist are excluded from progress calculation assert events[:4] == [ { "stage": None, @@ -771,34 +771,34 @@ async def test_api_progress_updates_addon_install_update( }, { "stage": None, - "progress": 50.0, + "progress": 9.2, "done": False, }, { "stage": None, - "progress": 54.6, + "progress": 25.6, "done": False, }, { "stage": None, - "progress": 62.8, + "progress": 35.4, "done": False, }, ] assert events[-5:] == [ { "stage": None, - "progress": 95.7, + "progress": 95.5, "done": False, }, { "stage": None, - "progress": 97.1, + "progress": 96.9, "done": False, }, { "stage": None, - "progress": 98.4, + "progress": 98.2, "done": False, }, { diff --git a/tests/api/test_supervisor.py b/tests/api/test_supervisor.py index 2c77f0815..9d8f1a418 100644 --- a/tests/api/test_supervisor.py +++ b/tests/api/test_supervisor.py @@ -359,8 +359,8 @@ async def test_api_progress_updates_supervisor_update( and evt.args[0]["data"]["event"] == WSEvent.JOB and evt.args[0]["data"]["data"]["name"] == "supervisor_update" ] - # Count-based progress: 4 layers (2 cached = 50%, 2 pulling = 25% each) - # Cached layers contribute immediately when downloading starts + # Count-based progress: 2 layers need pulling (each worth 50%) + # Layers that already exist are excluded from progress calculation assert events[:4] == [ { "stage": None, @@ -369,34 +369,34 @@ async def test_api_progress_updates_supervisor_update( }, { "stage": None, - "progress": 50.0, + "progress": 9.2, "done": False, }, { "stage": None, - "progress": 54.6, + "progress": 25.6, "done": False, }, { "stage": None, - "progress": 62.8, + "progress": 35.4, "done": False, }, ] assert events[-5:] == [ { "stage": None, - "progress": 95.7, + "progress": 95.5, "done": False, }, { "stage": None, - "progress": 97.1, + "progress": 96.9, "done": False, }, { "stage": None, - "progress": 98.4, + "progress": 98.2, "done": False, }, { diff --git a/tests/docker/test_pull_progress.py b/tests/docker/test_pull_progress.py index a4132b8c7..c4c04d9e1 100644 --- a/tests/docker/test_pull_progress.py +++ b/tests/docker/test_pull_progress.py @@ -591,11 +591,9 @@ class TestImagePullProgress: ) ) - # Count-based: 2 layers total, each = 50% - # cached: 100% (already exists) + # Only 1 layer needs pulling (cached layer excluded) # pulled: 35% (50% of 70% download weight) - # Total: (100 + 35) / 2 = 67.5% - assert progress.calculate_progress() == pytest.approx(67.5) + assert progress.calculate_progress() == pytest.approx(35.0) # Complete the pulled layer progress.process_event(