Browse Source

Unittests for esphome python code (#931)

pull/999/head
Tim Savage 2 years ago
committed by GitHub
parent
commit
c632b0e1d4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .coveragerc
  2. 3
      esphome/core.py
  3. 4
      pytest.ini
  4. 6
      requirements_test.txt
  5. 1
      script/fulltest
  6. 9
      script/unit_test
  7. 30
      tests/unit_tests/conftest.py
  8. 1
      tests/unit_tests/fixtures/helpers/file-a.txt
  9. 1
      tests/unit_tests/fixtures/helpers/file-b_1.txt
  10. 1
      tests/unit_tests/fixtures/helpers/file-b_2.txt
  11. 1
      tests/unit_tests/fixtures/helpers/file-c.txt
  12. 15
      tests/unit_tests/strategies.py
  13. 113
      tests/unit_tests/test_config_validation.py
  14. 491
      tests/unit_tests/test_core.py
  15. 208
      tests/unit_tests/test_helpers.py
  16. 326
      tests/unit_tests/test_pins.py

2
.coveragerc

@ -0,0 +1,2 @@
[run]
omit = esphome/components/*

3
esphome/core.py

@ -143,6 +143,9 @@ class TimePeriod:
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)

4
pytest.ini

@ -0,0 +1,4 @@
[pytest]
addopts =
--cov=esphome
--cov-branch

6
requirements_test.txt

@ -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
script/fulltest

@ -9,4 +9,5 @@ set -x
script/ci-custom.py
script/lint-python
script/lint-cpp
script/unit_test
script/test

9
script/unit_test

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
set -x
pytest tests/unit_tests

30
tests/unit_tests/conftest.py

@ -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
tests/unit_tests/fixtures/helpers/file-a.txt

@ -0,0 +1 @@
A files are unique.

1
tests/unit_tests/fixtures/helpers/file-b_1.txt

@ -0,0 +1 @@
All b files match.

1
tests/unit_tests/fixtures/helpers/file-b_2.txt

@ -0,0 +1 @@
All b files match.

1
tests/unit_tests/fixtures/helpers/file-c.txt

@ -0,0 +1 @@
C files are unique.

15
tests/unit_tests/strategies.py

@ -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
tests/unit_tests/test_config_validation.py

@ -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

491
tests/unit_tests/test_core.py

@ -0,0 +1,491 @@
import pytest
from hypothesis import given
from hypothesis.provisional import ip4_addr_strings
from strategies import mac_addr_strings
from esphome import core, const
class TestHexInt:
@pytest.mark.parametrize("value, expected", (
(1, "0x01"),
(255, "0xFF"),
(128, "0x80"),
(256, "0x100"),
(-1, "-0x01"), # TODO: this currently fails
))
def test_str(self, value, expected):
target = core.HexInt(value)
actual = str(target)
assert actual == expected
class TestIPAddress:
@given(value=ip4_addr_strings())
def test_init__valid(self, value):
core.IPAddress(*value.split("."))
@pytest.mark.parametrize("value", ("127.0.0", "localhost", ""))
def test_init__invalid(self, value):
with pytest.raises(ValueError, match="IPAddress must consist of 4 items"):
core.IPAddress(*value.split("."))
@given(value=ip4_addr_strings())
def test_str(self, value):
target = core.IPAddress(*value.split("."))
actual = str(target)
assert actual == value
class TestMACAddress:
@given(value=mac_addr_strings())
def test_init__valid(self, value):
core.MACAddress(*value.split(":"))
@pytest.mark.parametrize("value", ("1:2:3:4:5", "localhost", ""))
def test_init__invalid(self, value):
with pytest.raises(ValueError, match="MAC Address must consist of 6 items"):
core.MACAddress(*value.split(":"))
@given(value=mac_addr_strings())
def test_str(self, value):
target = core.MACAddress(*(int(v, 16) for v in value.split(":")))
actual = str(target)
assert actual == value
def test_as_hex(self):
target = core.MACAddress(0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF)
actual = target.as_hex
assert actual.text == "0xDEADBEEF00FFULL"
@pytest.mark.parametrize("value", (
1, 2, -1, 0, 1.0, -1.0, 42.0009, -42.0009
))
def test_is_approximately_integer__in_range(value):
actual = core.is_approximately_integer(value)
assert actual is True
@pytest.mark.parametrize("value", (
42.01, -42.01, 1.5
))
def test_is_approximately_integer__not_in_range(value):
actual = core.is_approximately_integer(value)
assert actual is False
class TestTimePeriod:
@pytest.mark.parametrize("kwargs, expected", (
({}, {}),
({"microseconds": 1}, {"microseconds": 1}),
({"microseconds": 1.0001}, {"microseconds": 1}),
({"milliseconds": 2}, {"milliseconds": 2}),
({"milliseconds": 2.0001}, {"milliseconds": 2}),
({"milliseconds": 2.01}, {"milliseconds": 2, "microseconds": 10}),
({"seconds": 3}, {"seconds": 3}),
({"seconds": 3.0001}, {"seconds": 3}),
({"seconds": 3.01}, {"seconds": 3, "milliseconds": 10}),
({"minutes": 4}, {"minutes": 4}),
({"minutes": 4.0001}, {"minutes": 4}),
({"minutes": 4.1}, {"minutes": 4, "seconds": 6}),
({"hours": 5}, {"hours": 5}),
({"hours": 5.0001}, {"hours": 5}),
({"hours": 5.1}, {"hours": 5, "minutes": 6}),
({"days": 6}, {"days": 6}),
({"days": 6.0001}, {"days": 6}),
({"days": 6.1}, {"days": 6, "hours": 2, "minutes": 24}),
))
def test_init(self, kwargs, expected):
target = core.TimePeriod(**kwargs)
actual = target.as_dict()
assert actual == expected
def test_init__microseconds_with_fraction(self):
with pytest.raises(ValueError, match="Maximum precision is microseconds"):
core.TimePeriod(microseconds=1.1)
@pytest.mark.parametrize("kwargs, expected", (
({}, "0s"),
({"microseconds": 1}, "1us"),
({"microseconds": 1.0001}, "1us"),
({"milliseconds": 2}, "2ms"),
({"milliseconds": 2.0001}, "2ms"),
({"milliseconds": 2.01}, "2010us"),
({"seconds": 3}, "3s"),
({"seconds": 3.0001}, "3s"),
({"seconds": 3.01}, "3010ms"),
({"minutes": 4}, "4min"),
({"minutes": 4.0001}, "4min"),
({"minutes": 4.1}, "246s"),
({"hours": 5}, "5h"),
({"hours": 5.0001}, "5h"),
({"hours": 5.1}, "306min"),
({"days": 6}, "6d"),
({"days": 6.0001}, "6d"),
({"days": 6.1}, "8784min"),
))
def test_str(self, kwargs, expected):
target = core.TimePeriod(**kwargs)
actual = str(target)
assert actual == expected
@pytest.mark.parametrize("comparison, other, expected", (
("__eq__", core.TimePeriod(microseconds=900), False),
("__eq__", core.TimePeriod(milliseconds=1), True),
("__eq__", core.TimePeriod(microseconds=1100), False),
("__eq__", 1000, NotImplemented),
("__eq__", "1000", NotImplemented),
("__eq__", True, NotImplemented),
("__eq__", object(), NotImplemented),
("__eq__", None, NotImplemented),
("__ne__", core.TimePeriod(microseconds=900), True),
("__ne__", core.TimePeriod(milliseconds=1), False),
("__ne__", core.TimePeriod(microseconds=1100), True),
("__ne__", 1000, NotImplemented),
("__ne__", "1000", NotImplemented),
("__ne__", True, NotImplemented),
("__ne__", object(), NotImplemented),
("__ne__", None, NotImplemented),
("__lt__", core.TimePeriod(microseconds=900), False),
("__lt__", core.TimePeriod(milliseconds=1), False),
("__lt__", core.TimePeriod(microseconds=1100), True),
("__lt__", 1000, NotImplemented),
("__lt__", "1000", NotImplemented),
("__lt__", True, NotImplemented),
("__lt__", object(), NotImplemented),
("__lt__", None, NotImplemented),
("__gt__", core.TimePeriod(microseconds=900), True),
("__gt__", core.TimePeriod(milliseconds=1), False),
("__gt__", core.TimePeriod(microseconds=1100), False),
("__gt__", 1000, NotImplemented),
("__gt__", "1000", NotImplemented),
("__gt__", True, NotImplemented),
("__gt__", object(), NotImplemented),
("__gt__", None, NotImplemented),
("__le__", core.TimePeriod(microseconds=900), False),
("__le__", core.TimePeriod(milliseconds=1), True),
("__le__", core.TimePeriod(microseconds=1100), True),
("__le__", 1000, NotImplemented),
("__le__", "1000", NotImplemented),
("__le__", True, NotImplemented),
("__le__", object(), NotImplemented),
("__le__", None, NotImplemented),
("__ge__", core.TimePeriod(microseconds=900), True),
("__ge__", core.TimePeriod(milliseconds=1), True),
("__ge__", core.TimePeriod(microseconds=1100), False),
("__ge__", 1000, NotImplemented),
("__ge__", "1000", NotImplemented),
("__ge__", True, NotImplemented),
("__ge__", object(), NotImplemented),
("__ge__", None, NotImplemented),
))
def test_comparison(self, comparison, other, expected):
target = core.TimePeriod(microseconds=1000)
actual = getattr(target, comparison)(other)
assert actual == expected
SAMPLE_LAMBDA = """
it.strftime(64, 0, id(my_font), TextAlign::TOP_CENTER, "%H:%M:%S", id(esptime).now());
it.printf(64, 16, id(my_font2), TextAlign::TOP_CENTER, "%.1f°C (%.1f%%)", id( office_tmp ).state, id(office_hmd).state);
"""
class TestLambda:
def test_init__copy_initializer(self):
value = core.Lambda("foo")
target = core.Lambda(value)
assert str(target) is value.value
def test_parts(self):
target = core.Lambda(SAMPLE_LAMBDA.strip())
# Check cache
assert target._parts is None
actual = target.parts
assert target._parts is actual
assert target.parts is actual
assert actual == [
"it.strftime(64, 0, ",
"my_font",
"",
", TextAlign::TOP_CENTER, \"%H:%M:%S\", ",
"esptime",
".",
"now());\nit.printf(64, 16, ",
"my_font2",
"",
", TextAlign::TOP_CENTER, \"%.1f°C (%.1f%%)\", ",
"office_tmp",
".",
"state, ",
"office_hmd",
".",
"state);"
]
def test_requires_ids(self):
target = core.Lambda(SAMPLE_LAMBDA.strip())
# Check cache
assert target._requires_ids is None
actual = target.requires_ids
assert target._requires_ids is actual
assert target.requires_ids is actual
assert actual == [
core.ID("my_font"),
core.ID("esptime"),
core.ID("my_font2"),
core.ID("office_tmp"),
core.ID("office_hmd"),
]
def test_value_setter(self):
target = core.Lambda("")
# Populate cache
_ = target.parts
_ = target.requires_ids
target.value = SAMPLE_LAMBDA
# Check cache has been cleared
assert target._parts is None
assert target._requires_ids is None
assert target.value == SAMPLE_LAMBDA
def test_repr(self):
target = core.Lambda("id(var).value == 1")
assert repr(target) == "Lambda<id(var).value == 1>"
class TestID:
@pytest.fixture
def target(self):
return core.ID(None, is_declaration=True, type="binary_sensor::Example")
@pytest.mark.parametrize("id, is_manual, expected", (
("foo", None, True),
(None, None, False),
("foo", True, True),
("foo", False, False),
(None, True, True),
))
def test_init__resolve_is_manual(self, id, is_manual, expected):
target = core.ID(id, is_manual=is_manual)
assert target.is_manual == expected
@pytest.mark.parametrize("registered_ids, expected", (
([], "binary_sensor_example"),
(["binary_sensor_example"], "binary_sensor_example_2"),
(["foo"], "binary_sensor_example"),
(["binary_sensor_example", "foo", "binary_sensor_example_2"], "binary_sensor_example_3"),
))
def test_resolve(self, target, registered_ids, expected):
actual = target.resolve(registered_ids)
assert actual == expected
assert str(target) == expected
def test_copy(self, target):
target.resolve([])
actual = target.copy()
assert actual is not target
assert all(getattr(actual, n) == getattr(target, n)
for n in ("id", "is_declaration", "type", "is_manual"))
@pytest.mark.parametrize("comparison, other, expected", (
("__eq__", core.ID(id="foo"), True),
("__eq__", core.ID(id="bar"), False),
("__eq__", 1000, NotImplemented),
("__eq__", "1000", NotImplemented),
("__eq__", True, NotImplemented),
("__eq__", object(), NotImplemented),
("__eq__", None, NotImplemented),
))
def test_comparison(self, comparison, other, expected):
target = core.ID(id="foo")
actual = getattr(target, comparison)(other)
assert actual == expected
class TestDocumentLocation:
@pytest.fixture
def target(self):
return core.DocumentLocation(
document="foo.txt",
line=10,
column=20,
)
def test_str(self, target):
actual = str(target)
assert actual == "foo.txt 10:20"
class TestDocumentRange:
@pytest.fixture
def target(self):
return core.DocumentRange(
core.DocumentLocation(
document="foo.txt",
line=10,
column=20,
),
core.DocumentLocation(
document="foo.txt",
line=15,
column=12,
),
)
def test_str(self, target):
actual = str(target)
assert actual == "[foo.txt 10:20 - foo.txt 15:12]"
class TestDefine:
@pytest.mark.parametrize("name, value, prop, expected", (
("ANSWER", None, "as_build_flag", "-DANSWER"),
("ANSWER", None, "as_macro", "#define ANSWER"),
("ANSWER", None, "as_tuple", ("ANSWER", None)),
("ANSWER", 42, "as_build_flag", "-DANSWER=42"),
("ANSWER", 42, "as_macro", "#define ANSWER 42"),
("ANSWER", 42, "as_tuple", ("ANSWER", 42)),
))
def test_properties(self, name, value, prop, expected):
target = core.Define(name, value)
actual = getattr(target, prop)
assert actual == expected
@pytest.mark.parametrize("comparison, other, expected", (
("__eq__", core.Define(name="FOO", value=42), True),
("__eq__", core.Define(name="FOO", value=13), False),
("__eq__", core.Define(name="FOO"), False),
("__eq__", core.Define(name="BAR", value=42), False),
("__eq__", core.Define(name="BAR"), False),
("__eq__", 1000, NotImplemented),
("__eq__", "1000", NotImplemented),
("__eq__", True, NotImplemented),
("__eq__", object(), NotImplemented),
("__eq__", None, NotImplemented),
))
def test_comparison(self, comparison, other, expected):
target = core.Define(name="FOO", value=42)
actual = getattr(target, comparison)(other)
assert actual == expected
class TestLibrary:
@pytest.mark.parametrize("name, value, prop, expected", (
("mylib", None, "as_lib_dep", "mylib"),
("mylib", None, "as_tuple", ("mylib", None)),
("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"),
("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")),
))
def test_properties(self, name, value, prop, expected):
target = core.Library(name, value)
actual = getattr(target, prop)
assert actual == expected
@pytest.mark.parametrize("comparison, other, expected", (
("__eq__", core.Library(name="libfoo", version="1.2.3"), True),
("__eq__", core.Library(name="libfoo", version="1.2.4"), False),
("__eq__", core.Library(name="libbar", version="1.2.3"), False),
("__eq__", 1000, NotImplemented),
("__eq__", "1000", NotImplemented),
("__eq__", True, NotImplemented),
("__eq__", object(), NotImplemented),
("__eq__", None, NotImplemented),
))
def test_comparison(self, comparison, other, expected):
target = core.Library(name="libfoo", version="1.2.3")
actual = getattr(target, comparison)(other)
assert actual == expected
class TestEsphomeCore:
@pytest.fixture
def target(self, fixture_path):
target = core.EsphomeCore()
target.build_path = "foo/build"
target.config_path = "foo/config"
return target
def test_reset(self, target):
"""Call reset on target and compare to new instance"""
other = core.EsphomeCore()
target.reset()
# TODO: raw_config and config differ, should they?
assert target.__dict__ == other.__dict__
def test_address__none(self, target):
assert target.address is None
def test_address__wifi(self, target):
target.config[const.CONF_WIFI] = {const.CONF_USE_ADDRESS: "1.2.3.4"}
target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"}
assert target.address == "1.2.3.4"
def test_address__ethernet(self, target):
target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"}
assert target.address == "4.3.2.1"
def test_is_esp32(self, target):
target.esp_platform = "ESP32"
assert target.is_esp32 is True
assert target.is_esp8266 is False
def test_is_esp8266(self, target):
target.esp_platform = "ESP8266"
assert target.is_esp32 is False
assert target.is_esp8266 is True

208
tests/unit_tests/test_helpers.py

@ -0,0 +1,208 @@
import pytest
from hypothesis import given
from hypothesis.provisional import ip4_addr_strings
from esphome import helpers
@pytest.mark.parametrize("preferred_string, current_strings, expected", (
("foo", [], "foo"),
# TODO: Should this actually start at 1?
("foo", ["foo"], "foo_2"),
("foo", ("foo",), "foo_2"),
("foo", ("foo", "foo_2"), "foo_3"),
("foo", ("foo", "foo_2", "foo_2"), "foo_3"),
))
def test_ensure_unique_string(preferred_string, current_strings, expected):
actual = helpers.ensure_unique_string(preferred_string, current_strings)
assert actual == expected
@pytest.mark.parametrize("text, expected", (
("foo", "foo"),
("foo\nbar", "foo\nbar"),
("foo\nbar\neek", "foo\n bar\neek"),
))
def test_indent_all_but_first_and_last(text, expected):
actual = helpers.indent_all_but_first_and_last(text)
assert actual == expected
@pytest.mark.parametrize("text, expected", (
("foo", [" foo"]),
("foo\nbar", [" foo", " bar"]),
("foo\nbar\neek", [" foo", " bar", " eek"]),
))
def test_indent_list(text, expected):
actual = helpers.indent_list(text)
assert actual == expected
@pytest.mark.parametrize("text, expected", (
("foo", " foo"),
("foo\nbar", " foo\n bar"),
("foo\nbar\neek", " foo\n bar\n eek"),
))
def test_indent(text, expected):
actual = helpers.indent(text)
assert actual == expected
@pytest.mark.parametrize("string, expected", (
("foo", '"foo"'),
("foo\nbar", '"foo\\012bar"'),
("foo\\bar", '"foo\\134bar"'),
('foo "bar"', '"foo \\042bar\\042"'),
('foo 🐍', '"foo \\360\\237\\220\\215"'),
))
def test_cpp_string_escape(string, expected):
actual = helpers.cpp_string_escape(string)
assert actual == expected
@pytest.mark.parametrize("host", (
"127.0.0", "localhost", "127.0.0.b",
))
def test_is_ip_address__invalid(host):
actual = helpers.is_ip_address(host)
assert actual is False
@given(value=ip4_addr_strings())
def test_is_ip_address__valid(value):
actual = helpers.is_ip_address(value)
assert actual is True
@pytest.mark.parametrize("var, value, default, expected", (
("FOO", None, False, False),
("FOO", None, True, True),
("FOO", "", False, False),
("FOO", "Yes", False, True),
("FOO", "123", False, True),
))
def test_get_bool_env(monkeypatch, var, value, default, expected):
if value is None:
monkeypatch.delenv(var, raising=False)
else:
monkeypatch.setenv(var, value)
actual = helpers.get_bool_env(var, default)
assert actual == expected
@pytest.mark.parametrize("value, expected", (
(None, False),
("Yes", True)
))
def test_is_hassio(monkeypatch, value, expected):
if value is None:
monkeypatch.delenv("ESPHOME_IS_HASSIO", raising=False)
else:
monkeypatch.setenv("ESPHOME_IS_HASSIO", value)
actual = helpers.is_hassio()
assert actual == expected
def test_walk_files(fixture_path):
path = fixture_path / "helpers"
actual = list(helpers.walk_files(path))
# Ensure paths start with the root
assert all(p.startswith(path.as_posix()) for p in actual)
class Test_write_file_if_changed:
def test_src_and_dst_match(self, tmp_path):
text = "A files are unique.\n"
initial = text
dst = tmp_path / "file-a.txt"
dst.write_text(initial)
helpers.write_file_if_changed(dst, text)
assert dst.read_text() == text
def test_src_and_dst_do_not_match(self, tmp_path):
text = "A files are unique.\n"
initial = "B files are unique.\n"
dst = tmp_path / "file-a.txt"
dst.write_text(initial)
helpers.write_file_if_changed(dst, text)
assert dst.read_text() == text
def test_dst_does_not_exist(self, tmp_path):
text = "A files are unique.\n"
dst = tmp_path / "file-a.txt"
helpers.write_file_if_changed(dst, text)
assert dst.read_text() == text
class Test_copy_file_if_changed:
def test_src_and_dst_match(self, tmp_path, fixture_path):
src = fixture_path / "helpers" / "file-a.txt"
initial = fixture_path / "helpers" / "file-a.txt"
dst = tmp_path / "file-a.txt"
dst.write_text(initial.read_text())
helpers.copy_file_if_changed(src, dst)
def test_src_and_dst_do_not_match(self, tmp_path, fixture_path):
src = fixture_path / "helpers" / "file-a.txt"
initial = fixture_path / "helpers" / "file-c.txt"
dst = tmp_path / "file-a.txt"
dst.write_text(initial.read_text())
helpers.copy_file_if_changed(src, dst)
assert src.read_text() == dst.read_text()
def test_dst_does_not_exist(self, tmp_path, fixture_path):
src = fixture_path / "helpers" / "file-a.txt"
dst = tmp_path / "file-a.txt"
helpers.copy_file_if_changed(src, dst)
assert dst.exists()
assert src.read_text() == dst.read_text()
@pytest.mark.parametrize("file1, file2, expected", (
# Same file
("file-a.txt", "file-a.txt", True),
# Different files, different size
("file-a.txt", "file-b_1.txt", False),
# Different files, same size
("file-a.txt", "file-c.txt", False),
# Same files
("file-b_1.txt", "file-b_2.txt", True),
# Not a file
("file-a.txt", "", False),
# File doesn't exist
("file-a.txt", "file-d.txt", False),
))
def test_file_compare(fixture_path, file1, file2, expected):
path1 = fixture_path / "helpers" / file1
path2 = fixture_path / "helpers" / file2
actual = helpers.file_compare(path1, path2)
assert actual == expected

326
tests/unit_tests/test_pins.py

@ -0,0 +1,326 @@
"""
Please Note:
These tests cover the process of identifying information about pins, they do not
check if the definition of MCUs and pins is correct.
"""
import logging
import pytest
from esphome.config_validation import Invalid
from esphome.core import EsphomeCore
from esphome import pins
MOCK_ESP8266_BOARD_ID = "_mock_esp8266"
MOCK_ESP8266_PINS = {'X0': 16, 'X1': 5, 'X2': 4, 'LED': 2}
MOCK_ESP8266_BOARD_ALIAS_ID = "_mock_esp8266_alias"
MOCK_ESP8266_FLASH_SIZE = pins.FLASH_SIZE_2_MB
MOCK_ESP32_BOARD_ID = "_mock_esp32"
MOCK_ESP32_PINS = {'Y0': 12, 'Y1': 8, 'Y2': 3, 'LED': 9, "A0": 8}
MOCK_ESP32_BOARD_ALIAS_ID = "_mock_esp32_alias"
UNKNOWN_PLATFORM = "STM32"
@pytest.fixture
def mock_mcu(monkeypatch):
"""
Add a mock MCU into the lists as a stable fixture
"""
pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS
pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE
pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID
pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE
pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS
pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID
yield
del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID]
del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID]
del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID]
del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID]
del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID]
del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID]
@pytest.fixture
def core(monkeypatch, mock_mcu):
core = EsphomeCore()
monkeypatch.setattr(pins, "CORE", core)
return core
@pytest.fixture
def core_esp8266(core):
core.esp_platform = "ESP8266"
core.board = MOCK_ESP8266_BOARD_ID
return core
@pytest.fixture
def core_esp32(core):
core.esp_platform = "ESP32"
core.board = MOCK_ESP32_BOARD_ID
return core
class Test_lookup_pin:
@pytest.mark.parametrize("value, expected", (
("X1", 5),
("MOSI", 13),
))
def test_valid_esp8266_pin(self, core_esp8266, value, expected):
actual = pins._lookup_pin(value)
assert actual == expected
def test_valid_esp8266_pin_alias(self, core_esp8266):
core_esp8266.board = MOCK_ESP8266_BOARD_ALIAS_ID
actual = pins._lookup_pin("X2")
assert actual == 4
@pytest.mark.parametrize("value, expected", (
("Y1", 8),
("A0", 8),
("MOSI", 23),
))
def test_valid_esp32_pin(self, core_esp32, value, expected):
actual = pins._lookup_pin(value)
assert actual == expected
@pytest.mark.xfail(reason="This may be expected")
def test_valid_32_pin_alias(self, core_esp32):
core_esp32.board = MOCK_ESP32_BOARD_ALIAS_ID
actual = pins._lookup_pin("Y2")
assert actual == 3
def test_invalid_pin(self, core_esp8266):
with pytest.raises(Invalid, match="Cannot resolve pin name 'X42' for board _mock_esp8266."):
pins._lookup_pin("X42")
def test_unsupported_platform(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins._lookup_pin("TX")
class Test_translate_pin:
@pytest.mark.parametrize("value, expected", (
(2, 2),
("3", 3),
("GPIO4", 4),
("TX", 1),
("Y0", 12),
))
def test_valid_values(self, core_esp32, value, expected):
actual = pins._translate_pin(value)
assert actual == expected
@pytest.mark.parametrize("value", ({}, None))
def test_invalid_values(self, core_esp32, value):
with pytest.raises(Invalid, match="This variable only supports"):
pins._translate_pin(value)
class Test_validate_gpio_pin:
def test_esp32_valid(self, core_esp32):
actual = pins.validate_gpio_pin("GPIO22")
assert actual == 22
@pytest.mark.parametrize("value, match", (
(-1, "ESP32: Invalid pin number: -1"),
(40, "ESP32: Invalid pin number: 40"),
(6, "This pin cannot be used on ESP32s and"),
(7, "This pin cannot be used on ESP32s and"),
(8, "This pin cannot be used on ESP32s and"),
(11, "This pin cannot be used on ESP32s and"),
(20, "The pin GPIO20 is not usable on ESP32s"),
(24, "The pin GPIO24 is not usable on ESP32s"),
(28, "The pin GPIO28 is not usable on ESP32s"),
(29, "The pin GPIO29 is not usable on ESP32s"),
(30, "The pin GPIO30 is not usable on ESP32s"),
(31, "The pin GPIO31 is not usable on ESP32s"),
))
def test_esp32_invalid_pin(self, core_esp32, value, match):
with pytest.raises(Invalid, match=match):
pins.validate_gpio_pin(value)
@pytest.mark.parametrize("value", (9, 10))
def test_esp32_warning(self, core_esp32, caplog, value):
caplog.at_level(logging.WARNING)
pins.validate_gpio_pin(value)
assert len(caplog.messages) == 1
assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.")
def test_esp8266_valid(self, core_esp8266):
actual = pins.validate_gpio_pin("GPIO12")
assert actual == 12
@pytest.mark.parametrize("value, match", (
(-1, "ESP8266: Invalid pin number: -1"),
(18, "ESP8266: Invalid pin number: 18"),
(6, "This pin cannot be used on ESP8266s and"),
(7, "This pin cannot be used on ESP8266s and"),
(8, "This pin cannot be used on ESP8266s and"),
(11, "This pin cannot be used on ESP8266s and"),
))
def test_esp8266_invalid_pin(self, core_esp8266, value, match):
with pytest.raises(Invalid, match=match):
pins.validate_gpio_pin(value)
@pytest.mark.parametrize("value", (9, 10))
def test_esp8266_warning(self, core_esp8266, caplog, value):
caplog.at_level(logging.WARNING)
pins.validate_gpio_pin(value)
assert len(caplog.messages) == 1
assert caplog.messages[0].endswith("flash interface in QUAD IO flash mode.")
def test_unknown_device(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins.validate_gpio_pin("0")
class Test_input_pin:
@pytest.mark.parametrize("value, expected", (
("X0", 16),
))
def test_valid_esp8266_values(self, core_esp8266, value, expected):
actual = pins.input_pin(value)
assert actual == expected
@pytest.mark.parametrize("value, expected", (
("Y0", 12),
(17, 17),
))
def test_valid_esp32_values(self, core_esp32, value, expected):
actual = pins.input_pin(value)
assert actual == expected
@pytest.mark.parametrize("value", (17,))
def test_invalid_esp8266_values(self, core_esp8266, value):
with pytest.raises(Invalid):
pins.input_pin(value)
def test_unknown_platform(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins.input_pin(2)
class Test_input_pullup_pin:
@pytest.mark.parametrize("value, expected", (
("X0", 16),
))
def test_valid_esp8266_values(self, core_esp8266, value, expected):
actual = pins.input_pullup_pin(value)
assert actual == expected
@pytest.mark.parametrize("value, expected", (
("Y0", 12),
(17, 17),
))
def test_valid_esp32_values(self, core_esp32, value, expected):
actual = pins.input_pullup_pin(value)
assert actual == expected
@pytest.mark.parametrize("value", (0,))
def test_invalid_esp8266_values(self, core_esp8266, value):
with pytest.raises(Invalid):
pins.input_pullup_pin(value)
def test_unknown_platform(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins.input_pullup_pin(2)
class Test_output_pin:
@pytest.mark.parametrize("value, expected", (
("X0", 16),
))
def test_valid_esp8266_values(self, core_esp8266, value, expected):
actual = pins.output_pin(value)
assert actual == expected
@pytest.mark.parametrize("value, expected", (
("Y0", 12),
(17, 17),
))
def test_valid_esp32_values(self, core_esp32, value, expected):
actual = pins.output_pin(value)
assert actual == expected
@pytest.mark.parametrize("value", (17,))
def test_invalid_esp8266_values(self, core_esp8266, value):
with pytest.raises(Invalid):
pins.output_pin(value)
@pytest.mark.parametrize("value", range(34, 40))
def test_invalid_esp32_values(self, core_esp32, value):
with pytest.raises(Invalid):
pins.output_pin(value)
def test_unknown_platform(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins.output_pin(2)
class Test_analog_pin:
@pytest.mark.parametrize("value, expected", (
(17, 17),
))
def test_valid_esp8266_values(self, core_esp8266, value, expected):
actual = pins.analog_pin(value)
assert actual == expected
@pytest.mark.parametrize("value, expected", (
(32, 32),
(39, 39),
))
def test_valid_esp32_values(self, core_esp32, value, expected):
actual = pins.analog_pin(value)
assert actual == expected
@pytest.mark.parametrize("value", ("X0",))
def test_invalid_esp8266_values(self, core_esp8266, value):
with pytest.raises(Invalid):
pins.analog_pin(value)
@pytest.mark.parametrize("value", ("Y0",))
def test_invalid_esp32_values(self, core_esp32, value):
with pytest.raises(Invalid):
pins.analog_pin(value)
def test_unknown_platform(self, core):
core.esp_platform = UNKNOWN_PLATFORM
with pytest.raises(NotImplementedError):
pins.analog_pin(2)
Loading…
Cancel
Save