From d44754889378e1359bd2b09bf78f095b90b92091 Mon Sep 17 00:00:00 2001 From: Tim Savage Date: Mon, 20 Apr 2020 10:05:58 +1000 Subject: [PATCH] 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 --- esphome/components/globals/__init__.py | 2 +- esphome/components/neopixelbus/light.py | 2 +- .../components/waveshare_epaper/display.py | 4 +- esphome/cpp_generator.py | 329 +++++++++--------- requirements_test.txt | 1 + tests/unit_tests/test_codegen.py | 26 ++ tests/unit_tests/test_core.py | 2 +- tests/unit_tests/test_cpp_generator.py | 293 ++++++++++++++++ tests/unit_tests/test_cpp_helpers.py | 85 +++++ 9 files changed, 574 insertions(+), 170 deletions(-) create mode 100644 tests/unit_tests/test_codegen.py create mode 100644 tests/unit_tests/test_cpp_generator.py create mode 100644 tests/unit_tests/test_cpp_helpers.py diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index d285f1e97f..e59a7e6acb 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -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]: diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index fb83e4740d..2b84882e59 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -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) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 77322cbb70..bbbd8c0d3a 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -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() diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b5239e9413..e9bcdc7d1f 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -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}>' diff --git a/requirements_test.txt b/requirements_test.txt index 85f7d511a8..268f78fcdf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -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 diff --git a/tests/unit_tests/test_codegen.py b/tests/unit_tests/test_codegen.py new file mode 100644 index 0000000000..931e191de6 --- /dev/null +++ b/tests/unit_tests/test_codegen.py @@ -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) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 14f6990bd9..cd0b0947f3 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -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): diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py new file mode 100644 index 0000000000..b130124b54 --- /dev/null +++ b/tests/unit_tests/test_cpp_generator.py @@ -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 == '' + + 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(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 == "." diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py new file mode 100644 index 0000000000..d8f32e7a51 --- /dev/null +++ b/tests/unit_tests/test_cpp_helpers.py @@ -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 == []