mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Emits one PROGMEM array per distinct type and contents and returns the same variable to later callers. The array name is made unique against every config id and registered variable, so a generated table can never collide with a user id.
840 lines
27 KiB
Python
840 lines
27 KiB
Python
from collections.abc import Iterator
|
|
import math
|
|
|
|
import pytest
|
|
|
|
from esphome import cpp_generator as cg, cpp_types as ct
|
|
from esphome.core import CORE, ID
|
|
|
|
|
|
class TestExpressions:
|
|
@pytest.mark.parametrize(
|
|
"target, expected",
|
|
(
|
|
(cg.RawExpression("foo && bar"), "foo && bar"),
|
|
(cg.AssignmentExpression(None, None, "foo", "bar"), 'foo = "bar"'),
|
|
(cg.AssignmentExpression(ct.float_, "*", "foo", 1), "float *foo = 1"),
|
|
(cg.AssignmentExpression(ct.float_, "", "foo", 1), "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 TestStaticCastExpression:
|
|
def test_str(self):
|
|
target = cg.StaticCastExpression(ct.bool_, 42)
|
|
|
|
actual = str(target)
|
|
|
|
assert actual == "static_cast<bool>(42)"
|
|
|
|
|
|
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"
|
|
"}"
|
|
)
|
|
|
|
def test_str__stateless_no_return(self):
|
|
"""Test stateless lambda (empty capture) generates correctly"""
|
|
target = cg.LambdaExpression(
|
|
('ESP_LOGD("main", "Test message");',),
|
|
(), # No parameters
|
|
"", # Empty capture (stateless)
|
|
)
|
|
|
|
actual = str(target)
|
|
|
|
assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}')
|
|
|
|
def test_str__stateless_with_return(self):
|
|
"""Test stateless lambda with return type generates correctly"""
|
|
target = cg.LambdaExpression(
|
|
("return global_value > 0;",),
|
|
(), # No parameters
|
|
"", # Empty capture (stateless)
|
|
bool, # Return type
|
|
)
|
|
|
|
actual = str(target)
|
|
|
|
assert actual == ("[]() -> bool {\n return global_value > 0;\n}")
|
|
|
|
def test_str__stateless_with_params(self):
|
|
"""Test stateless lambda with parameters generates correctly"""
|
|
target = cg.LambdaExpression(
|
|
("return foo + bar;",),
|
|
((int, "foo"), (float, "bar")),
|
|
"", # Empty capture (stateless)
|
|
float,
|
|
)
|
|
|
|
actual = str(target)
|
|
|
|
assert actual == (
|
|
"[](int32_t foo, float bar) -> float {\n return foo + bar;\n}"
|
|
)
|
|
|
|
def test_str__with_capture(self):
|
|
"""Test lambda with capture generates correctly"""
|
|
target = cg.LambdaExpression(
|
|
("return captured_var + x;",),
|
|
((int, "x"),),
|
|
"captured_var", # Has capture (not stateless)
|
|
int,
|
|
)
|
|
|
|
actual = str(target)
|
|
|
|
assert actual == (
|
|
"[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}"
|
|
)
|
|
|
|
|
|
class TestCallLambda:
|
|
"""Tests for the call_lambda() function."""
|
|
|
|
def test_call_lambda__return_expression_casts_to_return_type(self):
|
|
"""A lambda body that is just a return statement reduces to the
|
|
expression, cast to the lambda's return type."""
|
|
lamb = cg.LambdaExpression(("return foo + 1;",), (), "", ct.bool_)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.StaticCastExpression)
|
|
assert str(result) == "static_cast<bool>(foo + 1)"
|
|
|
|
def test_call_lambda__return_with_trailing_statements_is_called(self) -> None:
|
|
"""Only a lone return statement reduces; a longer body is called as is."""
|
|
lamb = cg.LambdaExpression(("return 1;\nfoo();",), (), "", ct.int_)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.CallExpression)
|
|
assert str(result).endswith("}()")
|
|
|
|
def test_call_lambda__braced_return_is_called(self) -> None:
|
|
"""A braced return needs the lambda's return type, so it is not reduced."""
|
|
lamb = cg.LambdaExpression(("return {};",), (), "", ct.int_)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.CallExpression)
|
|
assert "static_cast" not in str(result)
|
|
|
|
def test_call_lambda__return_expression_with_class_return_type_no_cast(self):
|
|
"""A class return type is not cast, since static_cast doesn't apply
|
|
to arbitrary class types."""
|
|
mock_class = cg.MockObjClass("foo::Bar", parents=())
|
|
lamb = cg.LambdaExpression(("return get_bar();",), (), "", mock_class)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.RawExpression)
|
|
assert str(result) == "get_bar()"
|
|
|
|
def test_call_lambda__no_return_with_parameters_calls_with_names(self):
|
|
"""A multi-statement lambda with parameters is called with the
|
|
parameter names as arguments."""
|
|
lamb = cg.LambdaExpression(
|
|
("do_something(x, y);",), ((int, "x"), (float, "y")), "=", ct.bool_
|
|
)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.CallExpression)
|
|
assert str(result) == (
|
|
"[=](int32_t x, float y) -> bool {\n do_something(x, y);\n}(x, y)"
|
|
)
|
|
|
|
def test_call_lambda__no_return_type_raises(self):
|
|
"""Calling a lambda with no declared return type is a developer
|
|
error: call_lambda is only for value-returning lambdas."""
|
|
lamb = cg.LambdaExpression(("do_something();",), (), "=")
|
|
|
|
with pytest.raises(AssertionError):
|
|
cg.call_lambda(lamb)
|
|
|
|
def test_call_lambda__identifier_starting_with_return_is_not_a_return_statement(
|
|
self,
|
|
):
|
|
"""A body that merely starts with the substring "return" (e.g. a call
|
|
to a function named returnValue()) must not be mistaken for a return
|
|
statement -- the match requires a word boundary after "return"."""
|
|
lamb = cg.LambdaExpression(("returnValue();",), (), "=", ct.bool_)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.CallExpression)
|
|
assert str(result) == "[=]() -> bool {\n returnValue();\n}()"
|
|
|
|
def test_call_lambda__no_return_no_parameters_calls_with_no_args(self):
|
|
"""A multi-statement lambda without parameters is called with no
|
|
arguments."""
|
|
lamb = cg.LambdaExpression(("do_something();",), (), "", ct.bool_)
|
|
|
|
result = cg.call_lambda(lamb)
|
|
|
|
assert isinstance(result, cg.CallExpression)
|
|
assert str(result) == "[]() -> bool {\n do_something();\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"),
|
|
(cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'),
|
|
(cg.FlashStringLiteral(""), 'ESPHOME_F("")'),
|
|
(
|
|
cg.FlashStringLiteral('quote"here'),
|
|
'ESPHOME_F("quote\\042here")',
|
|
),
|
|
),
|
|
)
|
|
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"),
|
|
'static constexpr 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 == "."
|
|
|
|
|
|
class TestStatementFunction:
|
|
"""Tests for the statement() function."""
|
|
|
|
def test_statement__expression_converted_to_statement(self):
|
|
"""Test that expressions are converted to ExpressionStatement."""
|
|
expr = cg.RawExpression("foo()")
|
|
result = cg.statement(expr)
|
|
|
|
assert isinstance(result, cg.ExpressionStatement)
|
|
assert str(result) == "foo();"
|
|
|
|
def test_statement__statement_unchanged(self):
|
|
"""Test that statements are returned unchanged."""
|
|
stmt = cg.RawStatement("foo()")
|
|
result = cg.statement(stmt)
|
|
|
|
assert result is stmt
|
|
assert str(result) == "foo()"
|
|
|
|
def test_statement__expression_statement_unchanged(self):
|
|
"""Test that ExpressionStatement is returned unchanged."""
|
|
stmt = cg.ExpressionStatement(42)
|
|
result = cg.statement(stmt)
|
|
|
|
assert result is stmt
|
|
assert str(result) == "42;"
|
|
|
|
def test_statement__line_comment_unchanged(self):
|
|
"""Test that LineComment is returned unchanged."""
|
|
stmt = cg.LineComment("This is a comment")
|
|
result = cg.statement(stmt)
|
|
|
|
assert result is stmt
|
|
assert str(result) == "// This is a comment"
|
|
|
|
|
|
class TestLiteralFunction:
|
|
"""Tests for the literal() function."""
|
|
|
|
def test_literal__creates_mockobj(self):
|
|
"""Test that literal() creates a MockObj."""
|
|
result = cg.literal("MY_CONSTANT")
|
|
|
|
assert isinstance(result, cg.MockObj)
|
|
assert result.base == "MY_CONSTANT"
|
|
assert result.op == ""
|
|
|
|
def test_literal__string_representation(self):
|
|
"""Test that literal names appear unquoted in generated code."""
|
|
result = cg.literal("nullptr")
|
|
|
|
assert str(result) == "nullptr"
|
|
|
|
def test_literal__can_be_used_in_expressions(self):
|
|
"""Test that literals can be used as part of larger expressions."""
|
|
null_lit = cg.literal("nullptr")
|
|
expr = cg.CallExpression(cg.RawExpression("my_func"), null_lit)
|
|
|
|
assert str(expr) == "my_func(nullptr)"
|
|
|
|
def test_literal__common_cpp_literals(self):
|
|
"""Test common C++ literal values."""
|
|
test_cases = [
|
|
("nullptr", "nullptr"),
|
|
("true", "true"),
|
|
("false", "false"),
|
|
("NULL", "NULL"),
|
|
("NAN", "NAN"),
|
|
]
|
|
|
|
for name, expected in test_cases:
|
|
result = cg.literal(name)
|
|
assert str(result) == expected
|
|
|
|
|
|
class TestLambdaConstructor:
|
|
"""Tests for the Lambda class constructor in core/__init__.py."""
|
|
|
|
def test_lambda__from_string(self):
|
|
"""Test Lambda constructor with string argument."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x + 1;")
|
|
|
|
assert lambda_obj.value == "return x + 1;"
|
|
assert str(lambda_obj) == "return x + 1;"
|
|
|
|
def test_lambda__from_expression(self):
|
|
"""Test Lambda constructor with Expression argument."""
|
|
from esphome.core import Lambda
|
|
|
|
expr = cg.RawExpression("x + 1")
|
|
lambda_obj = Lambda(expr)
|
|
|
|
# Expression should be converted to statement (with semicolon)
|
|
assert lambda_obj.value == "x + 1;"
|
|
|
|
def test_lambda__from_lambda(self):
|
|
"""Test Lambda constructor with another Lambda argument."""
|
|
from esphome.core import Lambda
|
|
|
|
original = Lambda("return x + 1;")
|
|
copy = Lambda(original)
|
|
|
|
assert copy.value == original.value
|
|
assert copy.value == "return x + 1;"
|
|
|
|
def test_lambda__parts_parsing(self):
|
|
"""Test that Lambda correctly parses parts with id() references."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return id(my_sensor).state;")
|
|
parts = lambda_obj.parts
|
|
|
|
# Parts should be split by LAMBDA_PROG regex: text, id, op, text
|
|
assert len(parts) == 4
|
|
assert parts[0] == "return "
|
|
assert parts[1] == "my_sensor"
|
|
assert parts[2] == "."
|
|
assert parts[3] == "state;"
|
|
|
|
def test_lambda__requires_ids(self):
|
|
"""Test that Lambda correctly extracts required IDs."""
|
|
from esphome.core import ID, Lambda
|
|
|
|
lambda_obj = Lambda("return id(sensor1).state + id(sensor2).value;")
|
|
ids = lambda_obj.requires_ids
|
|
|
|
assert len(ids) == 2
|
|
assert all(isinstance(id_obj, ID) for id_obj in ids)
|
|
assert ids[0].id == "sensor1"
|
|
assert ids[1].id == "sensor2"
|
|
|
|
def test_lambda__no_ids(self):
|
|
"""Test Lambda with no id() references."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return 42;")
|
|
ids = lambda_obj.requires_ids
|
|
|
|
assert len(ids) == 0
|
|
|
|
def test_lambda__comment_removal(self):
|
|
"""Test that comments are removed when parsing parts."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return id(sensor).state; // Get sensor state")
|
|
parts = lambda_obj.parts
|
|
|
|
# Comment should be replaced with space, not affect parsing
|
|
assert "my_sensor" not in str(parts)
|
|
|
|
def test_lambda__multiline_string(self):
|
|
"""Test Lambda with multiline string."""
|
|
from esphome.core import Lambda
|
|
|
|
code = """if (id(sensor).state > 0) {
|
|
return true;
|
|
}
|
|
return false;"""
|
|
lambda_obj = Lambda(code)
|
|
|
|
assert lambda_obj.value == code
|
|
assert "sensor" in [id_obj.id for id_obj in lambda_obj.requires_ids]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestProcessLambda:
|
|
"""Tests for the process_lambda() async function."""
|
|
|
|
async def test_process_lambda__none_value(self):
|
|
"""Test that None returns None."""
|
|
result = await cg.process_lambda(None, [])
|
|
|
|
assert result is None
|
|
|
|
async def test_process_lambda__with_expression(self):
|
|
"""Test process_lambda with Expression argument."""
|
|
|
|
expr = cg.RawExpression("return x + 1")
|
|
result = await cg.process_lambda(expr, [(int, "x")])
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
assert "x + 1" in str(result)
|
|
|
|
async def test_process_lambda__simple_lambda_no_ids(self):
|
|
"""Test process_lambda with simple Lambda without id() references."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x + 1;")
|
|
result = await cg.process_lambda(lambda_obj, [(int, "x")])
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
# Should have parameter
|
|
lambda_str = str(result)
|
|
assert "int32_t x" in lambda_str
|
|
assert "return x + 1;" in lambda_str
|
|
|
|
async def test_process_lambda__with_return_type(self):
|
|
"""Test process_lambda with return type specified."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x > 0;")
|
|
result = await cg.process_lambda(lambda_obj, [(int, "x")], return_type=bool)
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
lambda_str = str(result)
|
|
assert "-> bool" in lambda_str
|
|
|
|
async def test_process_lambda__with_capture(self):
|
|
"""Test process_lambda with capture specified."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return captured + x;")
|
|
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="captured")
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
lambda_str = str(result)
|
|
assert "[captured]" in lambda_str
|
|
|
|
async def test_process_lambda__empty_capture(self):
|
|
"""Test process_lambda with empty capture (stateless lambda)."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x + 1;")
|
|
result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="")
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
lambda_str = str(result)
|
|
assert "[]" in lambda_str
|
|
|
|
async def test_process_lambda__no_parameters(self):
|
|
"""Test process_lambda with no parameters."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return 42;")
|
|
result = await cg.process_lambda(lambda_obj, [])
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
lambda_str = str(result)
|
|
# Should have empty parameter list
|
|
assert "()" in lambda_str
|
|
|
|
async def test_process_lambda__multiple_parameters(self):
|
|
"""Test process_lambda with multiple parameters."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x + y + z;")
|
|
result = await cg.process_lambda(
|
|
lambda_obj, [(int, "x"), (float, "y"), (bool, "z")]
|
|
)
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
lambda_str = str(result)
|
|
assert "int32_t x" in lambda_str
|
|
assert "float y" in lambda_str
|
|
assert "bool z" in lambda_str
|
|
|
|
async def test_process_lambda__parameter_validation(self):
|
|
"""Test that malformed parameters raise assertion error."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda("return x;")
|
|
|
|
# Test invalid parameter format (not list of tuples)
|
|
with pytest.raises(AssertionError):
|
|
await cg.process_lambda(lambda_obj, "invalid")
|
|
|
|
# Test invalid tuple format (not 2-element tuples)
|
|
with pytest.raises(AssertionError):
|
|
await cg.process_lambda(lambda_obj, [(int, "x", "extra")])
|
|
|
|
# Test invalid tuple format (single element)
|
|
with pytest.raises(AssertionError):
|
|
await cg.process_lambda(lambda_obj, [(int,)])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__string_with_std_string_returns_flash_literal() -> None:
|
|
"""Static string with std::string output_type returns FlashStringLiteral."""
|
|
result = await cg.templatable("hello", [], ct.std_string)
|
|
|
|
assert isinstance(result, cg.FlashStringLiteral)
|
|
assert str(result) == 'ESPHOME_F("hello")'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__empty_string_with_std_string() -> None:
|
|
"""Empty static string with std::string output_type returns FlashStringLiteral."""
|
|
result = await cg.templatable("", [], ct.std_string)
|
|
|
|
assert isinstance(result, cg.FlashStringLiteral)
|
|
assert str(result) == 'ESPHOME_F("")'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__string_with_none_output_type() -> None:
|
|
"""Static string with output_type=None returns raw string (no wrapping)."""
|
|
result = await cg.templatable("hello", [], None)
|
|
|
|
assert isinstance(result, str)
|
|
assert result == "hello"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__int_with_std_string() -> None:
|
|
"""Non-string value with std::string output_type returns raw value."""
|
|
result = await cg.templatable(42, [], ct.std_string)
|
|
|
|
assert result == 42
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__string_with_non_string_output_type() -> None:
|
|
"""Static string with non-std::string output_type returns stateless lambda."""
|
|
result = await cg.templatable("hello", [], ct.bool_)
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
assert result.capture == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__with_to_exp_callable() -> None:
|
|
"""When to_exp is provided, it is applied to non-template values."""
|
|
result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2)
|
|
|
|
assert result == 84
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__with_to_exp_callable_and_output_type() -> None:
|
|
"""When to_exp is provided with non-string output_type, result is lambda-wrapped."""
|
|
result = await cg.templatable(42, [], ct.int_, to_exp=lambda x: x * 2)
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
assert result.capture == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__with_to_exp_dict() -> None:
|
|
"""When to_exp is a dict, value is looked up."""
|
|
mapping: dict[str, int] = {"on": 1, "off": 0}
|
|
result = await cg.templatable("on", [], None, to_exp=mapping)
|
|
|
|
assert result == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_templatable__lambda_with_std_string() -> None:
|
|
"""Lambda value returns LambdaExpression, not FlashStringLiteral."""
|
|
from esphome.core import Lambda
|
|
|
|
lambda_obj = Lambda('return "hello";')
|
|
result = await cg.templatable(lambda_obj, [], ct.std_string)
|
|
|
|
assert isinstance(result, cg.LambdaExpression)
|
|
|
|
|
|
class TestSharedProgmemArray:
|
|
def test_identical_contents_share_one_array(self) -> None:
|
|
CORE.config = {}
|
|
a = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3])
|
|
b = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3])
|
|
c = cg.shared_progmem_array("table", ct.uint8, [4])
|
|
assert a is b
|
|
assert str(a) != str(c)
|
|
assert sum("PROGMEM" in str(st) for st in CORE.main_statements) == 2
|
|
|
|
def test_same_contents_different_type_are_separate(self) -> None:
|
|
CORE.config = {}
|
|
a = cg.shared_progmem_array("table", ct.uint8, [1])
|
|
b = cg.shared_progmem_array("table", ct.uint16, [1])
|
|
assert str(a) != str(b)
|
|
|
|
def test_name_avoids_config_ids(self) -> None:
|
|
CORE.config = {"sensor": [{"id": ID("table", is_declaration=True)}]}
|
|
array = cg.shared_progmem_array("table", ct.uint8, [1])
|
|
assert str(array) == "table_2"
|
|
|
|
def test_name_avoids_registered_variables(self) -> None:
|
|
CORE.config = {}
|
|
CORE.register_variable(ID("table", is_declaration=True), cg.MockObj("table"))
|
|
array = cg.shared_progmem_array("table", ct.uint8, [1])
|
|
assert str(array) == "table_2"
|