/* * blebus unit tests — unix socket transport only (no BLE hardware needed). * * Tests the broker, Bus read/write, framing, broadcast, and timeouts * using the local unix socket path. The BLE L2CAP listener is NOT * exercised here: it needs a real adapter, so it is smoke-tested by * hand against one and a green run of this suite says nothing about it. * * Copyright (c) 2026 Are Bjørby * SPDX-License-Identifier: MIT */ #include "blebus.hpp" #include #include #include #include #include #include #include #include static int g_pass = 0; static int g_fail = 0; #define TEST(name) \ static void test_##name(); \ struct Reg_##name { Reg_##name() { tests.push_back({#name, test_##name}); } }; \ static Reg_##name reg_##name; \ static void test_##name() #define ASSERT(cond) do { \ if (!(cond)) { \ std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ g_fail++; \ return; \ } \ } while (0) struct TestEntry { const char* name; void (*fn)(); }; static std::vector tests; /* ── Helpers ──────────────────────────────────────────────────────────── */ static constexpr const char* BUS_NAME = "blebus_test"; struct ScopedBus { ScopedBus() { blebus::create(BUS_NAME); } ~ScopedBus() { try { blebus::destroy(BUS_NAME); } catch (...) {} } }; /* ── Tests ────────────────────────────────────────────────────────────── */ TEST(create_destroy) { blebus::create(BUS_NAME); blebus::destroy(BUS_NAME); } TEST(create_duplicate_throws) { ScopedBus sb; bool threw = false; try { blebus::create(BUS_NAME); } catch (const std::system_error& e) { threw = (e.code().value() == EEXIST); } ASSERT(threw); } TEST(destroy_nonexistent_throws) { bool threw = false; try { blebus::destroy("nonexistent_bus_xyz"); } catch (const std::system_error& e) { threw = (e.code().value() == ENOENT); } ASSERT(threw); } TEST(bus_connect) { ScopedBus sb; blebus::Bus bus(BUS_NAME); ASSERT(bus.name() == BUS_NAME); } TEST(bus_connect_no_broker_throws) { bool threw = false; try { blebus::Bus bus("nonexistent_bus_xyz"); } catch (const std::system_error&) { threw = true; } ASSERT(threw); } TEST(write_read_echo) { ScopedBus sb; blebus::Bus bus(BUS_NAME); const char* msg = "hello blebus"; bus.write(std::string_view(msg)); /* Give broker time to relay. */ std::this_thread::sleep_for(std::chrono::milliseconds(50)); uint8_t buf[256]; size_t n = bus.read(buf, sizeof(buf)); ASSERT(n == std::strlen(msg)); ASSERT(std::memcmp(buf, msg, n) == 0); } TEST(write_read_wait) { ScopedBus sb; blebus::Bus bus(BUS_NAME); const char* msg = "blocking read test"; /* Write in a background thread after a short delay. */ std::thread writer([&] { std::this_thread::sleep_for(std::chrono::milliseconds(50)); bus.write(std::string_view(msg)); }); uint8_t buf[256]; size_t n = bus.read_wait(buf, sizeof(buf), 2000); writer.join(); ASSERT(n == std::strlen(msg)); ASSERT(std::memcmp(buf, msg, n) == 0); } TEST(read_wait_timeout) { ScopedBus sb; blebus::Bus bus(BUS_NAME); auto t0 = std::chrono::steady_clock::now(); uint8_t buf[64]; size_t n = bus.read_wait(buf, sizeof(buf), 100); auto elapsed = std::chrono::steady_clock::now() - t0; ASSERT(n == 0); ASSERT(elapsed >= std::chrono::milliseconds(80)); ASSERT(elapsed < std::chrono::milliseconds(500)); } TEST(read_nonblocking_empty) { ScopedBus sb; blebus::Bus bus(BUS_NAME); uint8_t buf[64]; size_t n = bus.read(buf, sizeof(buf)); ASSERT(n == 0); } TEST(broadcast_two_readers) { ScopedBus sb; blebus::Bus a(BUS_NAME); blebus::Bus b(BUS_NAME); const char* msg = "broadcast"; a.write(std::string_view(msg)); std::this_thread::sleep_for(std::chrono::milliseconds(50)); uint8_t buf[64]; /* Both readers should see the message (including sender). */ size_t na = a.read(buf, sizeof(buf)); ASSERT(na == std::strlen(msg)); size_t nb = b.read(buf, sizeof(buf)); ASSERT(nb == std::strlen(msg)); ASSERT(std::memcmp(buf, msg, nb) == 0); } TEST(large_message) { ScopedBus sb; blebus::Bus bus(BUS_NAME); std::vector data(8192); for (size_t i = 0; i < data.size(); i++) data[i] = static_cast(i & 0xFF); bus.write(data.data(), data.size()); std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::vector buf(data.size() + 64); size_t n = bus.read(buf.data(), buf.size()); ASSERT(n == data.size()); ASSERT(std::memcmp(buf.data(), data.data(), n) == 0); } TEST(write_oversize_throws) { ScopedBus sb; blebus::Bus bus(BUS_NAME); std::vector data(blebus::MAX_MSG_SIZE + 1, 0x42); bool threw = false; try { bus.write(data.data(), data.size()); } catch (const std::system_error& e) { threw = (e.code().value() == EMSGSIZE); } ASSERT(threw); } TEST(multiple_messages) { ScopedBus sb; blebus::Bus bus(BUS_NAME); for (int i = 0; i < 5; i++) { std::string msg = "msg-" + std::to_string(i); bus.write(std::string_view(msg)); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); for (int i = 0; i < 5; i++) { uint8_t buf[64]; size_t n = bus.read(buf, sizeof(buf)); std::string expected = "msg-" + std::to_string(i); ASSERT(n == expected.size()); ASSERT(std::memcmp(buf, expected.data(), n) == 0); } } TEST(set_reader_name) { ScopedBus sb; blebus::Bus bus(BUS_NAME); bus.set_reader_name("test-reader"); ASSERT(bus.name() == BUS_NAME); } TEST(string_view_write) { ScopedBus sb; blebus::Bus bus(BUS_NAME); std::string_view sv = "string_view test"; bus.write(sv); std::this_thread::sleep_for(std::chrono::milliseconds(50)); uint8_t buf[64]; size_t n = bus.read(buf, sizeof(buf)); ASSERT(n == sv.size()); ASSERT(std::memcmp(buf, sv.data(), n) == 0); } /* ── Main ─────────────────────────────────────────────────────────────── */ int main() { std::printf("blebus unit tests (%zu tests)\n", tests.size()); for (auto& t : tests) { std::printf(" %-30s ", t.name); t.fn(); if (g_fail == 0) { std::printf("OK\n"); g_pass++; } g_fail = 0; } std::printf("\n%d passed, %d failed\n", g_pass, static_cast(tests.size()) - g_pass); return (g_pass == static_cast(tests.size())) ? 0 : 1; }