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]) initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE])
rhs = GlobalsComponent.new(template_args, 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) yield cg.register_component(glob, config)
if config[CONF_RESTORE_VALUE]: if config[CONF_RESTORE_VALUE]:

View file

@ -169,7 +169,7 @@ def to_code(config):
else: else:
out_type = NeoPixelRGBLightOutput.template(template) out_type = NeoPixelRGBLightOutput.template(template)
rhs = out_type.new() 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 light.register_light(var, config)
yield cg.register_component(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]] model_type, model = MODELS[config[CONF_MODEL]]
if model_type == 'a': if model_type == 'a':
rhs = WaveshareEPaperTypeA.new(model) 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': elif model_type == 'b':
rhs = model.new() rhs = model.new()
var = cg.Pvariable(config[CONF_ID], rhs, type=model) var = cg.Pvariable(config[CONF_ID], rhs, model)
else: else:
raise NotImplementedError() raise NotImplementedError()

View file

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