Files
hacs_waste_collection_schedule/tests/test_source_components.py

372 lines
13 KiB
Python

import os
import sys
from functools import cache
from importlib import import_module
from inspect import Parameter, signature
from types import GeneratorType, ModuleType
from typing import Any, Iterable, Type
import yaml
sys.path.append(
os.path.join(os.path.dirname(__file__), "..")
) # isort:skip # noqa: E402
from update_docu_links import ( # isort:skip # noqa: E402
BLACK_LIST,
COUNTRYCODES,
LANGUAGES,
)
SOURCES_NO_COUNTRY = [g.split("/")[-1].removesuffix(".md") for g in BLACK_LIST]
SOURCES_TO_EXCLUDE = ["__init__.py", "example.py"]
SOURCES_EXCLUDE_TEST_CASE_CHECK = ["multiple"]
EXTRA_INFO_TYPES: dict[str, Type] = {
"title": str,
"url": str,
"country": str,
"default_params": dict,
}
EXTRA_INFO_KEYS = list(EXTRA_INFO_TYPES.keys())
SOURCE_PATH = os.path.join(
os.path.dirname(__file__),
"../custom_components/waste_collection_schedule/waste_collection_schedule/source",
)
SOURCE_MD_PATH = os.path.join(os.path.dirname(__file__), "../doc/source/")
ICS_YAML_PATH = os.path.join(os.path.dirname(__file__), "../doc/ics/yaml")
ICS_MD_PATH = os.path.join(os.path.dirname(__file__), "../doc/ics")
def _get_module(source: str) -> ModuleType:
sys.path.append(
os.path.join(
os.path.dirname(__file__), "../custom_components/waste_collection_schedule"
)
)
return import_module(f"waste_collection_schedule.source.{source}")
ICS_MODULE = _get_module("ics")
@cache
def _get_sources() -> list[str]:
sources = []
for file in os.listdir(SOURCE_PATH):
if file.endswith(".py") and file not in SOURCES_TO_EXCLUDE:
sources.append(file[:-3])
print(file)
return sources
@cache
def _get_source_md() -> list[str]:
sources = []
for file in os.listdir(SOURCE_MD_PATH):
if file.endswith(".md"):
sources.append(file[:-3])
return sources
@cache
def _get_ics_sources() -> list[str]:
sources = []
for file in os.listdir(ICS_YAML_PATH):
if file.endswith(".yaml"):
sources.append(file[:-5])
return sources
@cache
def _get_ics_md() -> list[str]:
sources = []
for file in os.listdir(ICS_MD_PATH):
if file.endswith(".md"):
sources.append(file[:-3])
return sources
@cache
def _load_ics_yaml(source: str) -> Any:
with open(os.path.join(ICS_YAML_PATH, f"{source}.yaml")) as file:
return yaml.safe_load(file)
def _is_supported_country_code(code: str) -> bool:
return any(code == country["code"] for country in COUNTRYCODES)
def _has_supported_country_code(file: str) -> bool:
if file.endswith(".py"):
file.removesuffix(".py")
code = file.split("_")[-1]
return _is_supported_country_code(code)
def test_source_md_exists() -> None:
sources = _get_sources()
source_md = _get_source_md()
for source in sources:
assert source in source_md, f"missing source markdown file: {source}.md"
def test_no_extra_source_mds() -> None:
sources = _get_sources()
source_md = _get_source_md()
for source in source_md:
assert source in sources, f"found orphaned source markdown file: {source}.md"
def test_ics_md_exists() -> None:
sources = _get_ics_sources()
source_md = _get_ics_md()
for source in sources:
assert source in source_md, f"missing ics markdown file: {source}.md"
def test_no_extra_ics_mds() -> None:
sources = _get_ics_sources()
source_md = _get_ics_md()
for source in source_md:
assert source in sources, f"found orphaned ics markdown file: {source}.md"
def _param_translation_check(
source: str,
translations: Any,
init_params_names: Iterable[str],
source_param_to_test: str = "translations",
) -> None:
assert isinstance(
translations, dict
), f"{source_param_to_test} must be a dictionary in {source}"
for lang, lang_translations in translations.items():
assert (
lang in LANGUAGES
), f"unknown/unsupported language code {lang} in {source} {source_param_to_test}, must be one of {LANGUAGES}"
assert isinstance(
lang_translations, dict
), f"{source_param_to_test} must be a dictionary in {source}"
for argument, argument_translation in lang_translations.items():
assert isinstance(
argument, str
), f"{source_param_to_test} keys must be strings in {source} for language {lang}"
assert isinstance(
argument_translation, str
), f"{source_param_to_test} values must be strings in {source} for language {lang}"
assert (
argument in init_params_names
), f"{source_param_to_test} key {argument} for language {lang} not a valid parameter in Source class in {source}"
def _test_case_check(
name: Any,
test_case: Any,
source: str,
init_params_names: Iterable[str],
mandatory_init_params_names: Iterable[str],
) -> None:
assert isinstance(name, str), f"test_case key must be a string in source {source}"
assert isinstance(
test_case, dict
), f"test_case value must be a dictionary in source {source}"
for test_case_param in test_case.keys():
assert isinstance(
test_case_param, str
), f"test_case keys must be strings in source {source}"
assert (
test_case_param in init_params_names
), f"test_case key {test_case_param} not a valid parameter in Source class in source {source}"
for param in mandatory_init_params_names:
assert (
param in test_case.keys()
), f"missing mandatory parameter ({param}) in test_case '{name}' in source {source}"
def _test_source_has_necessary_parameters_test_cases(
module: ModuleType,
source: str,
init_params_names: Iterable[str],
mandatory_init_params_names: Iterable[str],
) -> None:
assert hasattr(module, "TEST_CASES"), f"missing test_cases in source {source}"
assert isinstance(
module.TEST_CASES, dict
), f"test_cases must be a dictionary in source {source}"
assert (
len(module.TEST_CASES) > 0
), f"test_cases must not be empty in source {source}"
if source not in SOURCES_EXCLUDE_TEST_CASE_CHECK:
for name, test_case in module.TEST_CASES.items():
_test_case_check(
name, test_case, source, init_params_names, mandatory_init_params_names
)
def _test_source_has_necessary_parameters_extra_info(
extra_info: dict, source: str, init_params_names: Iterable[str]
) -> None:
# check if callable
if callable(extra_info):
try:
extra_info = extra_info()
except Exception as e:
assert False, f"EXTRA_INFO() function in source {source} failed with {e}"
# check if is iterable (list, tupüle, set)
assert isinstance(
extra_info, (list, tuple, set, GeneratorType)
), f"EXTRA_INFO in source {source}, must be or return an iterable"
# check if all items are dictionaries
for item in extra_info:
assert isinstance(
item, dict
), f"EXTRA_INFO in source {source}, must return a list of dictionaries, but at least one is not a dict"
assert (
"title" in item
), f"EXTRA_INFO in source {source}, must have a new title key in each dictionary"
assert isinstance(
item["title"], str
), f"EXTRA_INFO in source {source}, must have a string title key in each dictionary"
for key in item.keys():
assert isinstance(
key, str
), f"EXTRA_INFO in source {source}, must only have string keys in each dictionary"
assert (
key in EXTRA_INFO_KEYS
), f"Found unknown key {key} in source {source}, must have only the following keys: {EXTRA_INFO_KEYS}"
assert isinstance(
item[key], EXTRA_INFO_TYPES[key]
), f"EXTRA_INFO in source {source}, key {key} must have type {EXTRA_INFO_TYPES[key]}"
if "country" in item:
assert _is_supported_country_code(
item["country"]
), f"unsupported country code in source {source} in EXTRA_INFO"
if "default_params" in item:
for key in item["default_params"].keys():
assert isinstance(
key, str
), f"EXTRA_INFO in source {source}, default_params keys must be strings"
assert (
key in init_params_names
), f"EXTRA_INFO in source {source}, default_params key {key} not a valid parameter in Source class"
def test_source_has_necessary_parameters() -> None:
sources = _get_sources()
for source in sources:
module = _get_module(source)
assert hasattr(module, "Source"), f"missing Source class in source {source}"
init_params = signature(module.Source.__init__).parameters
init_params_names = set(init_params.keys()) - {"self"}
mandatory_init_params_names = {
name
for name, param in init_params.items()
if param.default is Parameter.empty
} - {"self"}
assert hasattr(module, "TITLE"), f"missing TITLE in source {source}"
assert hasattr(module, "DESCRIPTION"), f"missing DESCRIPTION in source {source}"
assert hasattr(module, "URL"), f"missing URL in source {source}"
_test_source_has_necessary_parameters_test_cases(
module, source, init_params_names, mandatory_init_params_names
)
assert hasattr(
module.Source, "fetch"
), f"missing fetch method in Source class of source {source}"
if source not in SOURCES_NO_COUNTRY and not _has_supported_country_code(source):
assert hasattr(
module, "COUNTRY"
), f"missing COUNTRY in source {source} or supported countrycode in filename"
assert _is_supported_country_code(
module.COUNTRY
), f"unsupported country code in source {source}"
if hasattr(module, "EXTRA_INFO"):
_test_source_has_necessary_parameters_extra_info(
module.EXTRA_INFO, source, init_params_names
)
if hasattr(module, "HOW_TO_GET_ARGUMENTS_DESCRIPTION"):
assert isinstance(
module.HOW_TO_GET_ARGUMENTS_DESCRIPTION, dict
), f"HOW_TO_GET_ARGUMENTS_DESCRIPTION must be a dictionary in {source}"
for key, value in module.HOW_TO_GET_ARGUMENTS_DESCRIPTION.items():
assert (
key in LANGUAGES
), f"HOW_TO_GET_ARGUMENTS_DESCRIPTION key {key} must be a valid/supported language code in {source}, must be one of {LANGUAGES}"
assert isinstance(
value, str
), f"HOW_TO_GET_ARGUMENTS_DESCRIPTION values must be strings in {source}"
if hasattr(module, "PARAM_TRANSLATIONS"):
_param_translation_check(
source,
module.PARAM_TRANSLATIONS,
init_params_names,
"PARAM_TRANSLATIONS",
)
if hasattr(module, "PARAM_DESCRIPTIONS"):
_param_translation_check(
source,
module.PARAM_DESCRIPTIONS,
init_params_names,
"PARAM_DESCRIPTIONS",
)
def test_ics_source_has_necessary_parameters():
sources = _get_ics_sources()
init_params = signature(ICS_MODULE.Source.__init__).parameters
init_params_names = set(init_params.keys()) - {"self"}
mandatory_init_params_names = {
name for name, param in init_params.items() if param.default is Parameter.empty
} - {"self"}
for source in sources:
data = _load_ics_yaml(source)
assert isinstance(data, dict), f"yaml file {source}.yaml must be a dictionary"
assert "title" in data, f"missing title in yaml file {source}.yaml"
assert "url" in data, f"missing url in yaml file {source}.yaml"
assert "howto" in data, f"missing howto in yaml file {source}.yaml"
assert isinstance(
data["howto"], dict
), f"howto must be a dictionary in yaml file {source}.yaml"
assert (
"en" in data["howto"]
), f"missing english howto translation in {source}.yaml"
for key, value in data["howto"].items():
assert isinstance(key, str), f"howto keys must be strings in {source}.yaml"
assert (
key in LANGUAGES
), f"howto key {key} must be a valid/supported language code in {source}.yaml, must be one of {LANGUAGES}"
assert isinstance(
value, str
), f"howto values must be strings in {source}.yaml"
assert "test_cases" in data, f"missing test_cases in yaml file {source}.yaml"
assert isinstance(
data["test_cases"], dict
), f"test_cases must be a dictionary in yaml file {source}.yaml"
for name, test_case in data["test_cases"].items():
_test_case_check(
name,
test_case,
f"ICS:{source}",
init_params_names,
mandatory_init_params_names,
)
if "extra_info" in data:
_test_source_has_necessary_parameters_extra_info(
data["extra_info"], f"ICS:{source}", init_params_names
)