Skip to content

Commit

Permalink
Add test-before-setup rule to quality_scale validation
Browse files Browse the repository at this point in the history
  • Loading branch information
epenet committed Dec 9, 2024
1 parent 1ec91e7 commit 806cf20
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 1 deletion.
3 changes: 2 additions & 1 deletion script/hassfest/quality_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
reconfiguration_flow,
runtime_data,
strict_typing,
test_before_setup,
unique_config_entry,
)

Expand Down Expand Up @@ -56,7 +57,7 @@ class Rule:
Rule("has-entity-name", ScaledQualityScaleTiers.BRONZE),
Rule("runtime-data", ScaledQualityScaleTiers.BRONZE, runtime_data),
Rule("test-before-configure", ScaledQualityScaleTiers.BRONZE),
Rule("test-before-setup", ScaledQualityScaleTiers.BRONZE),
Rule("test-before-setup", ScaledQualityScaleTiers.BRONZE, test_before_setup),
Rule("unique-config-entry", ScaledQualityScaleTiers.BRONZE, unique_config_entry),
# SILVER
Rule("action-exceptions", ScaledQualityScaleTiers.SILVER),
Expand Down
66 changes: 66 additions & 0 deletions script/hassfest/quality_scale_validation/test_before_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Enforce that the integration raises correctly during initialisation.
https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/test-before-setup/
"""

import ast

from script.hassfest.model import Integration

_VALID_EXCEPTIONS = {
"ConfigEntryNotReady",
"ConfigEntryAuthFailed",
"ConfigEntryError",
}


def _raises_exception(async_setup_entry_function: ast.AsyncFunctionDef) -> bool:
"""Check that a valid exception is raised within `async_setup_entry`."""
for node in ast.walk(async_setup_entry_function):
if isinstance(node, ast.Raise):
if isinstance(node.exc, ast.Name) and node.exc.id in _VALID_EXCEPTIONS:
return True
if isinstance(node.exc, ast.Call) and node.exc.func.id in _VALID_EXCEPTIONS:
return True

return False


def _calls_first_refresh(async_setup_entry_function: ast.AsyncFunctionDef) -> bool:
"""Check that a async_config_entry_first_refresh within `async_setup_entry`."""
for node in ast.walk(async_setup_entry_function):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "async_config_entry_first_refresh"
):
return True

return False


def _get_setup_entry_function(module: ast.Module) -> ast.AsyncFunctionDef | None:
"""Get async_setup_entry function."""
for item in module.body:
if isinstance(item, ast.AsyncFunctionDef) and item.name == "async_setup_entry":
return item
return None


def validate(integration: Integration) -> list[str] | None:
"""Validate correct use of ConfigEntry.runtime_data."""
init_file = integration.path / "__init__.py"
init = ast.parse(init_file.read_text())

# Should not happen, but better to be safe
if not (async_setup_entry := _get_setup_entry_function(init)):
return [f"Could not find `async_setup_entry` in {init_file}"]

if not (
_raises_exception(async_setup_entry) or _calls_first_refresh(async_setup_entry)
):
return [
f"Integration does not raise one of {_VALID_EXCEPTIONS} "
f"in async_setup_entry ({init_file})"
]
return None

0 comments on commit 806cf20

Please sign in to comment.