Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,49 @@
.idea
.venv

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml

# Claude Code settings
.claude/*

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual environments
venv/
ENV/
env/
.env

# IDE
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db
719 changes: 719 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[tool.poetry]
name = "ha-tion-btle"
version = "4.2.0"
description = "Home Assistant custom component for Tion BTLE devices"
authors = ["IATkachenko"]
readme = "README.md"
packages = [
{ include = "custom_components" },
]

[tool.poetry.dependencies]
python = "^3.11"
tion-btle = "3.3.6"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-cov = "^4.0.0"
pytest-mock = "^3.12.0"
pytest-asyncio = "^0.23.0"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
"--cov=custom_components",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=80",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
asyncio_mode = "auto"

[tool.coverage.run]
source = ["custom_components"]
omit = [
"tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
show_missing = true
precision = 2

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"
Empty file added tests/__init__.py
Empty file.
129 changes: 129 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Shared pytest fixtures for testing Home Assistant custom component."""
import asyncio
import pytest
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock
from typing import AsyncGenerator, Generator

import pytest_asyncio


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as temp_dir:
yield Path(temp_dir)


@pytest.fixture
def mock_config_entry() -> Generator[Mock, None, None]:
"""Create a mock config entry for testing."""
mock_entry = Mock()
mock_entry.unique_id = "test_mac_address"
mock_entry.title = "Test Tion Device"
mock_entry.data = {
"mac": "AA:BB:CC:DD:EE:FF",
"model": "S3",
"name": "Test Tion"
}
mock_entry.options = {}
mock_entry.async_on_unload = Mock()
yield mock_entry


@pytest.fixture
def mock_hass() -> Generator[Mock, None, None]:
"""Create a mock Home Assistant instance."""
hass = Mock()
hass.data = {}
hass.config_entries = Mock()
hass.config_entries.async_forward_entry_setups = AsyncMock(return_value=True)
hass.config_entries.async_update_entry = Mock()
yield hass


@pytest.fixture
def mock_ble_device() -> Generator[Mock, None, None]:
"""Create a mock BLE device."""
device = Mock()
device.address = "AA:BB:CC:DD:EE:FF"
device.name = "Test Tion Device"
yield device


@pytest.fixture
def mock_tion_device() -> Generator[Mock, None, None]:
"""Create a mock Tion device."""
device = AsyncMock()
device.get.return_value = {
"state": "on",
"heater": "off",
"heating": "off",
"filter_remain": 100.5,
"fan_speed": 2,
"temperature": 20,
"target_temperature": 22,
"model": "S3",
"fw_version": "1.0.0"
}
device.set = AsyncMock()
device.connect = AsyncMock()
device.disconnect = AsyncMock()
device.update_btle_device = Mock()
yield device


@pytest.fixture
def mock_bluetooth() -> Generator[Mock, None, None]:
"""Mock bluetooth functionality."""
with pytest.mock.patch("custom_components.ha_tion_btle.bluetooth") as mock:
mock.async_ble_device_from_address.return_value = Mock()
mock.async_register_callback.return_value = Mock()
mock.BluetoothScanningMode.ACTIVE = "active"
yield mock


@pytest.fixture
def mock_tion_btle() -> Generator[Mock, None, None]:
"""Mock tion_btle module."""
with pytest.mock.patch("custom_components.ha_tion_btle.tion_btle") as mock:
# Mock the different device types
mock.TionS3 = Mock()
mock.TionLite = Mock()
mock.TionS4 = Mock()
yield mock


@pytest.fixture
def sample_device_data() -> dict:
"""Sample device data for testing."""
return {
"state": "on",
"heater": "off",
"heating": "off",
"filter_remain": 85,
"fan_speed": 3,
"temperature": 21,
"target_temperature": 23,
"model": "S3",
"fw_version": "1.2.3",
"is_on": True,
"is_heating": False,
"rssi": -45
}


@pytest.fixture
def mock_coordinator_data(sample_device_data) -> dict:
"""Mock coordinator data with processed values."""
return sample_device_data


# Event loop fixture is handled by pytest-asyncio automatically


# Test markers for different test categories
pytestmark = [
pytest.mark.asyncio,
]
Empty file added tests/integration/__init__.py
Empty file.
94 changes: 94 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Validation tests to verify testing infrastructure setup."""
import pytest
from pathlib import Path


def test_project_structure():
"""Test that the basic project structure exists."""
project_root = Path(__file__).parent.parent

# Check for main directories
assert (project_root / "custom_components").exists()
assert (project_root / "custom_components" / "ha_tion_btle").exists()
assert (project_root / "tests").exists()
assert (project_root / "tests" / "unit").exists()
assert (project_root / "tests" / "integration").exists()

# Check for key files
assert (project_root / "pyproject.toml").exists()
assert (project_root / "tests" / "conftest.py").exists()


def test_imports_work():
"""Test that basic imports work correctly."""
# Test basic Python import functionality
# Note: Home Assistant dependencies not available in test environment
import sys
import pathlib

# Verify we can at least see our package structure
project_root = pathlib.Path(__file__).parent.parent
custom_components_path = project_root / "custom_components" / "ha_tion_btle"

assert custom_components_path.exists()
assert (custom_components_path / "__init__.py").exists()
assert (custom_components_path / "const.py").exists()


def test_pytest_configuration():
"""Test that pytest is configured correctly."""
# This test itself validates that pytest is working
assert True


@pytest.mark.unit
def test_unit_marker():
"""Test that unit marker works."""
assert True


@pytest.mark.integration
def test_integration_marker():
"""Test that integration marker works."""
assert True


@pytest.mark.slow
def test_slow_marker():
"""Test that slow marker works."""
assert True


def test_fixtures_available(temp_dir, mock_config_entry, mock_hass):
"""Test that shared fixtures from conftest.py are available."""
# Test temp_dir fixture
assert temp_dir.exists()
assert temp_dir.is_dir()

# Test mock_config_entry fixture
assert mock_config_entry.unique_id == "test_mac_address"
assert mock_config_entry.data["mac"] == "AA:BB:CC:DD:EE:FF"

# Test mock_hass fixture
assert hasattr(mock_hass, 'data')
assert hasattr(mock_hass, 'config_entries')


def test_async_functionality():
"""Test that async test functionality works."""
async def example_async_function():
return "async_result"

import asyncio
result = asyncio.run(example_async_function())
assert result == "async_result"


@pytest.mark.asyncio
async def test_asyncio_marker():
"""Test that asyncio marker works with async tests."""
async def async_operation():
return "success"

result = await async_operation()
assert result == "success"
Empty file added tests/unit/__init__.py
Empty file.