From 055fd89909cf366f5ccc5ab4e6acf34c9c04d4b0 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 4 Feb 2024 07:18:00 +0000 Subject: [PATCH] Add development container and configs for test environment --- .devcontainer.json | 42 +++++++++++ .gitattributes | 1 + .../install_dependencies/action.yaml | 44 ++++++++++++ .github/workflows/pytest.yaml | 70 +++++++++++++++++++ .gitignore | 7 +- .ruff.toml | 48 +++++++++++++ .vscode/tasks.json | 11 +++ config/configuration.yaml | 8 +++ custom_components/__init__.py | 1 + custom_components/smaev/manifest.json | 1 - hacs.json | 2 +- requirements.txt | 9 +++ requirements_test.txt | 3 - scripts/develop | 20 ++++++ scripts/lint | 7 ++ scripts/setup | 8 +++ setup.cfg | 3 + tests/__init__.py | 1 + tests/conftest.py | 32 +++++++++ tests/test_init.py | 46 ++++++++++++ 20 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 .devcontainer.json create mode 100644 .gitattributes create mode 100644 .github/workflows/install_dependencies/action.yaml create mode 100644 .github/workflows/pytest.yaml create mode 100644 .ruff.toml create mode 100644 .vscode/tasks.json create mode 100644 config/configuration.yaml create mode 100644 custom_components/__init__.py create mode 100644 requirements.txt delete mode 100644 requirements_test.txt create mode 100755 scripts/develop create mode 100755 scripts/lint create mode 100755 scripts/setup create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_init.py diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 0000000..b5c8cc0 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,42 @@ +{ + "name": "alengwenus/ha-sma-ev-charger", + "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "python.pythonPath": "/usr/bin/python3", + "python.analysis.autoSearchPaths": false, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } + } + }, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/rust:1": {} + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/install_dependencies/action.yaml b/.github/workflows/install_dependencies/action.yaml new file mode 100644 index 0000000..6916dae --- /dev/null +++ b/.github/workflows/install_dependencies/action.yaml @@ -0,0 +1,44 @@ +name: 'Install Dependencies' +description: 'Install Home Assistant and test dependencies' +inputs: + python-version: + description: 'Python version' + required: true + default: '3.10' + core-version: + description: 'Home Assistant core version' + required: false + default: 'dev' + +runs: + using: "composite" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref }} + persist-credentials: false + fetch-depth: 0 + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: home-assistant/core + path: core + ref: ${{ inputs.core-version }} + - name: Set up Python ${{ inputs.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ inputs.python-version }} + - name: Install dependencies + shell: bash + run: | + echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" + pip install -r core/requirements.txt --use-pep517 + # because they decided to pull codecov the package from PyPI... + sed -i '/codecov/d' core/requirements_test.txt + pip install -r core/requirements_test.txt --use-pep517 + pip install -e core/ --use-pep517 + pip install pysmaev # this is in smaev's manifest.json + pip install $(python test_dependencies.py) --use-pep517 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 0000000..0a3402c --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,70 @@ +name: pytest + +on: + push: + branches: [feat/tests] + pull_request: + +jobs: + pytest: + name: Run pytest + runs-on: ubuntu-20.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.10" + core-version: "2022.11.5" + - python-version: "3.10" + core-version: "2022.12.9" + - python-version: "3.10" + core-version: "2023.1.7" + - python-version: "3.10" + core-version: "2023.2.5" + - python-version: "3.10" + core-version: "2023.3.6" + - python-version: "3.10" + core-version: "2023.4.6" + - python-version: "3.10" + core-version: "2023.5.2" + - python-version: "3.11" + core-version: "2023.6.1" + - python-version: "3.11" + core-version: "dev" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies + with: + python-version: ${{ matrix.python-version }} + core-version: ${{ matrix.core-version }} + + - name: Link custom_components/smaev + run: | + cd core + + # Link smaev tests + cd tests/components/ + ln -fs ../../../tests smaev + cd - + + - name: Run pytest + timeout-minutes: 60 + run: | + export PYTHONPATH=${PYTHONPATH}:${PWD} + cd core + python3 -X dev -m pytest \ + -vvv \ + -qq \ + --timeout=9 \ + --durations=10 \ + --cov="custom_components.smaev" \ + --cov-report=xml \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/smaev + env: + HA_CLONE: true diff --git a/.gitignore b/.gitignore index cc72b9c..645f15d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,6 @@ lib64 .project .pydevproject -# Visual Studio Code -.vscode - # Testing .tox .coverage @@ -49,3 +46,7 @@ desktop.ini # other sandbox example.py + +# Home Assistant configuration +config/* +!config/configuration.yaml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..b8b5067 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,48 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py310" + +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "E501", # line too long + "E731", # do not assign a lambda expression, use a def +] + +[flake8-pytest-style] +fixture-parentheses = false + +[pyupgrade] +keep-runtime-typing = true + +[mccabe] +max-complexity = 25 diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c6ee32f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Home Assistant on port 8123", + "type": "shell", + "command": "scripts/develop", + "problemMatcher": [] + } + ] +} diff --git a/config/configuration.yaml b/config/configuration.yaml new file mode 100644 index 0000000..ea81d64 --- /dev/null +++ b/config/configuration.yaml @@ -0,0 +1,8 @@ +# https://www.home-assistant.io/integrations/default_config/ +default_config: + +# https://www.home-assistant.io/integrations/logger/ +logger: + default: info + logs: + custom_components.smaev: debug diff --git a/custom_components/__init__.py b/custom_components/__init__.py new file mode 100644 index 0000000..d19d186 --- /dev/null +++ b/custom_components/__init__.py @@ -0,0 +1 @@ +"""Custom Components.""" diff --git a/custom_components/smaev/manifest.json b/custom_components/smaev/manifest.json index 93a96a8..1305534 100644 --- a/custom_components/smaev/manifest.json +++ b/custom_components/smaev/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": [], "documentation": "https://github.com/alengwenus/ha-sma-ev-charger", - "homekit": {}, "iot_class": "local_polling", "issue_tracker": "https://github.com/alengwenus/ha-sma-ev-charger/issues", "requirements": ["pysmaev==0.1.6"], diff --git a/hacs.json b/hacs.json index ebdafa3..a383eee 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "SMA EV Charger", "render_readme": true, - "homeassistant": "2023.9" + "homeassistant": "2023.9.0" } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4739aed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +pre-commit==3.5.0 +black==23.3.0 +ruff==0.1.6 +homeassistant==2024.1.5 +# Install HA and test dependencies (pytest, coverage) +# To pin the dev container to a specific HA version, set this dependency +# to the adequate version (add `==`) and rebuild the dev container. +# See https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/releases for version mappings. +pytest-homeassistant-custom-component==0.13.91 diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index 6332881..0000000 --- a/requirements_test.txt +++ /dev/null @@ -1,3 +0,0 @@ -pre-commit==3.5.0 -black==23.3.0 -ruff==0.1.6 diff --git a/scripts/develop b/scripts/develop new file mode 100755 index 0000000..89eda50 --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/integration_blueprint +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..55a1f48 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +pre-commit run --all-files diff --git a/scripts/setup b/scripts/setup new file mode 100755 index 0000000..0688d70 --- /dev/null +++ b/scripts/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --requirement requirements.txt +pre-commit install-hooks diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..74c027f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[tool:pytest] +testpaths = tests +asyncio_mode = auto diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..99d1067 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Home Assistant SMA EV Charger.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1932c47 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +"""Fixtures for testing.""" +import os +import sys + +import pytest + +# Tests in the dev enviromentment use the pytest_homeassistant_custom_component instead of +# a cloned HA core repo for a simple and clean structure. To still test against a HA core +# clone (e.g. the dev branch for which no pytest_homeassistant_custom_component exists +# because HA does not publish dev snapshot packages), set the HA_CLONE env variable. +if "HA_CLONE" in os.environ: + # Rewire the testing package to the cloned test modules. See the test `Dockerfile` + # for setup details. + sys.modules["pytest_homeassistant_custom_component"] = __import__("tests") + + +@pytest.fixture(name="device_info") +def device_info(): + """Return device info.""" + return { + "name": "SMA EV Charger 22", + "serial": "1234567890", + "model": "EVC22-3AC-10", + "manufacturer": "SMA", + "sw_version": "1.2.23.R", + } + + +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations): + """Enable use of custom_components.""" + yield diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..df041f7 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,46 @@ +"""Test init of SMA EV Charger integration.""" +from unittest.mock import patch + +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_HOST +from homeassistant.config_entries import ConfigEntryState +from custom_components import smaev + +from pytest_homeassistant_custom_component.common import MockConfigEntry + + +CONFIG_DATA = { + "host": "192.168.2.100", + "username": "Test", + "password": "Tester1234&", + "ssl": True, + "verify_ssl": False, +} + + +async def test_async_setup_entry(hass: HomeAssistant, device_info) -> None: + """Test a successful setup entry and unload of entry.""" + entry = MockConfigEntry( + domain=smaev.DOMAIN, + title=CONFIG_DATA[CONF_HOST], + unique_id="serial", + data=CONFIG_DATA, + options={}, + ) + + entry.add_to_hass(hass) + with ( + patch("pysmaev.core.SmaEvCharger.open"), + patch("pysmaev.core.SmaEvCharger.device_info", return_value=device_info), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(smaev.DOMAIN)) == 1 + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + assert not hass.data.get(smaev.DOMAIN)