Skip to content

Commit

Permalink
Unittests for esphome python code (esphome#931)
Browse files Browse the repository at this point in the history
  • Loading branch information
timsavage authored Mar 12, 2020
1 parent 714d28a commit c632b0e
Show file tree
Hide file tree
Showing 16 changed files with 1,212 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = esphome/components/*
3 changes: 3 additions & 0 deletions esphome/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ def __str__(self):
return f'{self.total_days}d'
return '0s'

def __repr__(self):
return f"TimePeriod<{self.total_microseconds}>"

@property
def total_microseconds(self):
return self.total_milliseconds * 1000 + (self.microseconds or 0)
Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
addopts =
--cov=esphome
--cov-branch
6 changes: 6 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ pylint==2.4.4 ; python_version>"3"
flake8==3.7.9
pillow
pexpect

# Unit tests
pytest==5.3.2
pytest-cov==2.8.1
pytest-mock==1.13.0
hypothesis==4.57.0
1 change: 1 addition & 0 deletions script/fulltest
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ set -x
script/ci-custom.py
script/lint-python
script/lint-cpp
script/unit_test
script/test
9 changes: 9 additions & 0 deletions script/unit_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

set -e

cd "$(dirname "$0")/.."

set -x

pytest tests/unit_tests
30 changes: 30 additions & 0 deletions tests/unit_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
ESPHome Unittests
~~~~~~~~~~~~~~~~~
Configuration file for unit tests.
If adding unit tests ensure that they are fast. Slower integration tests should
not be part of a unit test suite.
"""
import sys
import pytest

from pathlib import Path


here = Path(__file__).parent

# Configure location of package root
package_root = here.parent.parent
sys.path.insert(0, package_root.as_posix())


@pytest.fixture
def fixture_path() -> Path:
"""
Location of all fixture files.
"""
return here / "fixtures"

1 change: 1 addition & 0 deletions tests/unit_tests/fixtures/helpers/file-a.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A files are unique.
1 change: 1 addition & 0 deletions tests/unit_tests/fixtures/helpers/file-b_1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
All b files match.
1 change: 1 addition & 0 deletions tests/unit_tests/fixtures/helpers/file-b_2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
All b files match.
1 change: 1 addition & 0 deletions tests/unit_tests/fixtures/helpers/file-c.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
C files are unique.
15 changes: 15 additions & 0 deletions tests/unit_tests/strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Text

import hypothesis.strategies._internal.core as st
from hypothesis.strategies._internal.strategies import SearchStrategy


@st.defines_strategy_with_reusable_values
def mac_addr_strings():
# type: () -> SearchStrategy[Text]
"""A strategy for MAC address strings.
This consists of six strings representing integers [0..255],
without zero-padding, joined by dots.
"""
return st.builds("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}".format, *(6 * [st.integers(0, 255)]))
113 changes: 113 additions & 0 deletions tests/unit_tests/test_config_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import pytest
import string

from hypothesis import given, example
from hypothesis.strategies import one_of, text, integers, booleans, builds

from esphome import config_validation
from esphome.config_validation import Invalid
from esphome.core import Lambda, HexInt


def test_check_not_tamplatable__invalid():
with pytest.raises(Invalid, match="This option is not templatable!"):
config_validation.check_not_templatable(Lambda(""))


@given(one_of(
booleans(),
integers(),
text(alphabet=string.ascii_letters + string.digits)),
)
def test_alphanumeric__valid(value):
actual = config_validation.alphanumeric(value)

assert actual == str(value)


@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_"))
def test_valid_name__valid(value):
actual = config_validation.valid_name(value)

assert actual == value


@pytest.mark.parametrize("value", (
"foo bar", "FooBar", "foo::bar"
))
def test_valid_name__invalid(value):
with pytest.raises(Invalid):
config_validation.valid_name(value)


@given(one_of(integers(), text()))
def test_string__valid(value):
actual = config_validation.string(value)

assert actual == str(value)


@pytest.mark.parametrize("value", (
{}, [], True, False, None
))
def test_string__invalid(value):
with pytest.raises(Invalid):
config_validation.string(value)


@given(text())
def test_strict_string__valid(value):
actual = config_validation.string_strict(value)

assert actual == value


@pytest.mark.parametrize("value", (None, 123))
def test_string_string__invalid(value):
with pytest.raises(Invalid, match="Must be string, got"):
config_validation.string_strict(value)


@given(builds(lambda v: "mdi:" + v, text()))
@example("")
def test_icon__valid(value):
actual = config_validation.icon(value)

assert actual == value


def test_icon__invalid():
with pytest.raises(Invalid, match="Icons should start with prefix"):
config_validation.icon("foo")


@pytest.mark.parametrize("value", (
"True", "YES", "on", "enAblE", True
))
def test_boolean__valid_true(value):
assert config_validation.boolean(value) is True


@pytest.mark.parametrize("value", (
"False", "NO", "off", "disAblE", False
))
def test_boolean__valid_false(value):
assert config_validation.boolean(value) is False


@pytest.mark.parametrize("value", (
None, 1, 0, "foo"
))
def test_boolean__invalid(value):
with pytest.raises(Invalid, match="Expected boolean value"):
config_validation.boolean(value)


# TODO: ensure_list
@given(integers())
def hex_int__valid(value):
actual = config_validation.hex_int(value)

assert isinstance(actual, HexInt)
assert actual == value

Loading

0 comments on commit c632b0e

Please sign in to comment.