mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 00:10:22 +00:00
[api] Default response only string fields to a null StringRef (#19025)
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
TEST(StringRefStartsWith, ProperPrefixMatches) {
|
||||
@@ -59,4 +62,103 @@ TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) {
|
||||
EXPECT_TRUE(ref.starts_with(prefix));
|
||||
}
|
||||
|
||||
// The generated api messages start their encode only string fields as a null pointer with zero
|
||||
// length; every member must treat that exactly like the default constructed empty string.
|
||||
TEST(StringRefNullEmpty, BehavesAsEmptyString) {
|
||||
const StringRef null_empty{nullptr, 0};
|
||||
const StringRef empty;
|
||||
EXPECT_TRUE(null_empty.empty());
|
||||
EXPECT_EQ(null_empty.size(), 0u);
|
||||
EXPECT_EQ(null_empty.c_str(), nullptr);
|
||||
EXPECT_EQ(null_empty.byte(), nullptr);
|
||||
EXPECT_TRUE(null_empty == empty);
|
||||
EXPECT_FALSE(null_empty < empty);
|
||||
EXPECT_FALSE(empty < null_empty);
|
||||
EXPECT_TRUE(null_empty == ""); // NOLINT(readability-container-size-empty) - operator under test
|
||||
EXPECT_TRUE(null_empty == std::string()); // NOLINT(readability-container-size-empty) - operator under test
|
||||
EXPECT_EQ(null_empty.compare(empty), 0);
|
||||
EXPECT_EQ(null_empty.compare(""), 0);
|
||||
EXPECT_LT(null_empty.compare("a"), 0);
|
||||
EXPECT_TRUE(null_empty.starts_with(""));
|
||||
EXPECT_FALSE(null_empty.starts_with("a"));
|
||||
EXPECT_EQ(null_empty.str(), std::string());
|
||||
EXPECT_EQ(null_empty.substr(0), std::string());
|
||||
EXPECT_EQ(null_empty.find('a'), std::string::npos);
|
||||
EXPECT_EQ(null_empty.find("a"), std::string::npos);
|
||||
char buf[4] = "xyz";
|
||||
EXPECT_EQ(null_empty.copy(buf, sizeof(buf)), 0u);
|
||||
EXPECT_EQ(null_empty.begin(), null_empty.end());
|
||||
}
|
||||
|
||||
TEST(StringRefNullEmpty, ComparesAgainstText) {
|
||||
const StringRef null_empty{nullptr, 0};
|
||||
const StringRef text("abc", 3);
|
||||
EXPECT_FALSE(null_empty == text);
|
||||
EXPECT_FALSE(text == null_empty);
|
||||
EXPECT_LT(null_empty.compare(text), 0);
|
||||
EXPECT_GT(text.compare(null_empty), 0);
|
||||
EXPECT_TRUE(null_empty < text);
|
||||
EXPECT_FALSE(text < null_empty);
|
||||
EXPECT_TRUE(text.starts_with(null_empty));
|
||||
}
|
||||
|
||||
TEST(StringRefNullEmpty, TwoNullViewsAreEqual) {
|
||||
const StringRef a{nullptr, 0};
|
||||
const StringRef b{nullptr, 0};
|
||||
EXPECT_TRUE(a == b);
|
||||
EXPECT_FALSE(a < b);
|
||||
EXPECT_EQ(a.compare(b), 0);
|
||||
EXPECT_TRUE(a.starts_with(b));
|
||||
}
|
||||
|
||||
// Every iterator endpoint of a null view is the same null position: nothing is dereferenced and
|
||||
// no offset is applied to the null pointer, so the range is simply empty.
|
||||
TEST(StringRefNullEmpty, IteratorEndpointsFormAnEmptyRange) {
|
||||
const StringRef null_empty{nullptr, 0};
|
||||
EXPECT_EQ(null_empty.cbegin(), null_empty.cend());
|
||||
EXPECT_EQ(null_empty.rbegin(), null_empty.rend());
|
||||
EXPECT_EQ(null_empty.crbegin(), null_empty.crend());
|
||||
EXPECT_EQ(std::distance(null_empty.begin(), null_empty.end()), 0);
|
||||
size_t visited = 0;
|
||||
for (char c : null_empty) {
|
||||
(void) c;
|
||||
visited++;
|
||||
}
|
||||
EXPECT_EQ(visited, 0u);
|
||||
// NOLINTNEXTLINE(bugprone-string-constructor) - empty range under test
|
||||
EXPECT_EQ(std::string(null_empty.begin(), null_empty.end()), std::string());
|
||||
}
|
||||
|
||||
// The pointer and length constructor accepts an empty range at a null pointer; the copy into a
|
||||
// std::string reads nothing.
|
||||
TEST(StringRefNullEmpty, ConvertsToEmptyStdString) {
|
||||
const StringRef null_empty{nullptr, 0};
|
||||
const std::string copy = null_empty.str();
|
||||
EXPECT_TRUE(copy.empty());
|
||||
EXPECT_EQ(static_cast<std::string>(null_empty), std::string());
|
||||
EXPECT_EQ(null_empty.substr(0, 5), std::string());
|
||||
std::string target("keep");
|
||||
target += null_empty;
|
||||
EXPECT_EQ(target, "keep");
|
||||
}
|
||||
|
||||
// The number conversions hand the pointer to the C library; a null view must stop before that.
|
||||
TEST(StringRefNullEmpty, NumericConversionsReturnZero) {
|
||||
const StringRef null_empty{nullptr, 0};
|
||||
size_t pos = 99;
|
||||
EXPECT_EQ(stoi(null_empty, &pos), 0);
|
||||
EXPECT_EQ(pos, 0u);
|
||||
pos = 99;
|
||||
EXPECT_EQ(stol(null_empty, &pos, 16), 0L);
|
||||
EXPECT_EQ(pos, 0u);
|
||||
pos = 99;
|
||||
EXPECT_EQ(stof(null_empty, &pos), 0.0f);
|
||||
EXPECT_EQ(pos, 0u);
|
||||
pos = 99;
|
||||
EXPECT_EQ(stod(null_empty, &pos), 0.0);
|
||||
EXPECT_EQ(pos, 0u);
|
||||
EXPECT_EQ(stoi(null_empty), 0);
|
||||
EXPECT_EQ(stof(null_empty), 0.0f);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -18,7 +18,9 @@ sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
||||
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
|
||||
from api_protobuf import ( # noqa: E402
|
||||
MAX_MESSAGE_ID,
|
||||
SOURCE_BOTH,
|
||||
SOURCE_CLIENT,
|
||||
SOURCE_SERVER,
|
||||
_make_ifdef_line,
|
||||
build_message_type,
|
||||
create_field_type_info,
|
||||
@@ -199,6 +201,24 @@ def _decode_case(field_type: int, number: int, *, repeated: bool = False) -> str
|
||||
).decode_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("needs_decode", "force", "member"),
|
||||
[
|
||||
(False, False, "StringRef value{nullptr, 0}; // null until set, encode only"),
|
||||
(True, False, "StringRef value{};"),
|
||||
(False, True, "StringRef value{};"),
|
||||
],
|
||||
)
|
||||
def test_string_fields_default_to_null_only_when_never_read(
|
||||
needs_decode: bool, force: bool, member: str
|
||||
) -> None:
|
||||
"""Only a string that is neither decoded nor force encoded may start as a null StringRef."""
|
||||
ti = create_field_type_info(
|
||||
_field(STRING, force=force), needs_decode=needs_decode, needs_encode=True
|
||||
)
|
||||
assert ti.public_content == [member]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field_type", "number", "wire_type", "accessor"),
|
||||
[
|
||||
@@ -290,3 +310,21 @@ def test_message_gets_a_single_decode_field_override() -> None:
|
||||
(3, "WIRE_TYPE_FIXED32"),
|
||||
):
|
||||
assert f"case proto_tag({number}, {wire_type}):" in cpp, cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [SOURCE_CLIENT, SOURCE_SERVER, SOURCE_BOTH])
|
||||
def test_repeated_strings_generate_in_every_direction(source: int) -> None:
|
||||
"""A repeated string builds its element type from the type map, so the null
|
||||
default plumbing on the pointer string type never sees it."""
|
||||
desc = descriptor_pb2.DescriptorProto(name="Names")
|
||||
desc.field.add(
|
||||
name="names",
|
||||
number=1,
|
||||
type=STRING,
|
||||
label=descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED,
|
||||
)
|
||||
header, cpp, _ = build_message_type(desc, {}, {"Names": source})
|
||||
assert "std::vector<std::string> names{};" in header
|
||||
assert "nullptr" not in header
|
||||
if source != SOURCE_SERVER:
|
||||
assert "case proto_tag(1, WIRE_TYPE_LENGTH_DELIMITED):" in cpp
|
||||
|
||||
Reference in New Issue
Block a user