From 352121b7bf9dc7efddf1fb7193ba3ff66b5fc1c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 19:57:37 -1000 Subject: [PATCH 1/7] [core] Add CodSpeed benchmarks for hot helper functions Add benchmarks for format_hex_to, format_hex_pretty_to, format_mac_addr_upper, fnv1_hash, fnv1a_hash, fnv1_hash_object_id, parse_hex, crc8, crc16, value_accuracy_to_buf, int8_to_str, and base64_decode to establish baseline performance data before optimization. --- tests/benchmarks/core/bench_helpers.cpp | 267 +++++++++++++++++++++++- 1 file changed, 266 insertions(+), 1 deletion(-) diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index c6e1e6930eb..1f9d05fc33d 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -10,7 +10,6 @@ namespace esphome::benchmarks { static constexpr int kInnerIterations = 2000; // --- random_float() --- -// Ported from ol.yaml:148 "Random Float Benchmark" static void RandomFloat(benchmark::State &state) { for (auto _ : state) { @@ -38,4 +37,270 @@ static void RandomUint32(benchmark::State &state) { } BENCHMARK(RandomUint32); +// --- format_hex_to() - 6 bytes (MAC address sized) --- + +static void FormatHexTo_6Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45}; + char buffer[13]; // 6 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 6); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_6Bytes); + +// --- format_hex_to() - 16 bytes (UUID sized) --- + +static void FormatHexTo_16Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; + char buffer[33]; // 16 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 16); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_16Bytes); + +// --- format_hex_to() - 100 bytes (large payload) --- + +static void FormatHexTo_100Bytes(benchmark::State &state) { + uint8_t data[100]; + for (int i = 0; i < 100; i++) + data[i] = static_cast(i); + char buffer[201]; // 100 * 2 + 1 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_to(buffer, data, 100); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexTo_100Bytes); + +// --- format_hex_pretty_to() - 6 bytes with ':' separator --- + +static void FormatHexPrettyTo_6Bytes(benchmark::State &state) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45}; + char buffer[18]; // 6 * 3 + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_hex_pretty_to(buffer, data, 6); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatHexPrettyTo_6Bytes); + +// --- format_mac_addr_upper() --- + +static void FormatMacAddrUpper(benchmark::State &state) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + format_mac_addr_upper(mac, buffer); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(FormatMacAddrUpper); + +// --- fnv1_hash() - short string --- + +static void Fnv1Hash_Short(benchmark::State &state) { + const char *str = "sensor.temperature"; + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash(str); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1Hash_Short); + +// --- fnv1_hash() - long string --- + +static void Fnv1Hash_Long(benchmark::State &state) { + const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected"; + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash(str); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1Hash_Long); + +// --- fnv1a_hash() - short string --- +// Use DoNotOptimize on the input pointer to prevent constexpr evaluation + +static void Fnv1aHash_Short(benchmark::State &state) { + const char *str = "sensor.temperature"; + benchmark::DoNotOptimize(str); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1a_hash(str); + benchmark::ClobberMemory(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1aHash_Short); + +// --- fnv1a_hash() - long string --- + +static void Fnv1aHash_Long(benchmark::State &state) { + const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected"; + benchmark::DoNotOptimize(str); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1a_hash(str); + benchmark::ClobberMemory(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1aHash_Long); + +// --- fnv1_hash_object_id() - typical entity name --- + +static void Fnv1HashObjectId(benchmark::State &state) { + const char *name = "Living Room Temperature Sensor"; + size_t len = strlen(name); + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= fnv1_hash_object_id(name, len); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Fnv1HashObjectId); + +// --- parse_hex() - 6 bytes from string --- + +static void ParseHex_6Bytes(benchmark::State &state) { + const char *hex_str = "ABCDEF012345"; + uint8_t data[6]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + parse_hex(hex_str, data, 6); + } + benchmark::DoNotOptimize(data); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ParseHex_6Bytes); + +// --- parse_hex() - 16 bytes from string --- + +static void ParseHex_16Bytes(benchmark::State &state) { + const char *hex_str = "ABCDEF0123456789FEDCBA9876543210"; + uint8_t data[16]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + parse_hex(hex_str, data, 16); + } + benchmark::DoNotOptimize(data); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ParseHex_16Bytes); + +// --- crc8() - 8 bytes --- + +static void CRC8_8Bytes(benchmark::State &state) { + const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + for (auto _ : state) { + uint8_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= crc8(data, 8); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CRC8_8Bytes); + +// --- crc16() - 8 bytes --- + +static void CRC16_8Bytes(benchmark::State &state) { + const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + for (auto _ : state) { + uint16_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result ^= crc16(data, 8); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CRC16_8Bytes); + +// --- value_accuracy_to_buf() - typical sensor value --- + +static void ValueAccuracyToBuf(benchmark::State &state) { + char raw_buf[VALUE_ACCURACY_MAX_LEN]; + std::span buf(raw_buf); + float value = 23.456f; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + value_accuracy_to_buf(buf, value, 2); + } + benchmark::DoNotOptimize(raw_buf); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ValueAccuracyToBuf); + +// --- int8_to_str() --- + +static void Int8ToStr(benchmark::State &state) { + char buffer[5]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + int8_to_str(buffer, static_cast(i & 0xFF)); + } + benchmark::DoNotOptimize(buffer); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Int8ToStr); + +// --- base64_decode() - into pre-allocated buffer --- + +static void Base64Decode_32Bytes(benchmark::State &state) { + // 32 bytes encoded = 44 base64 chars + const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw=="; + size_t encoded_len = 44; + uint8_t output[32]; + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + base64_decode(encoded, encoded_len, output, sizeof(output)); + } + benchmark::DoNotOptimize(output); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Base64Decode_32Bytes); + } // namespace esphome::benchmarks From b8e4343d862515f3e71e5f9576c36dc681ab9012 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 20:04:44 -1000 Subject: [PATCH 2/7] [core] Optimize format_hex_internal by splitting separator loop Split the single loop into two paths (with/without separator) to eliminate the per-byte branch on separator. In the separator path, write the separator unconditionally and overwrite the last one with the null terminator. This also lets the compiler use constant stride values (2 or 3) instead of a runtime variable. Benchmarks show ~16-20% improvement on the separator path (format_hex_pretty_to, format_mac_addr_upper). --- esphome/core/helpers.cpp | 52 ++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 5940f6ec985..108d4488614 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -347,33 +347,49 @@ std::string format_mac_address_pretty(const uint8_t *mac) { return std::string(buf); } -// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase +// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. +// Splits into two loops to eliminate per-byte branch on separator. static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, char base) { if (length == 0) { buffer[0] = '\0'; return buffer; } - // With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator) - // Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator) - uint8_t stride = separator ? 3 : 2; - size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); - if (max_bytes == 0) { - buffer[0] = '\0'; - return buffer; - } - if (length > max_bytes) { - length = max_bytes; - } - for (size_t i = 0; i < length; i++) { - size_t pos = i * stride; - buffer[pos] = format_hex_char(data[i] >> 4, base); - buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); - if (separator && i < length - 1) { + if (separator) { + // With separator: "XX:XX:...:XX\0" = length * 3 bytes total + size_t max_bytes = buffer_size / 3; + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * 3; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); buffer[pos + 2] = separator; } + // Overwrite last separator with null terminator + buffer[length * 3 - 1] = '\0'; + } else { + // Without separator: "XXXX...XX\0" = length * 2 + 1 bytes total + size_t max_bytes = (buffer_size - 1) / 2; + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * 2; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + } + buffer[length * 2] = '\0'; } - buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } From 671e434400dc4009680418a38d4d7cd5567d08c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 20:25:35 -1000 Subject: [PATCH 3/7] [core] Force-inline format_hex_char to prevent outlining The loop split in format_hex_internal doubled the call sites for format_hex_char, causing the compiler to outline it (+30 bytes new symbol). Mark it always_inline to keep it inlined. --- esphome/core/helpers.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c26bbe17b75..1185db390e7 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1263,13 +1263,15 @@ constexpr uint8_t parse_hex_char(char c) { } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) -inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } +__attribute__((always_inline)) inline char format_hex_char(uint8_t v, char base) { + return v >= 10 ? base + (v - 10) : '0' + v; +} /// Convert a nibble (0-15) to lowercase hex char -inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } +__attribute__((always_inline)) inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } +__attribute__((always_inline)) inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. From bd66f26144d062a495bfb3b91eed2f4e0c063686 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Apr 2026 20:41:09 -1000 Subject: [PATCH 4/7] [core] Optimize format_hex_internal by eliminating per-byte length check Remove the per-byte `i < length - 1` check in the separator path by writing the separator unconditionally after each byte and overwriting the last one with the null terminator. Single-loop approach keeps code smaller than the original while eliminating the hot-path branch. Flash: -16 bytes vs baseline. --- esphome/core/helpers.cpp | 52 +++++++++++++++------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 108d4488614..2faa6adefb0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -348,48 +348,34 @@ std::string format_mac_address_pretty(const uint8_t *mac) { } // Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. -// Splits into two loops to eliminate per-byte branch on separator. +// When separator is set, it is written unconditionally after each byte and the last +// one is overwritten with '\0', eliminating the per-byte `i < length - 1` check. static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, char base) { if (length == 0) { buffer[0] = '\0'; return buffer; } - if (separator) { - // With separator: "XX:XX:...:XX\0" = length * 3 bytes total - size_t max_bytes = buffer_size / 3; - if (max_bytes == 0) { - buffer[0] = '\0'; - return buffer; - } - if (length > max_bytes) { - length = max_bytes; - } - for (size_t i = 0; i < length; i++) { - size_t pos = i * 3; - buffer[pos] = format_hex_char(data[i] >> 4, base); - buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + uint8_t stride = separator ? 3 : 2; + size_t max_bytes = separator ? (buffer_size / 3) : ((buffer_size - 1) / 2); + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * stride; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + if (separator) { buffer[pos + 2] = separator; } - // Overwrite last separator with null terminator - buffer[length * 3 - 1] = '\0'; - } else { - // Without separator: "XXXX...XX\0" = length * 2 + 1 bytes total - size_t max_bytes = (buffer_size - 1) / 2; - if (max_bytes == 0) { - buffer[0] = '\0'; - return buffer; - } - if (length > max_bytes) { - length = max_bytes; - } - for (size_t i = 0; i < length; i++) { - size_t pos = i * 2; - buffer[pos] = format_hex_char(data[i] >> 4, base); - buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); - } - buffer[length * 2] = '\0'; } + // With separator: overwrite last separator with '\0' + // Without: write '\0' after last hex char + buffer[length * stride - (separator ? 1 : 0)] = '\0'; return buffer; } From c945cbc64c9380ffe0d55dd666bb499ace9b501d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2026 08:10:39 -1000 Subject: [PATCH 5/7] bot comments --- esphome/core/helpers.cpp | 5 +++-- esphome/core/helpers.h | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2faa6adefb0..cbe22dd09aa 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -352,8 +352,9 @@ std::string format_mac_address_pretty(const uint8_t *mac) { // one is overwritten with '\0', eliminating the per-byte `i < length - 1` check. static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, char base) { - if (length == 0) { - buffer[0] = '\0'; + if (length == 0 || buffer_size == 0) { + if (buffer_size > 0) + buffer[0] = '\0'; return buffer; } uint8_t stride = separator ? 3 : 2; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 1185db390e7..3c42d7df076 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1263,15 +1263,13 @@ constexpr uint8_t parse_hex_char(char c) { } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) -__attribute__((always_inline)) inline char format_hex_char(uint8_t v, char base) { - return v >= 10 ? base + (v - 10) : '0' + v; -} +ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } /// Convert a nibble (0-15) to lowercase hex char -__attribute__((always_inline)) inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } +ESPHOME_ALWAYS_INLINE inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -__attribute__((always_inline)) inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } +ESPHOME_ALWAYS_INLINE inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. From a941422fa6c2997827dd776f8728f788312466a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2026 08:36:40 -1000 Subject: [PATCH 6/7] Add unit tests for format_hex helpers --- tests/components/core/test_helpers.cpp | 120 +++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/components/core/test_helpers.cpp diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp new file mode 100644 index 00000000000..c6fd0704468 --- /dev/null +++ b/tests/components/core/test_helpers.cpp @@ -0,0 +1,120 @@ +#include +#include + +#include "esphome/core/helpers.h" + +namespace esphome::testing { + +// --- format_hex_to() --- + +TEST(FormatHexTo, Basic) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[7]; // 3 * 2 + 1 + format_hex_to(buffer, data, 3); + EXPECT_STREQ(buffer, "abcdef"); +} + +TEST(FormatHexTo, SingleByte) { + const uint8_t data[] = {0x0F}; + char buffer[3]; + format_hex_to(buffer, data, 1); + EXPECT_STREQ(buffer, "0f"); +} + +TEST(FormatHexTo, ZeroLength) { + char buffer[4] = "xxx"; + format_hex_to(buffer, static_cast(sizeof(buffer)), static_cast(nullptr), 0); + EXPECT_STREQ(buffer, ""); +} + +TEST(FormatHexTo, ZeroBufferSize) { + char buffer[4] = "xxx"; + const uint8_t data[] = {0xAB}; + format_hex_to(buffer, static_cast(0), data, 1); + // Should not crash, buffer unchanged + EXPECT_EQ(buffer[0], 'x'); +} + +TEST(FormatHexTo, BufferTooSmall) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[5]; // only room for 2 bytes + format_hex_to(buffer, data, 3); + EXPECT_STREQ(buffer, "abcd"); +} + +TEST(FormatHexTo, MacAddress) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[13]; + format_hex_to(buffer, mac, 6); + EXPECT_STREQ(buffer, "aabbccddeeff"); +} + +// --- format_hex_pretty_to() --- + +TEST(FormatHexPrettyTo, BasicColon) { + const uint8_t data[] = {0xAB, 0xCD, 0xEF}; + char buffer[9]; // 3 * 3 + format_hex_pretty_to(buffer, data, 3); + EXPECT_STREQ(buffer, "AB:CD:EF"); +} + +TEST(FormatHexPrettyTo, SingleByte) { + const uint8_t data[] = {0x0F}; + char buffer[3]; + format_hex_pretty_to(buffer, data, 1); + EXPECT_STREQ(buffer, "0F"); +} + +TEST(FormatHexPrettyTo, ZeroLength) { + char buffer[4] = "xxx"; + format_hex_pretty_to(buffer, static_cast(sizeof(buffer)), static_cast(nullptr), 0); + EXPECT_STREQ(buffer, ""); +} + +TEST(FormatHexPrettyTo, ZeroBufferSize) { + char buffer[4] = "xxx"; + const uint8_t data[] = {0xAB}; + format_hex_pretty_to(buffer, static_cast(0), data, 1); + EXPECT_EQ(buffer[0], 'x'); +} + +TEST(FormatHexPrettyTo, CustomSeparator) { + const uint8_t data[] = {0xAA, 0xBB, 0xCC}; + char buffer[9]; + format_hex_pretty_to(buffer, data, 3, ':'); + EXPECT_STREQ(buffer, "AA:BB:CC"); +} + +// --- format_mac_addr_upper() --- + +TEST(FormatMacAddrUpper, Basic) { + const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac, buffer); + EXPECT_STREQ(buffer, "AA:BB:CC:DD:EE:FF"); +} + +TEST(FormatMacAddrUpper, AllZeros) { + const uint8_t mac[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac, buffer); + EXPECT_STREQ(buffer, "00:00:00:00:00:00"); +} + +// --- format_hex_char() --- + +TEST(FormatHexChar, LowercaseDigits) { + EXPECT_EQ(format_hex_char(0), '0'); + EXPECT_EQ(format_hex_char(9), '9'); + EXPECT_EQ(format_hex_char(10), 'a'); + EXPECT_EQ(format_hex_char(15), 'f'); +} + +TEST(FormatHexChar, UppercaseDigits) { + EXPECT_EQ(format_hex_pretty_char(0), '0'); + EXPECT_EQ(format_hex_pretty_char(9), '9'); + EXPECT_EQ(format_hex_pretty_char(10), 'A'); + EXPECT_EQ(format_hex_pretty_char(15), 'F'); +} + +} // namespace esphome::testing From 6fe6bf79d4dc7b0ab7fb56a1fd9524a9ab870454 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2026 09:30:06 -1000 Subject: [PATCH 7/7] Address Copilot review: fix namespace and custom separator test --- tests/components/core/test_helpers.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index c6fd0704468..00169621c34 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -3,7 +3,7 @@ #include "esphome/core/helpers.h" -namespace esphome::testing { +namespace esphome::core::testing { // --- format_hex_to() --- @@ -81,8 +81,8 @@ TEST(FormatHexPrettyTo, ZeroBufferSize) { TEST(FormatHexPrettyTo, CustomSeparator) { const uint8_t data[] = {0xAA, 0xBB, 0xCC}; char buffer[9]; - format_hex_pretty_to(buffer, data, 3, ':'); - EXPECT_STREQ(buffer, "AA:BB:CC"); + format_hex_pretty_to(buffer, data, 3, '-'); + EXPECT_STREQ(buffer, "AA-BB-CC"); } // --- format_mac_addr_upper() --- @@ -117,4 +117,4 @@ TEST(FormatHexChar, UppercaseDigits) { EXPECT_EQ(format_hex_pretty_char(15), 'F'); } -} // namespace esphome::testing +} // namespace esphome::core::testing