"""Validate coverage files.""" from __future__ import annotations from pathlib import Path from .model import Config, Integration DONT_IGNORE = ( "config_flow.py", "device_action.py", "device_condition.py", "device_trigger.py", "diagnostics.py", "group.py", "intent.py", "logbook.py", "media_source.py", "recorder.py", "scene.py", ) FORCE_COVERAGE = ("gold", "platinum") CORE_PREFIX = """# Sorted by hassfest. # # To sort, run python3 -m script.hassfest -p coverage [run] source = homeassistant omit = """ COMPONENTS_PREFIX = ( " # omit pieces of code that rely on external devices being present\n" ) SUFFIX = """[report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # TYPE_CHECKING and @overload blocks are never executed during pytest run if TYPE_CHECKING: @overload """ def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" coverage_path = config.root / ".coveragerc" not_found: list[str] = [] unsorted: list[str] = [] checking = False previous_line = "" with coverage_path.open("rt") as fp: for line in fp: line = line.strip() if line == COMPONENTS_PREFIX.strip(): previous_line = "" continue if not line or line.startswith("#"): continue if not checking: if line == "omit =": checking = True continue # Finished if line == "[report]": break path = Path(line) # Discard wildcard path_exists = path while "*" in path_exists.name: path_exists = path_exists.parent if not path_exists.exists(): not_found.append(line) continue if line < previous_line: unsorted.append(line) previous_line = line if not line.startswith("homeassistant/components/"): continue # Ignore sub-directories if len(path.parts) > 4: continue integration_path = path.parent integration = integrations[integration_path.name] if integration.quality_scale in FORCE_COVERAGE: integration.add_error( "coverage", f"has quality scale {integration.quality_scale} and " "should not be present in .coveragerc file", ) continue if (last_part := path.parts[-1]) in {"*", "const.py"} and Path( f"tests/components/{integration.domain}/__init__.py" ).exists(): integration.add_error( "coverage", f"has tests and should not use {last_part} in .coveragerc file", ) continue for check in DONT_IGNORE: if path.parts[-1] not in {"*", check}: continue if (integration_path / check).exists(): integration.add_error( "coverage", f"{check} must not be ignored by the .coveragerc file", ) if unsorted: config.add_error( "coverage", "Paths are unsorted in .coveragerc file. " "Run python3 -m script.hassfest\n - " f"{'\n - '.join(unsorted)}", fixable=True, ) if not_found: raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." ) def generate(integrations: dict[str, Integration], config: Config) -> None: """Sort coverage.""" coverage_path = config.root / ".coveragerc" core = [] components = [] section = "header" with coverage_path.open("rt") as fp: for line in fp: if line == "[report]\n": break if section != "core" and line == "omit =\n": section = "core" elif section != "components" and line == COMPONENTS_PREFIX: section = "components" elif section == "core" and line != "\n": core.append(line) elif section == "components" and line != "\n": components.append(line) assert core, "core should be a non-empty list" assert components, "components should be a non-empty list" content = ( f"{CORE_PREFIX}{"".join(sorted(core))}\n" f"{COMPONENTS_PREFIX}{"".join(sorted(components))}\n" f"\n{SUFFIX}" ) with coverage_path.open("w") as fp: fp.write(content)