core/tests/helpers/test_frame.py

535 lines
16 KiB
Python
Raw Normal View History

"""Test the frame helper."""
2023-01-22 16:26:24 +00:00
from typing import Any
from unittest.mock import ANY, Mock, patch
2021-01-01 21:31:56 +00:00
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers import frame
from homeassistant.loader import async_get_integration
2024-03-07 06:54:09 +00:00
from tests.common import extract_stack_to_frame
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=None,
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")
async def test_extract_frame_resolve_module(hass: HomeAssistant) -> 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")
async def test_get_integration_logger_resolve_module(hass: HomeAssistant) -> 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"
2023-02-20 10:42:56 +00:00
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(
2024-03-07 06:54:09 +00:00
"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",
)
2023-02-20 10:42:56 +00:00
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()",
),
]
),
2024-03-07 06:54:09 +00:00
),
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(
2024-03-07 06:54:09 +00:00
"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_error", "expected_log"),
[
pytest.param(
"homeassistant/test_core",
{},
True,
0,
id="core default",
),
pytest.param(
"homeassistant/components/test_core_integration",
{},
False,
1,
id="core integration default",
),
pytest.param(
"custom_components/test_custom_integration",
{},
False,
1,
id="custom integration default",
),
pytest.param(
"custom_components/test_custom_integration",
{"custom_integration_behavior": frame.ReportBehavior.IGNORE},
False,
0,
id="custom integration ignore",
),
pytest.param(
"custom_components/test_custom_integration",
{"custom_integration_behavior": frame.ReportBehavior.ERROR},
True,
1,
id="custom integration error",
),
pytest.param(
"homeassistant/components/test_integration_frame",
{"core_integration_behavior": frame.ReportBehavior.IGNORE},
False,
0,
id="core_integration_behavior ignore",
),
pytest.param(
"homeassistant/components/test_integration_frame",
{"core_integration_behavior": frame.ReportBehavior.ERROR},
True,
1,
id="core_integration_behavior error",
),
pytest.param(
"homeassistant/test_integration_frame",
{"core_behavior": frame.ReportBehavior.IGNORE},
False,
0,
id="core_behavior ignore",
),
pytest.param(
"homeassistant/test_integration_frame",
{"core_behavior": frame.ReportBehavior.LOG},
False,
1,
id="core_behavior log",
),
],
)
@pytest.mark.usefixtures("mock_integration_frame")
async def test_report_usage(
caplog: pytest.LogCaptureFixture,
keywords: dict[str, Any],
expected_error: bool,
expected_log: int,
) -> None:
"""Test report."""
what = "test_report_string"
errored = False
try:
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
frame.report_usage(what, **keywords)
except RuntimeError:
errored = True
assert errored == expected_error
assert caplog.text.count(what) == expected_log
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_prevent_flooding(
hass: HomeAssistant, 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(what, error_if_core=False)
assert expected_message in caplog.text
assert key in frame._REPORTED_INTEGRATIONS
assert len(frame._REPORTED_INTEGRATIONS) == 1
caplog.clear()
frame.report(what, error_if_core=False)
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())
async def test_breaks_in_ha_version(
hass: HomeAssistant, 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
2023-02-20 10:42:56 +00:00
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(what, error_if_core=False)
assert what in caplog.text
assert caplog.text.count(what) == 1
caplog.clear()
frame.report(what, error_if_core=False, log_custom_component_only=True)
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
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("did a bad thing", error_if_integration=True)
@pytest.mark.parametrize(
("integration_frame_path", "keywords", "expected_error", "expected_log"),
[
pytest.param(
"homeassistant/test_core",
{},
True,
0,
id="core default",
),
pytest.param(
"homeassistant/components/test_core_integration",
{},
False,
1,
id="core integration default",
),
pytest.param(
"custom_components/test_custom_integration",
{},
False,
1,
id="custom integration default",
),
pytest.param(
"custom_components/test_integration_frame",
{"log_custom_component_only": True},
False,
1,
id="log_custom_component_only with custom integration",
),
pytest.param(
"homeassistant/components/test_integration_frame",
{"log_custom_component_only": True},
False,
0,
id="log_custom_component_only with core integration",
),
pytest.param(
"homeassistant/test_integration_frame",
{"error_if_core": False},
False,
1,
id="disable error_if_core",
),
pytest.param(
"custom_components/test_integration_frame",
{"error_if_integration": True},
True,
1,
id="error_if_integration with custom integration",
),
pytest.param(
"homeassistant/components/test_integration_frame",
{"error_if_integration": True},
True,
1,
id="error_if_integration with core integration",
),
],
)
@pytest.mark.usefixtures("mock_integration_frame")
async def test_report(
caplog: pytest.LogCaptureFixture,
keywords: dict[str, Any],
expected_error: bool,
expected_log: int,
) -> None:
"""Test report."""
what = "test_report_string"
errored = False
try:
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
frame.report(what, **keywords)
except RuntimeError:
errored = True
assert errored == expected_error
assert caplog.text.count(what) == expected_log
@pytest.mark.parametrize(
("behavior", "integration_domain", "source", "logs_again"),
[
pytest.param(
"core_behavior",
None,
"code that",
True,
id="core",
),
pytest.param(
"core_behavior",
"unknown_integration",
"code that",
True,
id="unknown integration",
),
pytest.param(
"core_integration_behavior",
"sensor",
"that integration 'sensor'",
False,
id="core integration",
),
pytest.param(
"custom_integration_behavior",
"test_package",
"that custom integration 'test_package'",
False,
id="custom integration",
),
],
)
@pytest.mark.usefixtures("enable_custom_integrations")
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."""
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,
)