Validate quality scale tiers against the tier declared in the integration manifest (#131286)

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
Allen Porter 2024-11-22 10:27:40 -08:00 committed by GitHub
parent 0626b005e2
commit 96e67373db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 67 deletions

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from enum import IntEnum, StrEnum, auto from enum import StrEnum, auto
import json import json
from pathlib import Path from pathlib import Path
import subprocess import subprocess
@ -20,7 +20,7 @@ from voluptuous.humanize import humanize_error
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .model import Config, Integration from .model import Config, Integration, ScaledQualityScaleTiers
DOCUMENTATION_URL_SCHEMA = "https" DOCUMENTATION_URL_SCHEMA = "https"
DOCUMENTATION_URL_HOST = "www.home-assistant.io" DOCUMENTATION_URL_HOST = "www.home-assistant.io"
@ -28,15 +28,6 @@ DOCUMENTATION_URL_PATH_PREFIX = "/integrations/"
DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"} DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"}
class ScaledQualityScaleTiers(IntEnum):
"""Supported manifest quality scales."""
BRONZE = 1
SILVER = 2
GOLD = 3
PLATINUM = 4
class NonScaledQualityScaleTiers(StrEnum): class NonScaledQualityScaleTiers(StrEnum):
"""Supported manifest quality scales.""" """Supported manifest quality scales."""

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import IntEnum
import json import json
import pathlib import pathlib
from typing import Any, Literal from typing import Any, Literal
@ -230,3 +231,12 @@ class Integration:
self._manifest = manifest self._manifest = manifest
self.manifest_path = manifest_path self.manifest_path = manifest_path
class ScaledQualityScaleTiers(IntEnum):
"""Supported manifest quality scales."""
BRONZE = 1
SILVER = 2
GOLD = 3
PLATINUM = 4

View File

@ -9,32 +9,52 @@ from homeassistant.const import Platform
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.yaml import load_yaml_dict from homeassistant.util.yaml import load_yaml_dict
from .model import Config, Integration from .model import Config, Integration, ScaledQualityScaleTiers
RULES = [ QUALITY_SCALE_TIERS = {value.name.lower(): value for value in ScaledQualityScaleTiers}
"action-exceptions",
RULES = {
ScaledQualityScaleTiers.BRONZE: [
"action-setup", "action-setup",
"appropriate-polling", "appropriate-polling",
"async-dependency",
"brands", "brands",
"common-modules", "common-modules",
"config-entry-unloading",
"config-flow", "config-flow",
"config-flow-test-coverage", "config-flow-test-coverage",
"dependency-transparency", "dependency-transparency",
"docs-actions",
"docs-high-level-description",
"docs-installation-parameters",
"docs-installation-instructions",
"docs-removal-instructions",
"entity-event-setup",
"entity-unique-id",
"has-entity-name",
"runtime-data",
"test-before-configure",
"test-before-setup",
"unique-config-entry",
],
ScaledQualityScaleTiers.SILVER: [
"action-exceptions",
"config-entry-unloading",
"docs-configuration-parameters",
"docs-installation-parameters",
"entity-unavailable",
"integration-owner",
"log-when-unavailable",
"parallel-updates",
"reauthentication-flow",
"test-coverage",
],
ScaledQualityScaleTiers.GOLD: [
"devices", "devices",
"diagnostics", "diagnostics",
"discovery", "discovery",
"discovery-update-info", "discovery-update-info",
"docs-actions",
"docs-configuration-parameters",
"docs-data-update", "docs-data-update",
"docs-examples", "docs-examples",
"docs-high-level-description",
"docs-installation-instructions",
"docs-installation-parameters",
"docs-known-limitations", "docs-known-limitations",
"docs-removal-instructions",
"docs-supported-devices", "docs-supported-devices",
"docs-supported-functions", "docs-supported-functions",
"docs-troubleshooting", "docs-troubleshooting",
@ -43,28 +63,19 @@ RULES = [
"entity-category", "entity-category",
"entity-device-class", "entity-device-class",
"entity-disabled-by-default", "entity-disabled-by-default",
"entity-event-setup",
"entity-translations", "entity-translations",
"entity-unavailable",
"entity-unique-id",
"exception-translations", "exception-translations",
"has-entity-name",
"icon-translations", "icon-translations",
"inject-websession",
"integration-owner",
"log-when-unavailable",
"parallel-updates",
"reauthentication-flow",
"reconfiguration-flow", "reconfiguration-flow",
"repair-issues", "repair-issues",
"runtime-data",
"stale-devices", "stale-devices",
],
ScaledQualityScaleTiers.PLATINUM: [
"async-dependency",
"inject-websession",
"strict-typing", "strict-typing",
"test-before-configure", ],
"test-before-setup", }
"test-coverage",
"unique-config-entry",
]
INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"abode", "abode",
@ -1264,7 +1275,8 @@ SCHEMA = vol.Schema(
} }
), ),
) )
for rule in RULES for tier_list in RULES.values()
for rule in tier_list
} }
) )
} }
@ -1275,6 +1287,9 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
"""Validate quality scale file for integration.""" """Validate quality scale file for integration."""
if not integration.core: if not integration.core:
return return
declared_quality_scale = QUALITY_SCALE_TIERS.get(integration.quality_scale)
iqs_file = integration.path / "quality_scale.yaml" iqs_file = integration.path / "quality_scale.yaml"
has_file = iqs_file.is_file() has_file = iqs_file.is_file()
if not has_file: if not has_file:
@ -1288,6 +1303,12 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
"Quality scale definition not found. New integrations are required to at least reach the Bronze tier.", "Quality scale definition not found. New integrations are required to at least reach the Bronze tier.",
) )
return return
if declared_quality_scale is not None:
integration.add_error(
"quality_scale",
"Quality scale definition not found. Integrations that set a manifest quality scale must have a quality scale definition.",
)
return
return return
if integration.integration_type == "virtual": if integration.integration_type == "virtual":
integration.add_error( integration.add_error(
@ -1322,6 +1343,28 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
"quality_scale", f"Invalid {name}: {humanize_error(data, err)}" "quality_scale", f"Invalid {name}: {humanize_error(data, err)}"
) )
if declared_quality_scale is None:
return
rules_met = set()
for rule_name, rule_value in data.get("rules", {}).items():
status = rule_value["status"] if isinstance(rule_value, dict) else rule_value
if status in {"done", "exempt"}:
rules_met.add(rule_name)
# An integration must have all the necessary rules for the declared
# quality scale, and all the rules below.
for scale in ScaledQualityScaleTiers:
if scale > declared_quality_scale:
break
required_rules = set(RULES[scale])
if missing_rules := (required_rules - rules_met):
friendly_rule_str = "\n".join(f" {rule}: todo" for rule in missing_rules)
integration.add_error(
"quality_scale",
f"Quality scale tier {scale.name.lower()} requires quality scale rules to be met:\n{friendly_rule_str}",
)
def validate(integrations: dict[str, Integration], config: Config) -> None: def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Handle YAML files inside integrations.""" """Handle YAML files inside integrations."""