diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..f427e81 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,27 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + check-rule-format: + runs-on: ubuntu-latest + needs: [validate-lint] + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-asyncio + + - name: Check rule format with pytest + run: | + pytest -v -s ./test/check_rule_format.py diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 9fa283a..8b30a3e 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -3,10 +3,11 @@ name: Validate on: push: branches: - - main + - main pull_request: branches: - - main + - main + workflow_dispatch: jobs: validate-hassfest: @@ -30,16 +31,6 @@ jobs: category: integration ignore: brands - validate-format: - runs-on: ubuntu-latest - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - - - name: Check format - run: | - ./custom_components/xiaomi_home/test/test_all.sh - validate-lint: runs-on: ubuntu-latest steps: @@ -51,7 +42,7 @@ jobs: python -m pip install --upgrade pip pip install pylint - - name: Analyse the code with pylint + - name: Static analyse the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/custom_components/xiaomi_home/test/json_format.py b/custom_components/xiaomi_home/test/json_format.py deleted file mode 100644 index 3363d51..0000000 --- a/custom_components/xiaomi_home/test/json_format.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Check if a file is a valid JSON file. - -Usage: -python json_check.py [JSON file path] - -Example: -python json_check.py multi_lang.json -""" -import argparse -import json -import sys -import os - -def check_json_file(file_path): - try: - with open(file_path, "r", encoding="utf-8") as file: - json.load(file) - return True - except FileNotFoundError: - print(file_path, "is not found.") - return False - except json.JSONDecodeError: - print(file_path, "is not a valid JSON file.") - return False - -def main(): - parser = argparse.ArgumentParser( - description="Check if a file is a valid JSON file.") - parser.add_argument("file_path", help="JSON file path") - args = parser.parse_args() - script_name = os.path.basename(__file__) - file_name = os.path.basename(args.file_path) - - if not check_json_file(args.file_path): - print(args.file_path, script_name, "FAIL") - sys.exit(1) - - print(script_name, file_name, "PASS") - -if __name__ == "__main__": - main() diff --git a/custom_components/xiaomi_home/test/rule_format.py b/custom_components/xiaomi_home/test/rule_format.py deleted file mode 100644 index 7ed872b..0000000 --- a/custom_components/xiaomi_home/test/rule_format.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Check if conversion rules are valid. - -The files to be checked are in the directory of ../miot/specs/ -To run this script, PYTHONPATH must be set first. -See test_all.sh for the usage. - -You can run all tests by running: -``` -./test_all.sh -``` -""" -import sys -import os -import json - -def load_json(file_path: str) -> dict: - """Load json file.""" - with open(file_path, "r", encoding="utf-8") as file: - data = json.load(file) - return data - -def dict_str_str(d: dict) -> bool: - """restricted format: dict[str, str]""" - if not isinstance(d, dict): - return False - for k, v in d.items(): - if not isinstance(k, str) or not isinstance(v, str): - return False - return True - -def dict_str_dict(d: dict) -> bool: - """restricted format: dict[str, dict]""" - if not isinstance(d, dict): - return False - for k, v in d.items(): - if not isinstance(k, str) or not isinstance(v, dict): - return False - return True - -def nested_2_dict_str_str(d: dict) -> bool: - """restricted format: dict[str, dict[str, str]]""" - if not dict_str_dict(d): - return False - for v in d.values(): - if not dict_str_str(v): - return False - return True - -def nested_3_dict_str_str(d: dict) -> bool: - """restricted format: dict[str, dict[str, dict[str, str]]]""" - if not dict_str_dict(d): - return False - for v in d.values(): - if not nested_2_dict_str_str(v): - return False - return True - -def spec_filter(d: dict) -> bool: - """restricted format: dict[str, dict[str, list]]""" - if not dict_str_dict(d): - return False - for value in d.values(): - for k, v in value.items(): - if not isinstance(k, str) or not isinstance(v, list): - return False - if not all(isinstance(i, str) for i in v): - return False - return True - -def bool_trans(d: dict) -> bool: - """dict[str, dict[str, str] | dict[str, dict[str, str]] ]""" - if not isinstance(d, dict): - return False - if "data" not in d or "translate" not in d: - return False - if not dict_str_str(d["data"]): - return False - if not nested_3_dict_str_str(d["translate"]): - return False - - return True - - -def main(): - script_name = os.path.basename(__file__) - - source_dir = "../miot/specs" - if not bool_trans(load_json(f"{source_dir}/bool_trans.json")): - print(script_name, "bool_trans FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/multi_lang.json")): - print(script_name, "multi_lang FAIL") - sys.exit(1) - if not spec_filter(load_json(f"{source_dir}/spec_filter.json")): - print(script_name, "spec_filter FAIL") - sys.exit(1) - - source_dir = "../miot/i18n" - if not nested_3_dict_str_str(load_json(f"{source_dir}/de.json")): - print(script_name, "i18n de.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/en.json")): - print(script_name, "i18n en.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/es.json")): - print(script_name, "i18n es.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/fr.json")): - print(script_name, "i18n fr.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/ja.json")): - print(script_name, "i18n ja.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/ru.json")): - print(script_name, "i18n ru.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/zh-Hans.json")): - print(script_name, "i18n zh-Hans.json FAIL") - sys.exit(1) - if not nested_3_dict_str_str(load_json(f"{source_dir}/zh-Hant.json")): - print(script_name, "i18n zh-Hant.json FAIL") - sys.exit(1) - - print(script_name, "PASS") - -if __name__ == "__main__": - main() diff --git a/custom_components/xiaomi_home/test/test_all.sh b/custom_components/xiaomi_home/test/test_all.sh deleted file mode 100755 index 412ebfa..0000000 --- a/custom_components/xiaomi_home/test/test_all.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -e - -# Get the script path. -script_path=$(dirname "$0") -# Change to the script path. -cd "$script_path" -# Set PYTHONPATH. -cd .. -export PYTHONPATH=`pwd` -echo "PYTHONPATH=$PYTHONPATH" -cd - - -# Run the tests. -export source_dir="../miot/specs" -python3 json_format.py $source_dir/bool_trans.json -python3 json_format.py $source_dir/multi_lang.json -python3 json_format.py $source_dir/spec_filter.json -export source_dir="../miot/i18n" -python3 json_format.py $source_dir/de.json -python3 json_format.py $source_dir/en.json -python3 json_format.py $source_dir/es.json -python3 json_format.py $source_dir/fr.json -python3 json_format.py $source_dir/ja.json -python3 json_format.py $source_dir/ru.json -python3 json_format.py $source_dir/zh-Hans.json -python3 json_format.py $source_dir/zh-Hant.json -python3 rule_format.py diff --git a/test/check_rule_format.py b/test/check_rule_format.py new file mode 100644 index 0000000..ca05966 --- /dev/null +++ b/test/check_rule_format.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +"""Test rule format.""" +import json +from os import listdir, path +from typing import Optional + +SOURCE_DIR: str = path.dirname(path.abspath(__file__)) + + +def load_json_file(file_path: str) -> Optional[dict]: + try: + with open(file_path, 'r', encoding='utf-8') as file: + return json.load(file) + except FileNotFoundError: + print(file_path, 'is not found.') + return None + except json.JSONDecodeError: + print(file_path, 'is not a valid JSON file.') + return None + + +def dict_str_str(d: dict) -> bool: + """restricted format: dict[str, str]""" + if not isinstance(d, dict): + return False + for k, v in d.items(): + if not isinstance(k, str) or not isinstance(v, str): + return False + return True + + +def dict_str_dict(d: dict) -> bool: + """restricted format: dict[str, dict]""" + if not isinstance(d, dict): + return False + for k, v in d.items(): + if not isinstance(k, str) or not isinstance(v, dict): + return False + return True + + +def nested_2_dict_str_str(d: dict) -> bool: + """restricted format: dict[str, dict[str, str]]""" + if not dict_str_dict(d): + return False + for v in d.values(): + if not dict_str_str(v): + return False + return True + + +def nested_3_dict_str_str(d: dict) -> bool: + """restricted format: dict[str, dict[str, dict[str, str]]]""" + if not dict_str_dict(d): + return False + for v in d.values(): + if not nested_2_dict_str_str(v): + return False + return True + + +def spec_filter(d: dict) -> bool: + """restricted format: dict[str, dict[str, list]]""" + if not dict_str_dict(d): + return False + for value in d.values(): + for k, v in value.items(): + if not isinstance(k, str) or not isinstance(v, list): + return False + if not all(isinstance(i, str) for i in v): + return False + return True + + +def bool_trans(d: dict) -> bool: + """dict[str, dict[str, str] | dict[str, dict[str, str]] ]""" + if not isinstance(d, dict): + return False + if 'data' not in d or 'translate' not in d: + return False + if not dict_str_str(d['data']): + return False + if not nested_3_dict_str_str(d['translate']): + return False + return True + + +def test_bool_trans(): + data: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/miot/specs/bool_trans.json')) + assert data + assert bool_trans(data) + + +def test_spec_filter(): + data: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/miot/specs/spec_filter.json')) + assert data + assert spec_filter(data) + + +def test_multi_lang(): + data: dict = load_json_file( + path.join( + SOURCE_DIR, + '../custom_components/xiaomi_home/miot/specs/multi_lang.json')) + assert data + assert nested_3_dict_str_str(data) + + +def test_miot_i18n(): + i18n_path: str = path.join( + SOURCE_DIR, '../custom_components/xiaomi_home/miot/i18n') + for file_name in listdir(i18n_path): + file_path: str = path.join(i18n_path, file_name) + data: dict = load_json_file(file_path) + assert data + assert nested_3_dict_str_str(data)