From cc9b6eaaaef19145c972f3b6745d32305de3f2e4 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Mon, 11 May 2026 16:48:15 +0200 Subject: [PATCH 1/5] prusalink: extract press_button_and_verify fixture for button tests All four button tests followed the same shape: assert entity in `unknown` state, patch the API method, press the button, assert the mock was called, then patch with `side_effect=Conflict` and assert HomeAssistantError surfaces. Extract that into a single fixture so the tests only declare which entity/method pair they exercise. Per balloob's follow-up suggestion on #170193: https://github.com/home-assistant/core/pull/170193#discussion_r3219005842 Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/components/prusalink/test_button.py | 154 ++++++++-------------- 1 file changed, 55 insertions(+), 99 deletions(-) diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index 99406fb1a8e53..e238d023e8ead 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -10,8 +10,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.typing import ClientSessionGenerator - @pytest.fixture(autouse=True) def setup_button_platform_only(): @@ -20,6 +18,49 @@ def setup_button_platform_only(): yield +@pytest.fixture +def press_button_and_verify(hass: HomeAssistant): + """Return a helper that asserts the press path for a PrusaLink button. + + The helper verifies the entity is in the `unknown` state, that pressing it + invokes the matching pyprusalink method once, and that a `Conflict` from + the API surfaces as `HomeAssistantError`. + """ + + async def _press_and_verify(entity_id: str, method: str) -> None: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "unknown" + + with ( + patch(f"pyprusalink.PrusaLink.{method}") as mock_meth, + patch( + "homeassistant.components.prusalink.coordinator." + "PrusaLinkUpdateCoordinator._fetch_data" + ), + ): + await hass.services.async_call( + "button", + "press", + {"entity_id": entity_id}, + blocking=True, + ) + assert len(mock_meth.mock_calls) == 1 + + with ( + pytest.raises(HomeAssistantError), + patch(f"pyprusalink.PrusaLink.{method}", side_effect=Conflict), + ): + await hass.services.async_call( + "button", + "press", + {"entity_id": entity_id}, + blocking=True, + ) + + return _press_and_verify + + @pytest.mark.parametrize( ("object_id", "method"), [ @@ -31,40 +72,15 @@ async def test_button_pause_cancel( hass: HomeAssistant, mock_config_entry, mock_api, - hass_client: ClientSessionGenerator, mock_job_api_printing, mock_get_status_printing, - object_id, - method, + press_button_and_verify, + object_id: str, + method: str, ) -> None: - """Test cancel and pause button.""" - entity_id = f"button.{object_id}" + """Test cancel and pause buttons in PRINTING state.""" assert await async_setup_component(hass, "prusalink", {}) - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "unknown" - - with patch(f"pyprusalink.PrusaLink.{method}") as mock_meth: - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) - - assert len(mock_meth.mock_calls) == 1 - - # Verify it calls correct method + does error handling - with ( - pytest.raises(HomeAssistantError), - patch(f"pyprusalink.PrusaLink.{method}", side_effect=Conflict), - ): - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) + await press_button_and_verify(f"button.{object_id}", method) @pytest.mark.parametrize( @@ -78,86 +94,26 @@ async def test_button_resume_cancel( hass: HomeAssistant, mock_config_entry, mock_api, - hass_client: ClientSessionGenerator, mock_job_api_paused, - object_id, - method, + press_button_and_verify, + object_id: str, + method: str, ) -> None: - """Test resume button.""" - entity_id = f"button.{object_id}" + """Test cancel and resume buttons in PAUSED state.""" assert await async_setup_component(hass, "prusalink", {}) - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "unknown" - - with ( - patch(f"pyprusalink.PrusaLink.{method}") as mock_meth, - patch( - "homeassistant.components.prusalink.coordinator.PrusaLinkUpdateCoordinator._fetch_data" - ), - ): - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) - - assert len(mock_meth.mock_calls) == 1 - - # Verify it calls correct method + does error handling - with ( - pytest.raises(HomeAssistantError), - patch(f"pyprusalink.PrusaLink.{method}", side_effect=Conflict), - ): - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) + await press_button_and_verify(f"button.{object_id}", method) async def test_button_continue( hass: HomeAssistant, mock_config_entry, mock_api, - hass_client: ClientSessionGenerator, mock_job_api_attention, + press_button_and_verify, ) -> None: """Test continue button is enabled in ATTENTION state and calls continue_job.""" - entity_id = "button.mock_title_continue_job" assert await async_setup_component(hass, "prusalink", {}) - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "unknown" - - with ( - patch("pyprusalink.PrusaLink.continue_job") as mock_meth, - patch( - "homeassistant.components.prusalink.coordinator.PrusaLinkUpdateCoordinator._fetch_data" - ), - ): - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) - - assert len(mock_meth.mock_calls) == 1 - - # Verify error handling — Conflict raised by API surfaces as HomeAssistantError - with ( - pytest.raises(HomeAssistantError), - patch("pyprusalink.PrusaLink.continue_job", side_effect=Conflict), - ): - await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, - blocking=True, - ) + await press_button_and_verify("button.mock_title_continue_job", "continue_job") async def test_button_continue_unavailable_when_printing( From 03fba55cc9ee3d8a8f87ce1333a052c7f8ecf2da Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Mon, 11 May 2026 17:00:36 +0200 Subject: [PATCH 2/5] prusalink: drop ineffective _fetch_data patch in button-press helper Per Copilot's review on #170332: patching the base class `PrusaLinkUpdateCoordinator._fetch_data` is a no-op because each concrete coordinator overrides `_fetch_data`, so Python's MRO never resolves to the patched base-class method. The button press calls `coordinator.async_request_refresh()` on every coordinator (see `button.py:111-113`), so the original patch never actually prevented the refresh it claimed to. The refresh still succeeds silently against the already-mocked `mock_api` GET methods, which is why the tests pass without the patch. Tests stay green (6/6) and coverage on `button.py` remains 100%. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/components/prusalink/test_button.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index e238d023e8ead..6bfcee258eaf9 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -32,13 +32,7 @@ async def _press_and_verify(entity_id: str, method: str) -> None: assert state is not None assert state.state == "unknown" - with ( - patch(f"pyprusalink.PrusaLink.{method}") as mock_meth, - patch( - "homeassistant.components.prusalink.coordinator." - "PrusaLinkUpdateCoordinator._fetch_data" - ), - ): + with patch(f"pyprusalink.PrusaLink.{method}") as mock_meth: await hass.services.async_call( "button", "press", From 3a2b1e8e7d41313edc054ee2776ca7be366753e2 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Mon, 11 May 2026 17:59:24 +0200 Subject: [PATCH 3/5] prusalink: assert button-press method is awaited, not just called Per Copilot's review on #170332: `AsyncMock.assert_awaited_once()` fails if the integration ever stops awaiting the async pyprusalink method, while `mock_calls == 1` would silently pass that regression. `patch()` auto-detects the patched method as async since 3.8, so `mock_meth` is already an `AsyncMock` and the new assertion is the strictly stronger check. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/components/prusalink/test_button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index 6bfcee258eaf9..d52ca4fa8a1d0 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -39,7 +39,7 @@ async def _press_and_verify(entity_id: str, method: str) -> None: {"entity_id": entity_id}, blocking=True, ) - assert len(mock_meth.mock_calls) == 1 + mock_meth.assert_awaited_once() with ( pytest.raises(HomeAssistantError), From 4660d291040afbd53596e496f2fb649369e589c8 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Fri, 15 May 2026 11:15:07 +0200 Subject: [PATCH 4/5] Add return type hint for PrusaLink button test helper --- tests/components/prusalink/test_button.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index d52ca4fa8a1d0..ee3746cef7cef 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -1,5 +1,6 @@ """Test Prusalink buttons.""" +from collections.abc import Awaitable, Callable from unittest.mock import patch from pyprusalink.types import Conflict @@ -19,7 +20,9 @@ def setup_button_platform_only(): @pytest.fixture -def press_button_and_verify(hass: HomeAssistant): +def press_button_and_verify( + hass: HomeAssistant, +) -> Callable[[str, str], Awaitable[None]]: """Return a helper that asserts the press path for a PrusaLink button. The helper verifies the entity is in the `unknown` state, that pressing it From 1ab0c22a81c3a15be82a1f3862591db46ec39341 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Wed, 20 May 2026 13:38:15 +0200 Subject: [PATCH 5/5] Fix prusalink button test entity_id The continue_job button uses the entity_id with 'workshop_' prefix instead of just 'mock_title_'. See #170560 --- tests/components/prusalink/test_button.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index 5d87c13073b51..3f66a6619d629 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -109,9 +109,10 @@ async def test_button_continue( press_button_and_verify, ) -> None: """Test continue button is enabled in ATTENTION state and calls continue_job.""" - entity_id = "button.workshop_mock_title_continue_job" assert await async_setup_component(hass, "prusalink", {}) - await press_button_and_verify("button.mock_title_continue_job", "continue_job") + await press_button_and_verify( + "button.workshop_mock_title_continue_job", "continue_job" + ) async def test_button_continue_unavailable_when_printing(