diff --git a/tests/unit/test_round_robin.cpp b/tests/unit/test_round_robin.cpp new file mode 100644 index 00000000000..7e740c04c4f --- /dev/null +++ b/tests/unit/test_round_robin.cpp @@ -0,0 +1,184 @@ +/** + * @file tests/unit/test_round_robin.cpp + * @brief Test src/round_robin.h. + */ +#include "../tests_common.h" + +#include + +#include + +// ========== Basic iteration tests ========== + +TEST(RoundRobinTests, WrapsAroundOnIncrement) { + std::vector data = {10, 20, 30}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + EXPECT_EQ(*rr, 10); + ++rr; + EXPECT_EQ(*rr, 20); + ++rr; + EXPECT_EQ(*rr, 30); + ++rr; + // Should wrap around to the beginning + EXPECT_EQ(*rr, 10); +} + +TEST(RoundRobinTests, WrapsAroundOnDecrement) { + std::vector data = {10, 20, 30}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + // Decrement from the start should wrap to end + --rr; + EXPECT_EQ(*rr, 30); + --rr; + EXPECT_EQ(*rr, 20); + --rr; + EXPECT_EQ(*rr, 10); +} + +TEST(RoundRobinTests, PostIncrement) { + std::vector data = {1, 2, 3}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + auto prev = rr++; + EXPECT_EQ(*prev, 1); + EXPECT_EQ(*rr, 2); +} + +TEST(RoundRobinTests, PostDecrement) { + std::vector data = {1, 2, 3}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + ++rr; // move to 2 + + auto prev = rr--; + EXPECT_EQ(*prev, 2); + EXPECT_EQ(*rr, 1); +} + +// ========== Arithmetic operator tests ========== + +TEST(RoundRobinTests, PlusEqualsAdvancesMultipleSteps) { + std::vector data = {10, 20, 30, 40, 50}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + rr += 3; + EXPECT_EQ(*rr, 40); +} + +TEST(RoundRobinTests, PlusEqualsWrapsAround) { + std::vector data = {10, 20, 30}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + rr += 5; // wraps: 10->20->30->10->20->30... position 5 mod 3 = 2 + EXPECT_EQ(*rr, 30); +} + +TEST(RoundRobinTests, MinusEqualsRewindsMultipleSteps) { + std::vector data = {10, 20, 30, 40, 50}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + rr += 4; // at 50 + rr -= 2; // back to 30 + EXPECT_EQ(*rr, 30); +} + +TEST(RoundRobinTests, PlusOperatorDoesNotModifyOriginal) { + std::vector data = {10, 20, 30}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + auto rr2 = rr + 2; + EXPECT_EQ(*rr, 10); // original unchanged + EXPECT_EQ(*rr2, 30); +} + +TEST(RoundRobinTests, MinusOperatorDoesNotModifyOriginal) { + std::vector data = {10, 20, 30}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + rr += 2; // at 30 + + auto rr2 = rr - 1; + EXPECT_EQ(*rr, 30); // original unchanged + EXPECT_EQ(*rr2, 20); +} + +// ========== Comparison operator tests ========== + +TEST(RoundRobinTests, EqualityWhenSamePosition) { + std::vector data = {10, 20, 30}; + auto rr1 = round_robin_util::make_round_robin(data.begin(), data.end()); + auto rr2 = round_robin_util::make_round_robin(data.begin(), data.end()); + + EXPECT_TRUE(rr1 == rr2); + EXPECT_FALSE(rr1 != rr2); +} + +TEST(RoundRobinTests, InequalityWhenDifferentPosition) { + std::vector data = {10, 20, 30}; + auto rr1 = round_robin_util::make_round_robin(data.begin(), data.end()); + auto rr2 = round_robin_util::make_round_robin(data.begin(), data.end()); + ++rr2; + + EXPECT_FALSE(rr1 == rr2); + EXPECT_TRUE(rr1 != rr2); +} + +// ========== Difference operator tests ========== + +TEST(RoundRobinTests, DifferenceOperator) { + std::vector data = {10, 20, 30, 40, 50}; + auto rr1 = round_robin_util::make_round_robin(data.begin(), data.end()); + auto rr2 = round_robin_util::make_round_robin(data.begin(), data.end()); + rr2 += 3; + + auto diff = rr2 - rr1; + EXPECT_EQ(diff, 3); +} + +// ========== Single element tests ========== + +TEST(RoundRobinTests, SingleElementAlwaysReturnsSame) { + std::vector data = {42}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + EXPECT_EQ(*rr, 42); + ++rr; + EXPECT_EQ(*rr, 42); + ++rr; + EXPECT_EQ(*rr, 42); +} + +// ========== Multiple full cycles ========== + +TEST(RoundRobinTests, MultipleFullCycles) { + std::vector data = {1, 2, 3}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + // Go around twice + for (int cycle = 0; cycle < 2; ++cycle) { + EXPECT_EQ(*rr, 1); + ++rr; + EXPECT_EQ(*rr, 2); + ++rr; + EXPECT_EQ(*rr, 3); + ++rr; + } +} + +// ========== Pointer dereference test ========== + +TEST(RoundRobinTests, ArrowOperator) { + struct Item { + int value; + std::string name; + }; + + std::vector data = {{1, "one"}, {2, "two"}, {3, "three"}}; + auto rr = round_robin_util::make_round_robin(data.begin(), data.end()); + + EXPECT_EQ(rr->value, 1); + EXPECT_EQ(rr->name, "one"); + ++rr; + EXPECT_EQ(rr->value, 2); + EXPECT_EQ(rr->name, "two"); +} diff --git a/tests/unit/test_stat_trackers.cpp b/tests/unit/test_stat_trackers.cpp new file mode 100644 index 00000000000..32cc33c5872 --- /dev/null +++ b/tests/unit/test_stat_trackers.cpp @@ -0,0 +1,224 @@ +/** + * @file tests/unit/test_stat_trackers.cpp + * @brief Test src/stat_trackers.h and src/stat_trackers.cpp. + */ +#include "../tests_common.h" + +#include + +#include + +// ========== Format helper tests ========== + +TEST(StatTrackersFormatTests, OneDigitAfterDecimal) { + auto fmt = stat_trackers::one_digit_after_decimal(); + std::string result = (fmt % 3.14159).str(); + EXPECT_EQ(result, "3.1"); +} + +TEST(StatTrackersFormatTests, OneDigitAfterDecimalRoundsUp) { + auto fmt = stat_trackers::one_digit_after_decimal(); + std::string result = (fmt % 3.95).str(); + EXPECT_EQ(result, "4.0"); +} + +TEST(StatTrackersFormatTests, OneDigitAfterDecimalZero) { + auto fmt = stat_trackers::one_digit_after_decimal(); + std::string result = (fmt % 0.0).str(); + EXPECT_EQ(result, "0.0"); +} + +TEST(StatTrackersFormatTests, TwoDigitsAfterDecimal) { + auto fmt = stat_trackers::two_digits_after_decimal(); + std::string result = (fmt % 3.14159).str(); + EXPECT_EQ(result, "3.14"); +} + +TEST(StatTrackersFormatTests, TwoDigitsAfterDecimalRoundsUp) { + auto fmt = stat_trackers::two_digits_after_decimal(); + std::string result = (fmt % 3.999).str(); + EXPECT_EQ(result, "4.00"); +} + +TEST(StatTrackersFormatTests, TwoDigitsAfterDecimalZero) { + auto fmt = stat_trackers::two_digits_after_decimal(); + std::string result = (fmt % 0.0).str(); + EXPECT_EQ(result, "0.00"); +} + +TEST(StatTrackersFormatTests, TwoDigitsAfterDecimalNegative) { + auto fmt = stat_trackers::two_digits_after_decimal(); + std::string result = (fmt % -1.5).str(); + EXPECT_EQ(result, "-1.50"); +} + +// ========== min_max_avg_tracker tests ========== + +TEST(StatTrackersMinMaxAvgTests, CallbackNotCalledBeforeInterval) { + stat_trackers::min_max_avg_tracker tracker; + + bool callback_called = false; + auto callback = [&](int, int, double) { + callback_called = true; + }; + + // First call initializes the timer + tracker.collect_and_callback_on_interval(10, callback, std::chrono::seconds(60)); + EXPECT_FALSE(callback_called); + + // Second call within interval should not trigger callback + tracker.collect_and_callback_on_interval(20, callback, std::chrono::seconds(60)); + EXPECT_FALSE(callback_called); +} + +TEST(StatTrackersMinMaxAvgTests, CallbackCalledAfterInterval) { + stat_trackers::min_max_avg_tracker tracker; + + int result_min = 0; + int result_max = 0; + double result_avg = 0; + bool callback_called = false; + + auto callback = [&](int stat_min, int stat_max, double stat_avg) { + result_min = stat_min; + result_max = stat_max; + result_avg = stat_avg; + callback_called = true; + }; + + // Use a very short interval for testing + auto interval = std::chrono::seconds(0); + + // First call sets the timer + tracker.collect_and_callback_on_interval(10, callback, interval); + EXPECT_FALSE(callback_called); + + // Wait a tiny bit so time passes + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + // Second call should trigger callback since interval has passed + tracker.collect_and_callback_on_interval(20, callback, interval); + EXPECT_TRUE(callback_called); + + // The callback should have received stats from the first collection + EXPECT_EQ(result_min, 10); + EXPECT_EQ(result_max, 10); + EXPECT_DOUBLE_EQ(result_avg, 10.0); +} + +TEST(StatTrackersMinMaxAvgTests, TracksMinMaxAvgCorrectly) { + stat_trackers::min_max_avg_tracker tracker; + + int result_min = 0; + int result_max = 0; + double result_avg = 0; + bool callback_called = false; + + auto callback = [&](int stat_min, int stat_max, double stat_avg) { + result_min = stat_min; + result_max = stat_max; + result_avg = stat_avg; + callback_called = true; + }; + + auto interval = std::chrono::seconds(0); + + // Collect multiple values + tracker.collect_and_callback_on_interval(5, callback, interval); + + // Wait so interval passes + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + // Collect more values (these will be the "previous" batch reported) + tracker.collect_and_callback_on_interval(15, callback, interval); + + // First batch only had value 5 + EXPECT_TRUE(callback_called); + EXPECT_EQ(result_min, 5); + EXPECT_EQ(result_max, 5); + EXPECT_DOUBLE_EQ(result_avg, 5.0); +} + +TEST(StatTrackersMinMaxAvgTests, ResetClearsState) { + stat_trackers::min_max_avg_tracker tracker; + + bool callback_called = false; + auto callback = [&](int, int, double) { + callback_called = true; + }; + + // Collect some values + tracker.collect_and_callback_on_interval(100, callback, std::chrono::seconds(60)); + + // Reset + tracker.reset(); + + // After reset, first collect should reinitialize timer (not trigger callback) + tracker.collect_and_callback_on_interval(50, callback, std::chrono::seconds(0)); + EXPECT_FALSE(callback_called); +} + +TEST(StatTrackersMinMaxAvgTests, MultipleValuesInBatch) { + stat_trackers::min_max_avg_tracker tracker; + + int result_min = 0; + int result_max = 0; + double result_avg = 0; + bool callback_called = false; + + auto callback = [&](int stat_min, int stat_max, double stat_avg) { + result_min = stat_min; + result_max = stat_max; + result_avg = stat_avg; + callback_called = true; + }; + + // Use a longer interval so we can collect multiple values + auto interval = std::chrono::seconds(0); + + // First call initializes timer + tracker.collect_and_callback_on_interval(3, callback, std::chrono::seconds(60)); + tracker.collect_and_callback_on_interval(7, callback, std::chrono::seconds(60)); + tracker.collect_and_callback_on_interval(5, callback, std::chrono::seconds(60)); + + EXPECT_FALSE(callback_called); + + // Now wait and trigger callback + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + tracker.collect_and_callback_on_interval(100, callback, interval); + + EXPECT_TRUE(callback_called); + EXPECT_EQ(result_min, 3); + EXPECT_EQ(result_max, 7); + EXPECT_DOUBLE_EQ(result_avg, 5.0); // (3+7+5) / 3 +} + +TEST(StatTrackersMinMaxAvgTests, WorksWithDoubleType) { + stat_trackers::min_max_avg_tracker tracker; + + double result_min = 0; + double result_max = 0; + double result_avg = 0; + bool callback_called = false; + + auto callback = [&](double stat_min, double stat_max, double stat_avg) { + result_min = stat_min; + result_max = stat_max; + result_avg = stat_avg; + callback_called = true; + }; + + tracker.collect_and_callback_on_interval(1.5, callback, std::chrono::seconds(60)); + tracker.collect_and_callback_on_interval(2.5, callback, std::chrono::seconds(60)); + tracker.collect_and_callback_on_interval(3.5, callback, std::chrono::seconds(60)); + + EXPECT_FALSE(callback_called); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + tracker.collect_and_callback_on_interval(0.0, callback, std::chrono::seconds(0)); + + EXPECT_TRUE(callback_called); + EXPECT_DOUBLE_EQ(result_min, 1.5); + EXPECT_DOUBLE_EQ(result_max, 3.5); + EXPECT_DOUBLE_EQ(result_avg, 2.5); // (1.5+2.5+3.5) / 3 +} diff --git a/tests/unit/test_utility.cpp b/tests/unit/test_utility.cpp new file mode 100644 index 00000000000..fe65910d70b --- /dev/null +++ b/tests/unit/test_utility.cpp @@ -0,0 +1,344 @@ +/** + * @file tests/unit/test_utility.cpp + * @brief Test src/utility.h. + */ +#include "../tests_common.h" + +#include + +// ========== Hex conversion tests ========== + +struct HexConversionTest: testing::TestWithParam> {}; + +TEST_P(HexConversionTest, ToStringProducesExpectedHex) { + auto [input, rev, expected] = GetParam(); + auto hex = util::hex(input, rev); + EXPECT_EQ(hex.to_string(), expected); +} + +INSTANTIATE_TEST_SUITE_P( + UtilityTests, + HexConversionTest, + testing::Values( + std::make_tuple(0x00000000, false, "00000000"), + std::make_tuple(0xDEADBEEF, false, "DEADBEEF"), + std::make_tuple(0x12345678, false, "12345678"), + std::make_tuple(0x000000FF, false, "000000FF"), + std::make_tuple(0xDEADBEEF, true, "EFBEADDE"), + std::make_tuple(0x12345678, true, "78563412") + ) +); + +struct HexUint8Test: testing::TestWithParam> {}; + +TEST_P(HexUint8Test, SingleByteHex) { + auto [input, rev, expected] = GetParam(); + auto hex = util::hex(input, rev); + EXPECT_EQ(hex.to_string(), expected); +} + +INSTANTIATE_TEST_SUITE_P( + UtilityTests, + HexUint8Test, + testing::Values( + std::make_tuple(uint8_t {0x00}, false, "00"), + std::make_tuple(uint8_t {0xFF}, false, "FF"), + std::make_tuple(uint8_t {0xAB}, false, "AB"), + std::make_tuple(uint8_t {0x0F}, false, "0F") + ) +); + +// ========== hex_vec tests ========== + +TEST(UtilityHexVecTests, VectorToHexStringReversed) { + std::vector data = {0xDE, 0xAD, 0xBE, 0xEF}; + std::string result = util::hex_vec(data, true); + EXPECT_EQ(result, "DEADBEEF"); +} + +TEST(UtilityHexVecTests, VectorToHexStringNonReversed) { + std::vector data = {0xDE, 0xAD, 0xBE, 0xEF}; + std::string result = util::hex_vec(data, false); + EXPECT_EQ(result, "EFBEADDE"); +} + +TEST(UtilityHexVecTests, EmptyVector) { + std::vector data = {}; + std::string result = util::hex_vec(data, true); + EXPECT_EQ(result, ""); +} + +TEST(UtilityHexVecTests, SingleByte) { + std::vector data = {0x42}; + std::string result = util::hex_vec(data, true); + EXPECT_EQ(result, "42"); +} + + +// ========== from_hex tests ========== + +TEST(UtilityFromHexTests, ParseHexToUint32) { + auto result = util::from_hex("DEADBEEF", true); + EXPECT_EQ(result, 0xDEADBEEF); +} + +TEST(UtilityFromHexTests, ParseHexToUint32NonReversed) { + auto result = util::from_hex("DEADBEEF", false); + EXPECT_EQ(result, 0xEFBEADDE); +} + +TEST(UtilityFromHexTests, ParseHexLowercase) { + auto result = util::from_hex("deadbeef", true); + EXPECT_EQ(result, 0xDEADBEEF); +} + +TEST(UtilityFromHexTests, ParseHexToUint16) { + auto result = util::from_hex("ABCD", true); + EXPECT_EQ(result, 0xABCD); +} + +TEST(UtilityFromHexTests, ParseHexWithSeparators) { + // from_hex skips non-hex characters + auto result = util::from_hex("DE:AD:BE:EF", true); + EXPECT_EQ(result, 0xDEADBEEF); +} + +// ========== from_hex_vec tests ========== + +TEST(UtilityFromHexVecTests, ParseHexStringToBytes) { + std::string result = util::from_hex_vec("DEADBEEF", true); + EXPECT_EQ(result.size(), 4); + EXPECT_EQ(static_cast(result[0]), 0xDE); + EXPECT_EQ(static_cast(result[1]), 0xAD); + EXPECT_EQ(static_cast(result[2]), 0xBE); + EXPECT_EQ(static_cast(result[3]), 0xEF); +} + +TEST(UtilityFromHexVecTests, ParseHexStringNonReversed) { + std::string result = util::from_hex_vec("DEADBEEF", false); + EXPECT_EQ(result.size(), 4); + EXPECT_EQ(static_cast(result[0]), 0xEF); + EXPECT_EQ(static_cast(result[1]), 0xBE); + EXPECT_EQ(static_cast(result[2]), 0xAD); + EXPECT_EQ(static_cast(result[3]), 0xDE); +} + +// ========== from_chars / from_view tests ========== + +struct FromViewTest: testing::TestWithParam> {}; + +TEST_P(FromViewTest, ParsesCorrectly) { + auto [input, expected] = GetParam(); + EXPECT_EQ(util::from_view(input), expected); +} + +INSTANTIATE_TEST_SUITE_P( + UtilityTests, + FromViewTest, + testing::Values( + std::make_tuple("0", int64_t {0}), + std::make_tuple("1", int64_t {1}), + std::make_tuple("42", int64_t {42}), + std::make_tuple("12345", int64_t {12345}), + std::make_tuple("-1", int64_t {-1}), + std::make_tuple("-999", int64_t {-999}), + std::make_tuple("2147483647", int64_t {2147483647}), + std::make_tuple("-2147483648", int64_t {-2147483648LL}) + ) +); + +TEST(UtilityFromViewTests, EmptyStringReturnsZero) { + EXPECT_EQ(util::from_view(""), 0); +} + +// ========== Either tests ========== + +TEST(UtilityEitherTests, HasLeftWhenConstructedWithLeft) { + util::Either either {std::in_place_type, 42}; + EXPECT_TRUE(either.has_left()); + EXPECT_FALSE(either.has_right()); + EXPECT_EQ(either.left(), 42); +} + +TEST(UtilityEitherTests, HasRightWhenConstructedWithRight) { + util::Either either {std::in_place_type, "hello"}; + EXPECT_FALSE(either.has_left()); + EXPECT_TRUE(either.has_right()); + EXPECT_EQ(either.right(), "hello"); +} + +TEST(UtilityEitherTests, DefaultConstructedHasNeither) { + util::Either either; + EXPECT_FALSE(either.has_left()); + EXPECT_FALSE(either.has_right()); +} + +// ========== FailGuard tests ========== + +TEST(UtilityFailGuardTests, ExecutesOnDestruction) { + bool executed = false; + { + auto guard = util::fail_guard([&]() { executed = true; }); + } + EXPECT_TRUE(executed); +} + +TEST(UtilityFailGuardTests, DoesNotExecuteWhenDisabled) { + bool executed = false; + { + auto guard = util::fail_guard([&]() { executed = true; }); + guard.disable(); + } + EXPECT_FALSE(executed); +} + +TEST(UtilityFailGuardTests, MoveDoesNotDoubleExecute) { + int count = 0; + { + auto guard1 = util::fail_guard([&]() { count++; }); + auto guard2 = std::move(guard1); + } + EXPECT_EQ(count, 1); +} + +// ========== buffer_t tests ========== + +TEST(UtilityBufferTests, ConstructWithSize) { + util::buffer_t buf(10); + EXPECT_EQ(buf.size(), 10u); +} + +TEST(UtilityBufferTests, ConstructWithSizeAndValue) { + util::buffer_t buf(5, 42); + for (size_t i = 0; i < buf.size(); ++i) { + EXPECT_EQ(buf[i], 42); + } +} + +TEST(UtilityBufferTests, DefaultConstructIsEmpty) { + util::buffer_t buf; + EXPECT_EQ(buf.size(), 0u); +} + +TEST(UtilityBufferTests, IndexAccess) { + util::buffer_t buf(3); + buf[0] = 10; + buf[1] = 20; + buf[2] = 30; + EXPECT_EQ(buf[0], 10); + EXPECT_EQ(buf[1], 20); + EXPECT_EQ(buf[2], 30); +} + +TEST(UtilityBufferTests, BeginEndIterators) { + util::buffer_t buf(3, 7); + int sum = 0; + for (auto it = buf.begin(); it != buf.end(); ++it) { + sum += *it; + } + EXPECT_EQ(sum, 21); +} + +TEST(UtilityBufferTests, MoveConstruction) { + util::buffer_t buf1(3, 99); + util::buffer_t buf2(std::move(buf1)); + EXPECT_EQ(buf2.size(), 3u); + EXPECT_EQ(buf2[0], 99); + EXPECT_EQ(buf1.size(), 0u); +} + +TEST(UtilityBufferTests, CopyConstruction) { + util::buffer_t buf1(3, 55); + util::buffer_t buf2(buf1); + EXPECT_EQ(buf2.size(), 3u); + EXPECT_EQ(buf2[0], 55); + // original unchanged + EXPECT_EQ(buf1.size(), 3u); + EXPECT_EQ(buf1[0], 55); +} + +// ========== append_struct tests ========== + +TEST(UtilityAppendStructTests, AppendsDataCorrectly) { + struct TestStruct { + uint8_t a; + uint8_t b; + uint8_t c; + }; + + TestStruct s {0xAA, 0xBB, 0xCC}; + std::vector buf; + util::append_struct(buf, s); + + EXPECT_GE(buf.size(), 3u); + EXPECT_EQ(buf[0], 0xAA); + EXPECT_EQ(buf[1], 0xBB); + EXPECT_EQ(buf[2], 0xCC); +} + +// ========== endian tests ========== + +TEST(UtilityEndianTests, BigEndianConversion) { + uint32_t val = 0x01020304; + auto big = util::endian::big(val); + + // On little-endian systems, big() should reverse bytes + auto *bytes = reinterpret_cast(&big); + if constexpr (util::endian::endianness<>::little) { + EXPECT_EQ(bytes[0], 0x04); + EXPECT_EQ(bytes[1], 0x03); + EXPECT_EQ(bytes[2], 0x02); + EXPECT_EQ(bytes[3], 0x01); + } else { + EXPECT_EQ(bytes[0], 0x01); + EXPECT_EQ(bytes[1], 0x02); + EXPECT_EQ(bytes[2], 0x03); + EXPECT_EQ(bytes[3], 0x04); + } +} + +TEST(UtilityEndianTests, LittleEndianConversion) { + uint32_t val = 0x01020304; + auto little_val = util::endian::little(val); + + auto *bytes = reinterpret_cast(&little_val); + if constexpr (util::endian::endianness<>::little) { + // Already little endian, should be unchanged + EXPECT_EQ(bytes[0], 0x04); + EXPECT_EQ(bytes[1], 0x03); + EXPECT_EQ(bytes[2], 0x02); + EXPECT_EQ(bytes[3], 0x01); + } +} + +TEST(UtilityEndianTests, RoundTripBigEndian) { + uint32_t original = 0xDEADBEEF; + auto converted = util::endian::big(util::endian::big(original)); + EXPECT_EQ(converted, original); +} + +TEST(UtilityEndianTests, RoundTripLittleEndian) { + uint32_t original = 0xCAFEBABE; + auto converted = util::endian::little(util::endian::little(original)); + EXPECT_EQ(converted, original); +} + +// ========== log_hex tests ========== + +TEST(UtilityLogHexTests, FormatsWithPrefix) { + uint8_t val = 0xAB; + std::string result = util::log_hex(val); + EXPECT_EQ(result, "0xAB"); +} + +TEST(UtilityLogHexTests, FormatsZero) { + uint8_t val = 0x00; + std::string result = util::log_hex(val); + EXPECT_EQ(result, "0x00"); +} + +TEST(UtilityLogHexTests, Formats16Bit) { + uint16_t val = 0x1234; + std::string result = util::log_hex(val); + EXPECT_EQ(result, "0x1234"); +}