From 222f89838e3dc7bff5b5b3592766ea283f16091a Mon Sep 17 00:00:00 2001 From: Andrea Mattioli Date: Wed, 31 Jan 2024 11:51:54 +0000 Subject: [PATCH] add some files for CI --- .pre-commit-config.yaml | 125 +++++++++++++++++++++++++++++++++++ scripts/coverage | 11 +++ scripts/develop | 31 +++++++++ scripts/install/pip_packages | 9 +++ scripts/lint | 8 +++ scripts/run-in-env.sh | 21 ++++++ scripts/setup | 10 +++ scripts/test | 12 ++++ tests/README.md | 25 +++++++ tests/__init__.py | 1 + tests/conftest.py | 63 ++++++++++++++++++ 11 files changed, 316 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 scripts/coverage create mode 100644 scripts/develop create mode 100644 scripts/install/pip_packages create mode 100644 scripts/lint create mode 100644 scripts/run-in-env.sh create mode 100644 scripts/setup create mode 100644 scripts/test create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5b8d0c5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,125 @@ +ci: + skip: + - mypy + - pylint + +default_language_version: + python: python3.11 + +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py311-plus] + - repo: https://github.com/psf/black + rev: 23.11.0 + hooks: + - id: black + args: + - --quiet + <<: &python-files-with-tests + files: ^((custom_components|tests)/.+)?[^/]+\.py$ + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,deebot + - --skip="./.*,*.csv,*.json" + - --quiet-level=2 + exclude_types: [csv, json] + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.6.0 + - pydocstyle==6.1.1 + <<: &python-files + files: ^(custom_components/.+)?[^/]+\.py$ + - repo: https://github.com/PyCQA/bandit + rev: 1.7.5 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=bandit.yaml + <<: *python-files-with-tests + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: detect-private-key + - id: no-commit-to-branch + args: + - -b + - dev + - id: requirements-txt-fixer + - id: mixed-line-ending + args: + - --fix=lf + stages: [manual] + - id: pretty-format-json + args: + - --autofix + - --no-ensure-ascii + - --top-keys=domain,name + files: manifest\.json$ + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + additional_dependencies: + - prettier@3.1.0 + - prettier-plugin-sort-json@3.1.0 + exclude_types: + - python + exclude: manifest\.json$ + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.33.0 + hooks: + - id: yamllint + - repo: local + hooks: + # Run mypy through our wrapper script in order to get the possible + # pyenv and/or virtualenv activated; it may not have been e.g. if + # committing from a GUI tool that was not launched from an activated + # shell. + - id: mypy + name: Check with mypy + entry: scripts/run-in-env.sh mypy + language: script + types: [python] + <<: *python-files + - id: pylint + name: Check with pylint + entry: scripts/run-in-env.sh pylint + language: script + types: [python] + <<: *python-files + + - repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.6.1 + hooks: + - id: check-pre-commit-ci-config + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.27.2 + hooks: + - id: check-github-workflows + - id: check-dependabot + - id: check-jsonschema + name: Check translations files + files: ^custom_components/deebot/translations/ + types: + - json + args: + - --schemafile + - translations.schema.json \ No newline at end of file diff --git a/scripts/coverage b/scripts/coverage new file mode 100644 index 0000000..0d5731a --- /dev/null +++ b/scripts/coverage @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +bash scripts/test > /dev/null +python3 -m \ + coverage \ + report \ + --skip-covered \ No newline at end of file diff --git a/scripts/develop b/scripts/develop new file mode 100644 index 0000000..76dcaa8 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ ! -f "${PWD}/config/configuration.yaml" ]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config + echo "Creating default configuration." + echo " +default_config: + +logger: + default: info + logs: + homeassistant.components.vacuum: debug + custom_components.deebot: debug + deebot_client: debug + +# If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) +debugpy: +# wait: true +" >> "${PWD}/config/configuration.yaml" +fi + +# Set the python path to include our custom_components directory +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug \ No newline at end of file diff --git a/scripts/install/pip_packages b/scripts/install/pip_packages new file mode 100644 index 0000000..45f5262 --- /dev/null +++ b/scripts/install/pip_packages @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +python3 -m pip \ + install \ + --upgrade \ + --disable-pip-version-check \ + "${@}" \ No newline at end of file diff --git a/scripts/lint b/scripts/lint new file mode 100644 index 0000000..789daa9 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +pre-commit install-hooks; +pre-commit run --hook-stage manual --all-files; \ No newline at end of file diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh new file mode 100644 index 0000000..97f5b17 --- /dev/null +++ b/scripts/run-in-env.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eu + +# Activate pyenv and virtualenv if present, then run the specified command + +# pyenv, pyenv-virtualenv +if [ -s .python-version ]; then + PYENV_VERSION=$(head -n 1 .python-version) + export PYENV_VERSION +fi + +# other common virtualenvs +my_path=$(git rev-parse --show-toplevel) + +for venv in venv .venv .; do + if [ -f "${my_path}/${venv}/bin/activate" ]; then + . "${my_path}/${venv}/bin/activate" + fi +done + +exec "$@" \ No newline at end of file diff --git a/scripts/setup b/scripts/setup new file mode 100644 index 0000000..aa9f3d4 --- /dev/null +++ b/scripts/setup @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +scripts/install/pip_packages "pip" +scripts/install/pip_packages setuptools wheel +scripts/install/pip_packages --requirement requirements.txt +pre-commit install \ No newline at end of file diff --git a/scripts/test b/scripts/test new file mode 100644 index 0000000..468c852 --- /dev/null +++ b/scripts/test @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m \ + pytest \ + tests \ + -rxf -x -v -l \ + --cov=./ \ + --cov-report=xml \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..66ab54f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,25 @@ +# Why? + +While tests aren't required to publish a custom component for Home Assistant, they will generally make development easier because good tests will expose when changes you want to make to the component logic will break expected functionality. Home Assistant uses [`pytest`](https://docs.pytest.org/en/latest/) for its tests, and the tests that have been included are modeled after tests that are written for core Home Assistant integrations. These tests pass with 100% coverage (unless something has changed ;) ) and have comments to help you understand the purpose of different parts of the test. + +# Getting Started + +To begin, it is recommended to create a virtual environment to install dependencies: + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +You can then install the dependencies that will allow you to run tests: +`pip3 install -r requirements_test.txt.` + +This will install `homeassistant`, `pytest`, and `pytest-homeassistant-custom-component`, a plugin which allows you to leverage helpers that are available in Home Assistant for core integration tests. + +# Useful commands + +| Command | Description | +| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pytest tests/` | This will run all tests in `tests/` and tell you how many passed/failed | +| `pytest --durations=10 --cov-report term-missing --cov=custom_components.integration_blueprint tests` | This tells `pytest` that your target module to test is `custom_components.integration_blueprint` so that it can give you a [code coverage](https://en.wikipedia.org/wiki/Code_coverage) summary, including % of code that was executed and the line numbers of missed executions. | +| `pytest tests/test_init.py -k test_setup_unload_and_reload_entry` | Runs the `test_setup_unload_and_reload_entry` test function located in `tests/test_init.py` | \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9850051 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for bticino X8000 integration.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f7835f6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +"""Global fixtures for integration_blueprint integration.""" +# Fixtures allow you to replace functions with a Mock object. You can perform +# many options via the Mock to reflect a particular behavior from the original +# function that you want to see without going through the function's actual logic. +# Fixtures can either be passed into tests as parameters, or if autouse=True, they +# will automatically be used across all tests. +# +# Fixtures that are defined in conftest.py are available across all tests. You can also +# define fixtures within a particular test file to scope them locally. +# +# pytest_homeassistant_custom_component provides some fixtures that are provided by +# Home Assistant core. You can find those fixture definitions here: +# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py +# +# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that +# pytest includes fixtures OOB which you can use as defined on this page) +from unittest.mock import patch + +import pytest + +pytest_plugins = "pytest_homeassistant_custom_component" + + +# This fixture enables loading custom integrations in all tests. +# Remove to enable selective use of this fixture +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations): + yield + + +# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent +# notifications. These calls would fail without this fixture since the persistent_notification +# integration is never loaded during a test. +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +# This fixture, when used, will result in calls to async_get_data to return None. To have the call +# return a value, we would add the `return_value=` parameter to the patch call. +@pytest.fixture(name="bypass_get_data") +def bypass_get_data_fixture(): + """Skip calls to get data from API.""" + with patch( + "custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_get_data" + ): + yield + + +# In this fixture, we are forcing calls to async_get_data to raise an Exception. This is useful +# for exception handling. +@pytest.fixture(name="error_on_get_data") +def error_get_data_fixture(): + """Simulate error when retrieving data from API.""" + with patch( + "custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_get_data", + side_effect=Exception, + ): + yield