mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00

* Drop report method from frame helper * Adjust test_prevent_flooding * Adjust test_report_missing_integration_frame * Adjust test_report_error_if_integration * Remove test_report
612 lines
19 KiB
Python
612 lines
19 KiB
Python
"""Test the frame helper."""
|
|
|
|
from contextlib import AbstractContextManager, nullcontext as does_not_raise
|
|
from typing import Any
|
|
from unittest.mock import ANY, Mock, patch
|
|
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import frame
|
|
from homeassistant.loader import async_get_integration
|
|
|
|
from tests.common import MockModule, extract_stack_to_frame, mock_integration
|
|
|
|
|
|
async def test_extract_frame_integration(
|
|
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
|
) -> None:
|
|
"""Test extracting the current frame from integration context."""
|
|
integration_frame = frame.get_integration_frame()
|
|
assert integration_frame == frame.IntegrationFrame(
|
|
custom_integration=False,
|
|
frame=mock_integration_frame,
|
|
integration="hue",
|
|
module="homeassistant.components.hue.light",
|
|
relative_filename="homeassistant/components/hue/light.py",
|
|
)
|
|
|
|
|
|
async def test_get_integration_logger(
|
|
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
|
) -> None:
|
|
"""Test extracting the current frame to get the logger."""
|
|
logger = frame.get_integration_logger(__name__)
|
|
assert logger.name == "homeassistant.components.hue"
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations", "hass")
|
|
async def test_extract_frame_resolve_module() -> None:
|
|
"""Test extracting the current frame from integration context."""
|
|
# pylint: disable-next=import-outside-toplevel
|
|
from custom_components.test_integration_frame import call_get_integration_frame
|
|
|
|
integration_frame = call_get_integration_frame()
|
|
|
|
assert integration_frame == frame.IntegrationFrame(
|
|
custom_integration=True,
|
|
frame=ANY,
|
|
integration="test_integration_frame",
|
|
module="custom_components.test_integration_frame",
|
|
relative_filename="custom_components/test_integration_frame/__init__.py",
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("enable_custom_integrations", "hass")
|
|
async def test_get_integration_logger_resolve_module() -> None:
|
|
"""Test getting the logger from integration context."""
|
|
# pylint: disable-next=import-outside-toplevel
|
|
from custom_components.test_integration_frame import call_get_integration_logger
|
|
|
|
logger = call_get_integration_logger(__name__)
|
|
|
|
assert logger.name == "custom_components.test_integration_frame"
|
|
|
|
|
|
async def test_extract_frame_integration_with_excluded_integration(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test extracting the current frame from integration context."""
|
|
correct_frame = Mock(
|
|
filename="/home/dev/homeassistant/components/mdns/light.py",
|
|
lineno="23",
|
|
line="self.light.is_on",
|
|
)
|
|
with patch(
|
|
"homeassistant.helpers.frame.get_current_frame",
|
|
return_value=extract_stack_to_frame(
|
|
[
|
|
Mock(
|
|
filename="/home/dev/homeassistant/core.py",
|
|
lineno="23",
|
|
line="do_something()",
|
|
),
|
|
correct_frame,
|
|
Mock(
|
|
filename="/home/dev/homeassistant/components/zeroconf/usage.py",
|
|
lineno="23",
|
|
line="self.light.is_on",
|
|
),
|
|
Mock(
|
|
filename="/home/dev/mdns/lights.py",
|
|
lineno="2",
|
|
line="something()",
|
|
),
|
|
]
|
|
),
|
|
):
|
|
integration_frame = frame.get_integration_frame(
|
|
exclude_integrations={"zeroconf"}
|
|
)
|
|
|
|
assert integration_frame == frame.IntegrationFrame(
|
|
custom_integration=False,
|
|
frame=correct_frame,
|
|
integration="mdns",
|
|
module=None,
|
|
relative_filename="homeassistant/components/mdns/light.py",
|
|
)
|
|
|
|
|
|
async def test_extract_frame_no_integration(caplog: pytest.LogCaptureFixture) -> None:
|
|
"""Test extracting the current frame without integration context."""
|
|
with (
|
|
patch(
|
|
"homeassistant.helpers.frame.get_current_frame",
|
|
return_value=extract_stack_to_frame(
|
|
[
|
|
Mock(
|
|
filename="/home/paulus/homeassistant/core.py",
|
|
lineno="23",
|
|
line="do_something()",
|
|
),
|
|
Mock(
|
|
filename="/home/paulus/aiohue/lights.py",
|
|
lineno="2",
|
|
line="something()",
|
|
),
|
|
]
|
|
),
|
|
),
|
|
pytest.raises(frame.MissingIntegrationFrame),
|
|
):
|
|
frame.get_integration_frame()
|
|
|
|
|
|
async def test_get_integration_logger_no_integration(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test getting fallback logger without integration context."""
|
|
with patch(
|
|
"homeassistant.helpers.frame.get_current_frame",
|
|
return_value=extract_stack_to_frame(
|
|
[
|
|
Mock(
|
|
filename="/home/paulus/homeassistant/core.py",
|
|
lineno="23",
|
|
line="do_something()",
|
|
),
|
|
Mock(
|
|
filename="/home/paulus/aiohue/lights.py",
|
|
lineno="2",
|
|
line="something()",
|
|
),
|
|
]
|
|
),
|
|
):
|
|
logger = frame.get_integration_logger(__name__)
|
|
|
|
assert logger.name == __name__
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("integration_frame_path", "keywords", "expected_result", "expected_log"),
|
|
[
|
|
pytest.param(
|
|
"homeassistant/test_core",
|
|
{},
|
|
pytest.raises(RuntimeError, match="test_report_string"),
|
|
0,
|
|
id="core default",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_core_integration",
|
|
{},
|
|
does_not_raise(),
|
|
1,
|
|
id="core integration default",
|
|
),
|
|
pytest.param(
|
|
"custom_components/test_custom_integration",
|
|
{},
|
|
does_not_raise(),
|
|
1,
|
|
id="custom integration default",
|
|
),
|
|
pytest.param(
|
|
"custom_components/test_custom_integration",
|
|
{"custom_integration_behavior": frame.ReportBehavior.IGNORE},
|
|
does_not_raise(),
|
|
0,
|
|
id="custom integration ignore",
|
|
),
|
|
pytest.param(
|
|
"custom_components/test_custom_integration",
|
|
{"custom_integration_behavior": frame.ReportBehavior.ERROR},
|
|
pytest.raises(RuntimeError, match="test_report_string"),
|
|
1,
|
|
id="custom integration error",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_integration_frame",
|
|
{"core_integration_behavior": frame.ReportBehavior.IGNORE},
|
|
does_not_raise(),
|
|
0,
|
|
id="core_integration_behavior ignore",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_integration_frame",
|
|
{"core_integration_behavior": frame.ReportBehavior.ERROR},
|
|
pytest.raises(RuntimeError, match="test_report_string"),
|
|
1,
|
|
id="core_integration_behavior error",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/test_integration_frame",
|
|
{"core_behavior": frame.ReportBehavior.IGNORE},
|
|
does_not_raise(),
|
|
0,
|
|
id="core_behavior ignore",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/test_integration_frame",
|
|
{"core_behavior": frame.ReportBehavior.LOG},
|
|
does_not_raise(),
|
|
1,
|
|
id="core_behavior log",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("hass", "mock_integration_frame")
|
|
async def test_report_usage(
|
|
caplog: pytest.LogCaptureFixture,
|
|
snapshot: SnapshotAssertion,
|
|
keywords: dict[str, Any],
|
|
expected_result: AbstractContextManager,
|
|
expected_log: int,
|
|
) -> None:
|
|
"""Test report_usage.
|
|
|
|
Note: This test doesn't set up mock integrations, so it will not
|
|
find the correct issue tracker URL, and we don't check for that.
|
|
"""
|
|
|
|
what = "test_report_string"
|
|
|
|
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()), expected_result:
|
|
frame.report_usage(what, **keywords)
|
|
|
|
assert caplog.text.count(what) == expected_log
|
|
reports = [
|
|
rec.message for rec in caplog.records if rec.message.startswith("Detected")
|
|
]
|
|
assert reports == snapshot
|
|
|
|
|
|
async def test_report_usage_no_hass() -> None:
|
|
"""Test report_usage when frame helper is not set up."""
|
|
|
|
with pytest.raises(RuntimeError, match="Frame helper not set up"):
|
|
frame.report_usage("blablabla")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"integration_frame_path",
|
|
[
|
|
pytest.param(
|
|
"homeassistant/test_core",
|
|
id="core",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_core_integration",
|
|
id="core integration",
|
|
),
|
|
pytest.param(
|
|
"custom_components/test_custom_integration",
|
|
id="custom integration",
|
|
),
|
|
pytest.param(
|
|
"custom_components/unknown_custom_integration",
|
|
id="unknown custom integration",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_integration_frame")
|
|
async def test_report_usage_find_issue_tracker(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test report_usage finds the correct issue tracker.
|
|
|
|
Note: The issue tracker is found by loader.async_suggest_report_issue, this
|
|
test is a sanity check to ensure async_suggest_report_issue is given the
|
|
right parameters.
|
|
"""
|
|
|
|
what = "test_report_string"
|
|
mock_integration(hass, MockModule("test_core_integration"))
|
|
mock_integration(
|
|
hass,
|
|
MockModule(
|
|
"test_custom_integration",
|
|
partial_manifest={"issue_tracker": "https://blablabla.com"},
|
|
),
|
|
built_in=False,
|
|
)
|
|
|
|
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.LOG)
|
|
|
|
assert caplog.text.count(what) == 1
|
|
reports = [
|
|
rec.message for rec in caplog.records if rec.message.startswith("Detected")
|
|
]
|
|
assert reports == snapshot
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"integration_frame_path",
|
|
[
|
|
pytest.param(
|
|
"homeassistant/test_core",
|
|
id="core",
|
|
),
|
|
pytest.param(
|
|
"homeassistant/components/test_core_integration",
|
|
id="core integration",
|
|
),
|
|
pytest.param(
|
|
"custom_components/test_custom_integration",
|
|
id="custom integration",
|
|
),
|
|
pytest.param(
|
|
"custom_components/unknown_custom_integration",
|
|
id="unknown custom integration",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_integration_frame")
|
|
async def test_report_usage_find_issue_tracker_other_thread(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test report_usage finds the correct issue tracker.
|
|
|
|
In this test, we run the report_usage in a separate thread.
|
|
|
|
Note: The issue tracker is found by loader.async_suggest_report_issue, this
|
|
test is a sanity check to ensure async_suggest_report_issue is given the
|
|
right parameters.
|
|
"""
|
|
|
|
what = "test_report_string"
|
|
mock_integration(hass, MockModule("test_core_integration"))
|
|
mock_integration(
|
|
hass,
|
|
MockModule(
|
|
"test_custom_integration",
|
|
partial_manifest={"issue_tracker": "https://blablabla.com"},
|
|
),
|
|
built_in=False,
|
|
)
|
|
|
|
def sync_job() -> None:
|
|
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.LOG)
|
|
|
|
await hass.async_add_executor_job(sync_job)
|
|
|
|
assert caplog.text.count(what) == 1
|
|
reports = [
|
|
rec.message for rec in caplog.records if rec.message.startswith("Detected")
|
|
]
|
|
assert reports == snapshot
|
|
|
|
|
|
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
|
@pytest.mark.usefixtures("hass", "mock_integration_frame")
|
|
async def test_prevent_flooding(
|
|
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
|
) -> None:
|
|
"""Test to ensure a report is only written once to the log."""
|
|
|
|
what = "accessed hi instead of hello"
|
|
key = "/home/paulus/homeassistant/components/hue/light.py:23"
|
|
integration = "hue"
|
|
filename = "homeassistant/components/hue/light.py"
|
|
|
|
expected_message = (
|
|
f"Detected that integration '{integration}' {what} at {filename}, line "
|
|
f"{mock_integration_frame.lineno}: {mock_integration_frame.line}. "
|
|
f"Please create a bug report at https://github.com/home-assistant/core/issues?"
|
|
f"q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+{integration}%22"
|
|
)
|
|
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.LOG)
|
|
assert expected_message in caplog.text
|
|
assert key in frame._REPORTED_INTEGRATIONS
|
|
assert len(frame._REPORTED_INTEGRATIONS) == 1
|
|
|
|
caplog.clear()
|
|
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.LOG)
|
|
assert expected_message not in caplog.text
|
|
assert key in frame._REPORTED_INTEGRATIONS
|
|
assert len(frame._REPORTED_INTEGRATIONS) == 1
|
|
|
|
|
|
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
|
@pytest.mark.usefixtures("hass", "mock_integration_frame")
|
|
async def test_breaks_in_ha_version(
|
|
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
|
) -> None:
|
|
"""Test to ensure a report is only written once to the log."""
|
|
|
|
what = "accessed hi instead of hello"
|
|
integration = "hue"
|
|
filename = "homeassistant/components/hue/light.py"
|
|
|
|
expected_message = (
|
|
f"Detected that integration '{integration}' {what} at {filename}, line "
|
|
f"{mock_integration_frame.lineno}: {mock_integration_frame.line}. "
|
|
f"This will stop working in Home Assistant 2024.11, please create a bug "
|
|
"report at https://github.com/home-assistant/core/issues?"
|
|
f"q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+{integration}%22"
|
|
)
|
|
|
|
frame.report_usage(what, breaks_in_ha_version="2024.11")
|
|
assert expected_message in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("hass")
|
|
async def test_report_missing_integration_frame(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test reporting when no integration is detected."""
|
|
|
|
what = "teststring"
|
|
with patch(
|
|
"homeassistant.helpers.frame.get_integration_frame",
|
|
side_effect=frame.MissingIntegrationFrame,
|
|
):
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.LOG)
|
|
assert what in caplog.text
|
|
assert caplog.text.count(what) == 1
|
|
|
|
caplog.clear()
|
|
|
|
frame.report_usage(what, core_behavior=frame.ReportBehavior.IGNORE)
|
|
assert caplog.text == ""
|
|
|
|
|
|
@pytest.mark.parametrize("run_count", [1, 2])
|
|
# Run this twice to make sure the flood check does not
|
|
# kick in when error_if_integration=True
|
|
@pytest.mark.usefixtures("hass")
|
|
async def test_report_error_if_integration(
|
|
caplog: pytest.LogCaptureFixture, run_count: int
|
|
) -> None:
|
|
"""Test RuntimeError is raised if error_if_integration is set."""
|
|
frames = extract_stack_to_frame(
|
|
[
|
|
Mock(
|
|
filename="/home/paulus/homeassistant/core.py",
|
|
lineno="23",
|
|
line="do_something()",
|
|
),
|
|
Mock(
|
|
filename="/home/paulus/homeassistant/components/hue/light.py",
|
|
lineno="23",
|
|
line="self.light.is_on",
|
|
),
|
|
Mock(
|
|
filename="/home/paulus/aiohue/lights.py",
|
|
lineno="2",
|
|
line="something()",
|
|
),
|
|
]
|
|
)
|
|
with (
|
|
patch(
|
|
"homeassistant.helpers.frame.get_current_frame",
|
|
return_value=frames,
|
|
),
|
|
pytest.raises(
|
|
RuntimeError,
|
|
match=(
|
|
"Detected that integration 'hue' did a bad"
|
|
" thing at homeassistant/components/hue/light.py"
|
|
),
|
|
),
|
|
):
|
|
frame.report_usage(
|
|
"did a bad thing", core_integration_behavior=frame.ReportBehavior.ERROR
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"behavior",
|
|
"integration_domain",
|
|
"integration_frame_path",
|
|
"source",
|
|
"logs_again",
|
|
),
|
|
[
|
|
pytest.param(
|
|
"core_behavior",
|
|
None,
|
|
"homeassistant",
|
|
"code that",
|
|
True,
|
|
id="core",
|
|
),
|
|
pytest.param(
|
|
"core_behavior",
|
|
"unknown_integration",
|
|
"homeassistant",
|
|
"code that",
|
|
True,
|
|
id="unknown integration",
|
|
),
|
|
pytest.param(
|
|
"core_integration_behavior",
|
|
"sensor",
|
|
"homeassistant",
|
|
"that integration 'sensor'",
|
|
False,
|
|
id="core integration",
|
|
),
|
|
pytest.param(
|
|
"custom_integration_behavior",
|
|
"test_package",
|
|
"homeassistant",
|
|
"that custom integration 'test_package'",
|
|
False,
|
|
id="custom integration",
|
|
),
|
|
# Assert integration found in stack frame has priority over integration_domain
|
|
pytest.param(
|
|
"core_integration_behavior",
|
|
"sensor",
|
|
"homeassistant/components/hue",
|
|
"that integration 'hue'",
|
|
False,
|
|
id="core integration stack mismatch",
|
|
),
|
|
# Assert integration found in stack frame has priority over integration_domain
|
|
pytest.param(
|
|
"custom_integration_behavior",
|
|
"test_package",
|
|
"custom_components/hue",
|
|
"that custom integration 'hue'",
|
|
False,
|
|
id="custom integration stack mismatch",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("enable_custom_integrations", "mock_integration_frame")
|
|
async def test_report_integration_domain(
|
|
hass: HomeAssistant,
|
|
caplog: pytest.LogCaptureFixture,
|
|
behavior: str,
|
|
integration_domain: str | None,
|
|
source: str,
|
|
logs_again: bool,
|
|
) -> None:
|
|
"""Test report_usage when integration_domain is specified."""
|
|
await async_get_integration(hass, "sensor")
|
|
await async_get_integration(hass, "test_package")
|
|
|
|
what = "test_report_string"
|
|
lookup_text = f"Detected {source} {what}"
|
|
|
|
caplog.clear()
|
|
frame.report_usage(
|
|
what,
|
|
**{behavior: frame.ReportBehavior.IGNORE},
|
|
integration_domain=integration_domain,
|
|
)
|
|
|
|
assert lookup_text not in caplog.text
|
|
|
|
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
|
|
frame.report_usage(
|
|
what,
|
|
**{behavior: frame.ReportBehavior.LOG},
|
|
integration_domain=integration_domain,
|
|
)
|
|
|
|
assert lookup_text in caplog.text
|
|
|
|
# Check that it does not log again
|
|
caplog.clear()
|
|
frame.report_usage(
|
|
what,
|
|
**{behavior: frame.ReportBehavior.LOG},
|
|
integration_domain=integration_domain,
|
|
)
|
|
|
|
assert (lookup_text in caplog.text) == logs_again
|
|
|
|
# Check that it raises
|
|
with pytest.raises(RuntimeError, match=lookup_text):
|
|
frame.report_usage(
|
|
what,
|
|
**{behavior: frame.ReportBehavior.ERROR},
|
|
integration_domain=integration_domain,
|
|
)
|