Tests for CPP Code generation and some Python3 improvements (#961)

* Basic pytest configuration

* Added unit_test script that triggers pytest

* Changed "fixtures" to fixture_path

This is consistent with pytest's tmp_path

* Initial unit tests for esphome.helpers

* Disabled coverage reporting for esphome/components.

Focus initial unittest efforts on the core code.

* Migrated some ip_address to hypothesis

* Added a hypothesis MAC address strategy

* Initial tests for core

* Added hypothesis to requirements

* Added tests for core classes

TestTimePeriod
Lambda
ID
DocumentLocation
DocumentRange
Define
Library

* Updated test config so package root is discovered

* Setup fixtures and inital tests for pins

* Added tests for validate GPIO

* Added tests for pin type

* Added initial config_validation tests

* Added more tests for config_validation

* Added comparison unit tests

* Added repr to core.TimePeriod. Simplified identifying faults in tests

* Fixed inverted gt/lt tests

* Some tests for Espcore

* Updated syntax for Python3

* Removed usage of kwarg that isn't required

* Started writing test cases

* Started writing test cases for cpp_generator

* Additional docs and more Python3 releated improvements

* More test cases for cpp_generator.

* Fixed linter errors

* Add codegen tests to ensure file API remains stable

* Add test cases for cpp_helpers
This commit is contained in:
Tim Savage 2020-04-20 10:05:58 +10:00 committed by GitHub
parent 269812e781
commit d447548893
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 574 additions and 170 deletions

View File

@ -30,7 +30,7 @@ def to_code(config):
initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE])
rhs = GlobalsComponent.new(template_args, initial_value)
glob = cg.Pvariable(config[CONF_ID], rhs, type=res_type)
glob = cg.Pvariable(config[CONF_ID], rhs, res_type)
yield cg.register_component(glob, config)
if config[CONF_RESTORE_VALUE]:

View File

@ -169,7 +169,7 @@ def to_code(config):
else:
out_type = NeoPixelRGBLightOutput.template(template)
rhs = out_type.new()
var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, type=out_type)
var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type)
yield light.register_light(var, config)
yield cg.register_component(var, config)

View File

@ -57,10 +57,10 @@ def to_code(config):
model_type, model = MODELS[config[CONF_MODEL]]
if model_type == 'a':
rhs = WaveshareEPaperTypeA.new(model)
var = cg.Pvariable(config[CONF_ID], rhs, type=WaveshareEPaperTypeA)
var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA)
elif model_type == 'b':
rhs = model.new()
var = cg.Pvariable(config[CONF_ID], rhs, type=model)
var = cg.Pvariable(config[CONF_ID], rhs, model)
else:
raise NotImplementedError()

View File

@ -1,9 +1,9 @@
import abc
import inspect
import math
# pylint: disable=unused-import, wrong-import-order
from typing import Any, Generator, List, Optional, Tuple, Type, Union, Dict, Callable # noqa
from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence
from esphome.core import ( # noqa
CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds,
@ -13,29 +13,35 @@ from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
from esphome.util import OrderedDict
class Expression:
class Expression(abc.ABC):
__slots__ = ()
@abc.abstractmethod
def __str__(self):
raise NotImplementedError
"""
Convert expression into C++ code
"""
SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod,
Type[bool], Type[int], Type[float], List[Any]]
Type[bool], Type[int], Type[float], Sequence[Any]]
class RawExpression(Expression):
def __init__(self, text): # type: (Union[str, str]) -> None
super().__init__()
__slots__ = ("text", )
def __init__(self, text: str):
self.text = text
def __str__(self):
return str(self.text)
return self.text
# pylint: disable=redefined-builtin
class AssignmentExpression(Expression):
def __init__(self, type, modifier, name, rhs, obj):
super().__init__()
self.type = type
__slots__ = ("type", "modifier", "name", "rhs", "obj")
def __init__(self, type_, modifier, name, rhs, obj):
self.type = type_
self.modifier = modifier
self.name = name
self.rhs = safe_exp(rhs)
@ -48,9 +54,10 @@ class AssignmentExpression(Expression):
class VariableDeclarationExpression(Expression):
def __init__(self, type, modifier, name):
super().__init__()
self.type = type
__slots__ = ("type", "modifier", "name")
def __init__(self, type_, modifier, name):
self.type = type_
self.modifier = modifier
self.name = name
@ -59,8 +66,9 @@ class VariableDeclarationExpression(Expression):
class ExpressionList(Expression):
def __init__(self, *args):
super().__init__()
__slots__ = ("args", )
def __init__(self, *args: Optional[SafeExpType]):
# Remove every None on end
args = list(args)
while args and args[-1] is None:
@ -76,8 +84,9 @@ class ExpressionList(Expression):
class TemplateArguments(Expression):
def __init__(self, *args): # type: (*SafeExpType) -> None
super().__init__()
__slots__ = ("args", )
def __init__(self, *args: SafeExpType):
self.args = ExpressionList(*args)
def __str__(self):
@ -88,8 +97,9 @@ class TemplateArguments(Expression):
class CallExpression(Expression):
def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None
super().__init__()
__slots__ = ("base", "template_args", "args")
def __init__(self, base: Expression, *args: SafeExpType):
self.base = base
if args and isinstance(args[0], TemplateArguments):
self.template_args = args[0]
@ -105,9 +115,11 @@ class CallExpression(Expression):
class StructInitializer(Expression):
def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None
super().__init__()
__slots__ = ("base", "args")
def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]):
self.base = base
# TODO: args is always a Tuple, is this check required?
if not isinstance(args, OrderedDict):
args = OrderedDict(args)
self.args = OrderedDict()
@ -126,9 +138,10 @@ class StructInitializer(Expression):
class ArrayInitializer(Expression):
def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None
super().__init__()
self.multiline = kwargs.get('multiline', False)
__slots__ = ("multiline", "args")
def __init__(self, *args: Any, multiline: bool = False):
self.multiline = multiline
self.args = []
for arg in args:
if arg is None:
@ -150,18 +163,20 @@ class ArrayInitializer(Expression):
class ParameterExpression(Expression):
def __init__(self, type, id):
super().__init__()
self.type = safe_exp(type)
self.id = id
__slots__ = ("type", "id")
def __init__(self, type_, id_):
self.type = safe_exp(type_)
self.id = id_
def __str__(self):
return f"{self.type} {self.id}"
class ParameterListExpression(Expression):
def __init__(self, *parameters):
super().__init__()
__slots__ = ("parameters", )
def __init__(self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]]):
self.parameters = []
for parameter in parameters:
if not isinstance(parameter, ParameterExpression):
@ -173,8 +188,9 @@ class ParameterListExpression(Expression):
class LambdaExpression(Expression):
def __init__(self, parts, parameters, capture='=', return_type=None):
super().__init__()
__slots__ = ("parts", "parameters", "capture", "return_type")
def __init__(self, parts, parameters, capture: str = '=', return_type=None):
self.parts = parts
if not isinstance(parameters, ParameterListExpression):
parameters = ParameterListExpression(*parameters)
@ -194,23 +210,25 @@ class LambdaExpression(Expression):
return ''.join(str(part) for part in self.parts)
class Literal(Expression):
def __str__(self):
raise NotImplementedError
# pylint: disable=abstract-method
class Literal(Expression, metaclass=abc.ABCMeta):
__slots__ = ()
class StringLiteral(Literal):
def __init__(self, string): # type: (Union[str, str]) -> None
super().__init__()
__slots__ = ("string", )
def __init__(self, string: str):
self.string = string
def __str__(self):
return '{}'.format(cpp_string_escape(self.string))
return cpp_string_escape(self.string)
class IntLiteral(Literal):
def __init__(self, i): # type: (Union[int]) -> None
super().__init__()
__slots__ = ("i", )
def __init__(self, i: int):
self.i = i
def __str__(self):
@ -224,7 +242,9 @@ class IntLiteral(Literal):
class BoolLiteral(Literal):
def __init__(self, binary): # type: (bool) -> None
__slots__ = ("binary", )
def __init__(self, binary: bool):
super().__init__()
self.binary = binary
@ -233,8 +253,9 @@ class BoolLiteral(Literal):
class HexIntLiteral(Literal):
def __init__(self, i): # type: (int) -> None
super().__init__()
__slots__ = ("i", )
def __init__(self, i: int):
self.i = HexInt(i)
def __str__(self):
@ -242,21 +263,18 @@ class HexIntLiteral(Literal):
class FloatLiteral(Literal):
def __init__(self, value): # type: (float) -> None
super().__init__()
self.float_ = value
__slots__ = ("f", )
def __init__(self, value: float):
self.f = value
def __str__(self):
if math.isnan(self.float_):
if math.isnan(self.f):
return "NAN"
return f"{self.float_}f"
return f"{self.f}f"
# pylint: disable=bad-continuation
def safe_exp(
obj # type: Union[Expression, bool, str, int, float, TimePeriod, list]
):
# type: (...) -> Expression
def safe_exp(obj: SafeExpType) -> Expression:
"""Try to convert obj to an expression by automatically converting native python types to
expressions/literals.
"""
@ -301,17 +319,20 @@ def safe_exp(
raise ValueError("Object is not an expression", obj)
class Statement:
def __init__(self):
pass
class Statement(abc.ABC):
__slots__ = ()
@abc.abstractmethod
def __str__(self):
raise NotImplementedError
"""
Convert statement into C++ code
"""
class RawStatement(Statement):
def __init__(self, text):
super().__init__()
__slots__ = ("text", )
def __init__(self, text: str):
self.text = text
def __str__(self):
@ -319,8 +340,9 @@ class RawStatement(Statement):
class ExpressionStatement(Statement):
__slots__ = ("expression", )
def __init__(self, expression):
super().__init__()
self.expression = safe_exp(expression)
def __str__(self):
@ -328,115 +350,105 @@ class ExpressionStatement(Statement):
class LineComment(Statement):
def __init__(self, value): # type: (str) -> None
super().__init__()
self._value = value
__slots__ = ("value", )
def __init__(self, value: str):
self.value = value
def __str__(self):
parts = self._value.split('\n')
parts = self.value.split('\n')
parts = [f'// {x}' for x in parts]
return '\n'.join(parts)
class ProgmemAssignmentExpression(AssignmentExpression):
def __init__(self, type, name, rhs, obj):
super().__init__(
type, '', name, rhs, obj
)
__slots__ = ()
def __init__(self, type_, name, rhs, obj):
super().__init__(type_, '', name, rhs, obj)
def __str__(self):
type_ = self.type
return f"static const {type_} {self.name}[] PROGMEM = {self.rhs}"
return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}"
def progmem_array(id, rhs):
def progmem_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id, '.')
assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj)
obj = MockObj(id_, '.')
assignment = ProgmemAssignmentExpression(id_.type, id_, rhs, obj)
CORE.add(assignment)
CORE.register_variable(id, obj)
CORE.register_variable(id_, obj)
return obj
def statement(expression): # type: (Union[Expression, Statement]) -> Statement
def statement(expression: Union[Expression, Statement]) -> Statement:
"""Convert expression into a statement unless is already a statement.
"""
if isinstance(expression, Statement):
return expression
return ExpressionStatement(expression)
def variable(id, # type: ID
rhs, # type: SafeExpType
type=None # type: MockObj
):
# type: (...) -> MockObj
def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
"""Declare a new variable (not pointer type) in the code generation.
:param id: The ID used to declare the variable.
:param id_: The ID used to declare the variable.
:param rhs: The expression to place on the right hand side of the assignment.
:param type: Manually define a type for the variable, only use this when it's not possible
:param type_: Manually define a type for the variable, only use this when it's not possible
to do so during config validation phase (for example because of template arguments).
:returns The new variable as a MockObj.
"""
assert isinstance(id, ID)
assert isinstance(id_, ID)
rhs = safe_exp(rhs)
obj = MockObj(id, '.')
if type is not None:
id.type = type
assignment = AssignmentExpression(id.type, '', id, rhs, obj)
obj = MockObj(id_, '.')
if type_ is not None:
id_.type = type_
assignment = AssignmentExpression(id_.type, '', id_, rhs, obj)
CORE.add(assignment)
CORE.register_variable(id, obj)
CORE.register_variable(id_, obj)
return obj
def Pvariable(id, # type: ID
rhs, # type: SafeExpType
type=None # type: MockObj
):
# type: (...) -> MockObj
def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
"""Declare a new pointer variable in the code generation.
:param id: The ID used to declare the variable.
:param id_: The ID used to declare the variable.
:param rhs: The expression to place on the right hand side of the assignment.
:param type: Manually define a type for the variable, only use this when it's not possible
:param type_: Manually define a type for the variable, only use this when it's not possible
to do so during config validation phase (for example because of template arguments).
:returns The new variable as a MockObj.
"""
rhs = safe_exp(rhs)
obj = MockObj(id, '->')
if type is not None:
id.type = type
decl = VariableDeclarationExpression(id.type, '*', id)
obj = MockObj(id_, '->')
if type_ is not None:
id_.type = type_
decl = VariableDeclarationExpression(id_.type, '*', id_)
CORE.add_global(decl)
assignment = AssignmentExpression(None, None, id, rhs, obj)
assignment = AssignmentExpression(None, None, id_, rhs, obj)
CORE.add(assignment)
CORE.register_variable(id, obj)
CORE.register_variable(id_, obj)
return obj
def new_Pvariable(id, # type: ID
*args # type: *SafeExpType
):
def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
"""Declare a new pointer variable in the code generation by calling it's constructor
with the given arguments.
:param id: The ID used to declare the variable (also specifies the type).
:param id_: The ID used to declare the variable (also specifies the type).
:param args: The values to pass to the constructor.
:returns The new variable as a MockObj.
"""
if args and isinstance(args[0], TemplateArguments):
id = id.copy()
id.type = id.type.template(args[0])
id_ = id_.copy()
id_.type = id_.type.template(args[0])
args = args[1:]
rhs = id.type.new(*args)
return Pvariable(id, rhs)
rhs = id_.type.new(*args)
return Pvariable(id_, rhs)
def add(expression, # type: Union[Expression, Statement]
):
# type: (...) -> None
def add(expression: Union[Expression, Statement]):
"""Add an expression to the codegen section.
After this is called, the given given expression will
@ -445,17 +457,12 @@ def add(expression, # type: Union[Expression, Statement]
CORE.add(expression)
def add_global(expression, # type: Union[SafeExpType, Statement]
):
# type: (...) -> None
def add_global(expression: Union[SafeExpType, Statement]):
"""Add an expression to the codegen global storage (above setup())."""
CORE.add_global(expression)
def add_library(name, # type: str
version # type: Optional[str]
):
# type: (...) -> None
def add_library(name: str, version: Optional[str]):
"""Add a library to the codegen library storage.
:param name: The name of the library (for example 'AsyncTCP')
@ -464,17 +471,12 @@ def add_library(name, # type: str
CORE.add_library(Library(name, version))
def add_build_flag(build_flag, # type: str
):
# type: (...) -> None
def add_build_flag(build_flag: str):
"""Add a global build flag to the compiler flags."""
CORE.add_build_flag(build_flag)
def add_define(name, # type: str
value=None, # type: Optional[SafeExpType]
):
# type: (...) -> None
def add_define(name: str, value: SafeExpType = None):
"""Add a global define to the auto-generated defines.h file.
Optionally define a value to set this define to.
@ -486,42 +488,40 @@ def add_define(name, # type: str
@coroutine
def get_variable(id): # type: (ID) -> Generator[MockObj]
def get_variable(id_: ID) -> Generator["MockObj", None, None]:
"""
Wait for the given ID to be defined in the code generation and
return it as a MockObj.
This is a coroutine, you need to await it with a 'yield' expression!
:param id: The ID to retrieve
:param id_: The ID to retrieve
:return: The variable as a MockObj.
"""
var = yield CORE.get_variable(id)
var = yield CORE.get_variable(id_)
yield var
@coroutine
def get_variable_with_full_id(id): # type: (ID) -> Generator[ID, MockObj]
def get_variable_with_full_id(id_: ID) -> Generator[Tuple[ID, "MockObj"], None, None]:
"""
Wait for the given ID to be defined in the code generation and
return it as a MockObj.
This is a coroutine, you need to await it with a 'yield' expression!
:param id: The ID to retrieve
:param id_: The ID to retrieve
:return: The variable as a MockObj.
"""
full_id, var = yield CORE.get_variable_with_full_id(id)
full_id, var = yield CORE.get_variable_with_full_id(id_)
yield full_id, var
@coroutine
def process_lambda(value, # type: Lambda
parameters, # type: List[Tuple[SafeExpType, str]]
capture='=', # type: str
return_type=None # type: Optional[SafeExpType]
):
# type: (...) -> Generator[LambdaExpression]
def process_lambda(
value: Lambda, parameters: List[Tuple[SafeExpType, str]],
capture: str = '=', return_type: SafeExpType = None
) -> Generator[LambdaExpression, None, None]:
"""Process the given lambda value into a LambdaExpression.
This is a coroutine because lambdas can depend on other IDs,
@ -560,11 +560,10 @@ def is_template(value):
@coroutine
def templatable(value, # type: Any
args, # type: List[Tuple[SafeExpType, str]]
output_type, # type: Optional[SafeExpType],
to_exp=None # type: Optional[Any]
):
def templatable(value: Any,
args: List[Tuple[SafeExpType, str]],
output_type: Optional[SafeExpType],
to_exp: Any = None):
"""Generate code for a templatable config option.
If `value` is a templated value, the lambda expression is returned.
@ -593,12 +592,13 @@ class MockObj(Expression):
Mostly consists of magic methods that allow ESPHome's codegen syntax.
"""
__slots__ = ("base", "op")
def __init__(self, base, op='.'):
self.base = base
self.op = op
super().__init__()
def __getattr__(self, attr): # type: (str) -> MockObj
def __getattr__(self, attr: str) -> "MockObj":
next_op = '.'
if attr.startswith('P') and self.op not in ['::', '']:
attr = attr[1:]
@ -611,55 +611,55 @@ class MockObj(Expression):
call = CallExpression(self.base, *args)
return MockObj(call, self.op)
def __str__(self): # type: () -> str
def __str__(self):
return str(self.base)
def __repr__(self):
return 'MockObj<{}>'.format(str(self.base))
@property
def _(self): # type: () -> MockObj
def _(self) -> "MockObj":
return MockObj(f'{self.base}{self.op}')
@property
def new(self): # type: () -> MockObj
def new(self) -> "MockObj":
return MockObj(f'new {self.base}', '->')
def template(self, *args): # type: (*SafeExpType) -> MockObj
def template(self, *args: SafeExpType) -> "MockObj":
if len(args) != 1 or not isinstance(args[0], TemplateArguments):
args = TemplateArguments(*args)
else:
args = args[0]
return MockObj(f'{self.base}{args}')
def namespace(self, name): # type: (str) -> MockObj
def namespace(self, name: str) -> "MockObj":
return MockObj(f'{self._}{name}', '::')
def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass
def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass":
op = '' if self.op == '' else '::'
return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents)
def struct(self, name): # type: (str) -> MockObjClass
def struct(self, name: str) -> "MockObjClass":
return self.class_(name)
def enum(self, name, is_class=False): # type: (str, bool) -> MockObj
def enum(self, name: str, is_class: bool = False) -> "MockObj":
return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op)
def operator(self, name): # type: (str) -> MockObj
def operator(self, name: str) -> "MockObj":
if name == 'ref':
return MockObj(f'{self.base} &', '')
if name == 'ptr':
return MockObj(f'{self.base} *', '')
if name == "const":
return MockObj(f'const {self.base}', '')
raise NotImplementedError
raise ValueError("Expected one of ref, ptr, const.")
@property
def using(self): # type: () -> MockObj
def using(self) -> "MockObj":
assert self.op == '::'
return MockObj(f'using namespace {self.base}')
def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj
def __getitem__(self, item: Union[str, Expression]) -> "MockObj":
next_op = '.'
if isinstance(item, str) and item.startswith('P'):
item = item[1:]
@ -678,13 +678,13 @@ class MockObjEnum(MockObj):
kwargs['base'] = base
MockObj.__init__(self, *args, **kwargs)
def __str__(self): # type: () -> str
def __str__(self):
if self._is_class:
return super().__str__()
return f'{self.base}{self.op}{self._enum}'
def __repr__(self):
return 'MockObj<{}>'.format(str(self.base))
return f'MockObj<{str(self.base)}>'
class MockObjClass(MockObj):
@ -699,7 +699,7 @@ class MockObjClass(MockObj):
# pylint: disable=protected-access
self._parents += paren._parents
def inherits_from(self, other): # type: (MockObjClass) -> bool
def inherits_from(self, other: "MockObjClass") -> bool:
if self == other:
return True
for parent in self._parents:
@ -707,8 +707,7 @@ class MockObjClass(MockObj):
return True
return False
def template(self, *args):
# type: (*SafeExpType) -> MockObjClass
def template(self, *args: SafeExpType) -> "MockObjClass":
if len(args) != 1 or not isinstance(args[0], TemplateArguments):
args = TemplateArguments(*args)
else:
@ -718,4 +717,4 @@ class MockObjClass(MockObj):
return MockObjClass(f'{self.base}{args}', parents=new_parents)
def __repr__(self):
return 'MockObjClass<{}, parents={}>'.format(str(self.base), self._parents)
return f'MockObjClass<{str(self.base)}, parents={self._parents}>'

View File

@ -21,4 +21,5 @@ pexpect
pytest==5.3.2
pytest-cov==2.8.1
pytest-mock==1.13.0
asyncmock==0.4.2
hypothesis==4.57.0

View File

@ -0,0 +1,26 @@
import pytest
from esphome import codegen as cg
# Test interface remains the same.
@pytest.mark.parametrize("attr", (
# from cpp_generator
"Expression", "RawExpression", "RawStatement", "TemplateArguments",
"StructInitializer", "ArrayInitializer", "safe_exp", "Statement", "LineComment",
"progmem_array", "statement", "variable", "Pvariable", "new_Pvariable",
"add", "add_global", "add_library", "add_build_flag", "add_define",
"get_variable", "get_variable_with_full_id", "process_lambda", "is_template", "templatable", "MockObj",
"MockObjClass",
# from cpp_helpers
"gpio_pin_expression", "register_component", "build_registry_entry",
"build_registry_list", "extract_registry_entry_config", "register_parented",
"global_ns", "void", "nullptr", "float_", "double", "bool_", "int_", "std_ns", "std_string",
"std_vector", "uint8", "uint16", "uint32", "int32", "const_char_ptr", "NAN",
"esphome_ns", "App", "Nameable", "Component", "ComponentPtr",
# from cpp_types
"PollingComponent", "Application", "optional", "arduino_json_ns", "JsonObject",
"JsonObjectRef", "JsonObjectConstRef", "Controller", "GPIOPin"
))
def test_exists(attr):
assert hasattr(cg, attr)

View File

@ -459,13 +459,13 @@ class TestEsphomeCore:
target.config_path = "foo/config"
return target
@pytest.mark.xfail(reason="raw_config and config differ, should they?")
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):

View File

@ -0,0 +1,293 @@
from typing import Iterator
import math
import pytest
from esphome import cpp_generator as cg
from esphome import cpp_types as ct
class TestExpressions:
@pytest.mark.parametrize("target, expected", (
(cg.RawExpression("foo && bar"), "foo && bar"),
(cg.AssignmentExpression(None, None, "foo", "bar", None), 'foo = "bar"'),
(cg.AssignmentExpression(ct.float_, "*", "foo", 1, None), 'float *foo = 1'),
(cg.AssignmentExpression(ct.float_, "", "foo", 1, None), 'float foo = 1'),
(cg.VariableDeclarationExpression(ct.int32, "*", "foo"), "int32_t *foo"),
(cg.VariableDeclarationExpression(ct.int32, "", "foo"), "int32_t foo"),
(cg.ParameterExpression(ct.std_string, "foo"), "std::string foo"),
))
def test_str__simple(self, target: cg.Expression, expected: str):
actual = str(target)
assert actual == expected
class TestExpressionList:
SAMPLE_ARGS = (1, "2", True, None, None)
def test_str(self):
target = cg.ExpressionList(*self.SAMPLE_ARGS)
actual = str(target)
assert actual == '1, "2", true'
def test_iter(self):
target = cg.ExpressionList(*self.SAMPLE_ARGS)
actual = iter(target)
assert isinstance(actual, Iterator)
assert len(tuple(actual)) == 3
class TestTemplateArguments:
SAMPLE_ARGS = (int, 1, "2", True, None, None)
def test_str(self):
target = cg.TemplateArguments(*self.SAMPLE_ARGS)
actual = str(target)
assert actual == '<int32_t, 1, "2", true>'
def test_iter(self):
target = cg.TemplateArguments(*self.SAMPLE_ARGS)
actual = iter(target)
assert isinstance(actual, Iterator)
assert len(tuple(actual)) == 4
class TestCallExpression:
def test_str__no_template_args(self):
target = cg.CallExpression(
cg.RawExpression("my_function"),
1, "2", False
)
actual = str(target)
assert actual == 'my_function(1, "2", false)'
def test_str__with_template_args(self):
target = cg.CallExpression(
cg.RawExpression("my_function"),
cg.TemplateArguments(int, float),
1, "2", False
)
actual = str(target)
assert actual == 'my_function<int32_t, float>(1, "2", false)'
class TestStructInitializer:
def test_str(self):
target = cg.StructInitializer(
cg.MockObjClass("foo::MyStruct", parents=()),
("state", "on"),
("min_length", 1),
("max_length", 5),
("foo", None),
)
actual = str(target)
assert actual == 'foo::MyStruct{\n' \
' .state = "on",\n' \
' .min_length = 1,\n' \
' .max_length = 5,\n' \
'}'
class TestArrayInitializer:
def test_str__empty(self):
target = cg.ArrayInitializer(
None, None
)
actual = str(target)
assert actual == "{}"
def test_str__not_multiline(self):
target = cg.ArrayInitializer(
1, 2, 3, 4
)
actual = str(target)
assert actual == "{1, 2, 3, 4}"
def test_str__multiline(self):
target = cg.ArrayInitializer(
1, 2, 3, 4, multiline=True
)
actual = str(target)
assert actual == "{\n 1,\n 2,\n 3,\n 4,\n}"
class TestParameterListExpression:
def test_str(self):
target = cg.ParameterListExpression(
cg.ParameterExpression(int, "foo"),
(float, "bar"),
)
actual = str(target)
assert actual == "int32_t foo, float bar"
class TestLambdaExpression:
def test_str__no_return(self):
target = cg.LambdaExpression(
(
"if ((foo == 5) && (bar < 10))) {\n",
"}",
),
((int, "foo"), (float, "bar")),
)
actual = str(target)
assert actual == (
"[=](int32_t foo, float bar) {\n"
" if ((foo == 5) && (bar < 10))) {\n"
" }\n"
"}"
)
def test_str__with_return(self):
target = cg.LambdaExpression(
("return (foo == 5) && (bar < 10));", ),
cg.ParameterListExpression((int, "foo"), (float, "bar")),
"=",
bool,
)
actual = str(target)
assert actual == (
"[=](int32_t foo, float bar) -> bool {\n"
" return (foo == 5) && (bar < 10));\n"
"}"
)
class TestLiterals:
@pytest.mark.parametrize("target, expected", (
(cg.StringLiteral("foo"), '"foo"'),
(cg.IntLiteral(0), "0"),
(cg.IntLiteral(42), "42"),
(cg.IntLiteral(4304967295), "4304967295ULL"),
(cg.IntLiteral(2150483647), "2150483647UL"),
(cg.IntLiteral(-2150083647), "-2150083647LL"),
(cg.BoolLiteral(True), "true"),
(cg.BoolLiteral(False), "false"),
(cg.HexIntLiteral(0), "0x00"),
(cg.HexIntLiteral(42), "0x2A"),
(cg.HexIntLiteral(682), "0x2AA"),
(cg.FloatLiteral(0.0), "0.0f"),
(cg.FloatLiteral(4.2), "4.2f"),
(cg.FloatLiteral(1.23456789), "1.23456789f"),
(cg.FloatLiteral(math.nan), "NAN"),
))
def test_str__simple(self, target: cg.Literal, expected: str):
actual = str(target)
assert actual == expected
FAKE_ENUM_VALUE = cg.EnumValue()
FAKE_ENUM_VALUE.enum_value = "foo"
@pytest.mark.parametrize("obj, expected_type", (
(cg.RawExpression("foo"), cg.RawExpression),
(FAKE_ENUM_VALUE, cg.StringLiteral),
(True, cg.BoolLiteral),
("foo", cg.StringLiteral),
(cg.HexInt(42), cg.HexIntLiteral),
(42, cg.IntLiteral),
(42.1, cg.FloatLiteral),
(cg.TimePeriodMicroseconds(microseconds=42), cg.IntLiteral),
(cg.TimePeriodMilliseconds(milliseconds=42), cg.IntLiteral),
(cg.TimePeriodSeconds(seconds=42), cg.IntLiteral),
(cg.TimePeriodMinutes(minutes=42), cg.IntLiteral),
((1, 2, 3), cg.ArrayInitializer),
([1, 2, 3], cg.ArrayInitializer),
))
def test_safe_exp__allowed_values(obj, expected_type):
actual = cg.safe_exp(obj)
assert isinstance(actual, expected_type)
@pytest.mark.parametrize("obj, expected_type", (
(bool, ct.bool_),
(int, ct.int32),
(float, ct.float_),
))
def test_safe_exp__allowed_types(obj, expected_type):
actual = cg.safe_exp(obj)
assert actual is expected_type
@pytest.mark.parametrize("obj, expected_error", (
(cg.ID("foo"), "Object foo is an ID."),
((x for x in "foo"), r"Object <.*> is a coroutine."),
(None, "Object is not an expression"),
))
def test_safe_exp__invalid_values(obj, expected_error):
with pytest.raises(ValueError, match=expected_error):
cg.safe_exp(obj)
class TestStatements:
@pytest.mark.parametrize("target, expected", (
(cg.RawStatement("foo && bar"), "foo && bar"),
(cg.ExpressionStatement("foo"), '"foo";'),
(cg.ExpressionStatement(42), '42;'),
(cg.LineComment("The point of foo is..."), "// The point of foo is..."),
(cg.LineComment("Help help\nI'm being repressed"), "// Help help\n// I'm being repressed"),
(
cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar", None),
'static const uint16_t foo[] PROGMEM = "bar"'
)
))
def test_str__simple(self, target: cg.Statement, expected: str):
actual = str(target)
assert actual == expected
# TODO: This method has side effects in CORE
# def test_progmem_array():
# pass
class TestMockObj:
def test_getattr(self):
target = cg.MockObj("foo")
actual = target.eek
assert isinstance(actual, cg.MockObj)
assert actual.base == "foo.eek"
assert actual.op == "."

View File

@ -0,0 +1,85 @@
import pytest
from mock import Mock
from esphome import cpp_helpers as ch
from esphome import const
from esphome.cpp_generator import MockObj
def test_gpio_pin_expression__conf_is_none(monkeypatch):
target = ch.gpio_pin_expression(None)
actual = next(target)
assert actual is None
def test_gpio_pin_expression__new_pin(monkeypatch):
target = ch.gpio_pin_expression({
const.CONF_NUMBER: 42,
const.CONF_MODE: "input",
const.CONF_INVERTED: False
})
actual = next(target)
assert isinstance(actual, MockObj)
def test_register_component(monkeypatch):
var = Mock(base="foo.bar")
app_mock = Mock(register_component=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
monkeypatch.setattr(ch, "CORE", core_mock)
add_mock = Mock()
monkeypatch.setattr(ch, "add", add_mock)
target = ch.register_component(var, {})
actual = next(target)
assert actual is var
add_mock.assert_called_once()
app_mock.register_component.assert_called_with(var)
assert core_mock.component_ids == []
def test_register_component__no_component_id(monkeypatch):
var = Mock(base="foo.eek")
core_mock = Mock(component_ids=["foo.bar"])
monkeypatch.setattr(ch, "CORE", core_mock)
with pytest.raises(ValueError, match="Component ID foo.eek was not declared to"):
target = ch.register_component(var, {})
next(target)
def test_register_component__with_setup_priority(monkeypatch):
var = Mock(base="foo.bar")
app_mock = Mock(register_component=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
monkeypatch.setattr(ch, "CORE", core_mock)
add_mock = Mock()
monkeypatch.setattr(ch, "add", add_mock)
target = ch.register_component(var, {
const.CONF_SETUP_PRIORITY: "123",
const.CONF_UPDATE_INTERVAL: "456",
})
actual = next(target)
assert actual is var
add_mock.assert_called()
assert add_mock.call_count == 3
app_mock.register_component.assert_called_with(var)
assert core_mock.component_ids == []