/* * unit_test.cpp — SockBus C++17 unit tests * Copyright (c) 2025-2026 Are Bjørby * SPDX-License-Identifier: MIT */ #include "sockbus.hpp" #include #include #include #include #include #include #include static int tests_run; static int tests_passed; #define RUN(name) do { \ std::printf(" %-50s ", #name); \ std::fflush(stdout); \ tests_run++; \ if (test_##name()) { tests_passed++; std::printf("PASS\n"); } \ else std::printf("FAIL (line %d)\n", __LINE__); \ } while (0) /* ── Helpers ─────────────────────────────────────────────────────────── */ static char conf_path[256]; static void setup_config() { std::snprintf(conf_path, sizeof(conf_path), "/tmp/sockbus_test_%d.conf", getpid()); std::ofstream f(conf_path); f << "# sockbus test config\n" << "test_bus 127.0.0.1:19100\n" << "test_bus2 127.0.0.1:19101\n" << "test_bus3 127.0.0.1:19103\n"; setenv("SOCKBUS_CONF", conf_path, 1); } static void teardown_config() { unlink(conf_path); unsetenv("SOCKBUS_CONF"); } static void settle() { usleep(80000); } /* Safe bus cleanup (ignores errors) */ static void safe_destroy(const char* name) { try { sockbus::destroy(name); } catch (...) {} } /* ── Lifecycle tests ─────────────────────────────────────────────────── */ static int test_create_destroy() { try { sockbus::create("test_bus"); settle(); sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } static int test_create_duplicate() { try { sockbus::create("test_bus"); settle(); } catch (...) { return 0; } try { sockbus::create("test_bus"); safe_destroy("test_bus"); return 0; } catch (const std::system_error& e) { safe_destroy("test_bus"); return (e.code().value() == EEXIST); } catch (...) { safe_destroy("test_bus"); return 0; } } static int test_destroy_unknown() { try { sockbus::destroy("nonexistent"); return 0; } catch (const std::system_error& e) { return (e.code().value() == ENOENT); } } static int test_create_empty_name() { try { sockbus::create(""); return 0; } catch (const std::system_error& e) { return (e.code().value() == EINVAL); } } static int test_destroy_empty_name() { try { sockbus::destroy(""); return 0; } catch (const std::system_error& e) { return (e.code().value() == EINVAL); } } static int test_create_custom_size() { try { sockbus::create("test_bus", 4096); settle(); { sockbus::Bus bus("test_bus"); } sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } static int test_broker_reuse() { try { sockbus::create("test_bus"); settle(); sockbus::destroy("test_bus"); settle(); sockbus::create("test_bus"); settle(); { sockbus::Bus bus("test_bus"); } sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } /* ── Open/close tests ────────────────────────────────────────────────── */ static int test_open_no_bus() { try { sockbus::Bus bus("test_bus"); return 0; } catch (...) { return 1; } } static int test_open_close() { try { sockbus::create("test_bus"); settle(); { sockbus::Bus bus("test_bus"); } sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } static int test_open_empty_name() { try { sockbus::Bus bus(""); return 0; } catch (const std::system_error& e) { return (e.code().value() == EINVAL); } } static int test_open_multiple_handles() { try { sockbus::create("test_bus"); settle(); sockbus::Bus h1("test_bus"); sockbus::Bus h2("test_bus"); sockbus::Bus h3("test_bus"); sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } /* ── Write tests ─────────────────────────────────────────────────────── */ static int test_write_read() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus reader("test_bus"); settle(); const char* msg = "hello sockbus"; size_t w = writer.write(std::string_view(msg, std::strlen(msg))); if (w != std::strlen(msg)) return 0; settle(); uint8_t buf[256] = {}; size_t r = reader.read(buf, sizeof(buf)); int ok = (r == std::strlen(msg) && std::memcmp(buf, msg, std::strlen(msg)) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_write_null_data() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.write(nullptr, 10); sockbus::destroy("test_bus"); return 0; } catch (const std::system_error& e) { safe_destroy("test_bus"); return (e.code().value() == EINVAL); } } static int test_write_one_byte() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t byte = 0xAB; bus.write(&byte, 1); settle(); uint8_t out = 0; size_t r = bus.read(&out, 1); int ok = (r == 1 && out == 0xAB); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_write_exact_max() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); auto data = std::make_unique(sockbus::MAX_MSG_SIZE); size_t w = bus.write(data.get(), sockbus::MAX_MSG_SIZE); int ok = (w == sockbus::MAX_MSG_SIZE); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_msg_too_large() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); auto big = std::make_unique(sockbus::MAX_MSG_SIZE + 1); bus.write(big.get(), sockbus::MAX_MSG_SIZE + 1); sockbus::destroy("test_bus"); return 0; } catch (const std::system_error& e) { safe_destroy("test_bus"); return (e.code().value() == EMSGSIZE); } } static int test_write_binary_with_nulls() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t data[] = {0x00, 0xFF, 0x00, 0xAA, 0x00, 0x55, 0x00, 0x01}; bus.write(data, sizeof(data)); settle(); uint8_t out[16] = {}; size_t r = bus.read(out, sizeof(out)); int ok = (r == sizeof(data) && std::memcmp(out, data, sizeof(data)) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Read tests ──────────────────────────────────────────────────────── */ static int test_read_null_buffer() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.read(nullptr, 64); sockbus::destroy("test_bus"); return 0; } catch (const std::system_error& e) { safe_destroy("test_bus"); return (e.code().value() == EINVAL); } } static int test_nonblock_empty() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t buf[256]; size_t n = bus.read(buf, sizeof(buf)); int ok = (n == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_small_buffer() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); bus.write(std::string_view("truncate me")); settle(); uint8_t buf[4] = {}; size_t r = bus.read(buf, sizeof(buf)); int ok = (r == 4 && std::memcmp(buf, "trun", 4) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_consumes_frame() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); bus.write(std::string_view("first")); bus.write(std::string_view("second")); settle(); uint8_t buf[3] = {}; size_t r1 = bus.read(buf, sizeof(buf)); uint8_t buf2[64] = {}; size_t r2 = bus.read(buf2, sizeof(buf2)); int ok = (r1 == 3 && std::memcmp(buf, "fir", 3) == 0 && r2 == 6 && std::memcmp(buf2, "second", 6) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Self-echo / broadcast tests ─────────────────────────────────────── */ static int test_self_echo() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); bus.write(std::string_view("echo")); settle(); uint8_t buf[256] = {}; size_t n = bus.read(buf, sizeof(buf)); int ok = (n == 4 && std::memcmp(buf, "echo", 4) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_broadcast() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus r1("test_bus"); sockbus::Bus r2("test_bus"); settle(); writer.write(std::string_view("broadcast")); settle(); uint8_t b1[256] = {}, b2[256] = {}; size_t n1 = r1.read(b1, sizeof(b1)); size_t n2 = r2.read(b2, sizeof(b2)); int ok = (n1 == 9 && std::memcmp(b1, "broadcast", 9) == 0 && n2 == 9 && std::memcmp(b2, "broadcast", 9) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_multiple_writers() { try { sockbus::create("test_bus"); settle(); sockbus::Bus w1("test_bus"); sockbus::Bus w2("test_bus"); sockbus::Bus reader("test_bus"); settle(); w1.write(std::string_view("from-w1")); w2.write(std::string_view("from-w2")); settle(); uint8_t b1[64] = {}, b2[64] = {}; size_t n1 = reader.read(b1, sizeof(b1)); size_t n2 = reader.read(b2, sizeof(b2)); int ok = (n1 == 7 && n2 == 7); int got_w1 = (std::memcmp(b1, "from-w1", 7) == 0 || std::memcmp(b2, "from-w1", 7) == 0); int got_w2 = (std::memcmp(b1, "from-w2", 7) == 0 || std::memcmp(b2, "from-w2", 7) == 0); ok = ok && got_w1 && got_w2; sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_multiple_messages() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus reader("test_bus"); settle(); writer.write(std::string_view("one")); writer.write(std::string_view("two")); writer.write(std::string_view("three")); settle(); uint8_t buf[256]; size_t n = reader.read(buf, sizeof(buf)); if (n != 3 || std::memcmp(buf, "one", 3) != 0) { sockbus::destroy("test_bus"); return 0; } n = reader.read(buf, sizeof(buf)); if (n != 3 || std::memcmp(buf, "two", 3) != 0) { sockbus::destroy("test_bus"); return 0; } n = reader.read(buf, sizeof(buf)); if (n != 5 || std::memcmp(buf, "three", 5) != 0) { sockbus::destroy("test_bus"); return 0; } sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } static int test_rapid_fire() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); int count = 100; for (int i = 0; i < count; i++) { char msg[16]; int len = std::snprintf(msg, sizeof(msg), "m%d", i); bus.write(reinterpret_cast(msg), static_cast(len)); } usleep(300000); int received = 0; uint8_t buf[64]; for (int i = 0; i < count + 10; i++) { size_t n = bus.read(buf, sizeof(buf)); if (n > 0) received++; else break; } int ok = (received >= 50); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Blocking read tests ─────────────────────────────────────────────── */ static int test_read_wait_timeout() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t buf[256]; size_t n = bus.read_wait(buf, sizeof(buf), 100); int ok = (n == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_wait_data() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus reader("test_bus"); settle(); writer.write(std::string_view("waited")); uint8_t buf[256] = {}; size_t n = reader.read_wait(buf, sizeof(buf), 1000); int ok = (n == 6 && std::memcmp(buf, "waited", 6) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_wait_zero_is_poll() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t buf[64]; size_t n = bus.read_wait(buf, sizeof(buf), 0); int ok = (n == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_wait_null_buffer() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.read_wait(nullptr, 64, 100); sockbus::destroy("test_bus"); return 0; } catch (const std::system_error& e) { safe_destroy("test_bus"); return (e.code().value() == EINVAL); } } static int test_read_wait_immediate() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); bus.write(std::string_view("ready")); settle(); timespec t0{}, t1{}; clock_gettime(CLOCK_MONOTONIC, &t0); uint8_t buf[64] = {}; size_t n = bus.read_wait(buf, sizeof(buf), 5000); clock_gettime(CLOCK_MONOTONIC, &t1); long elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000 + (t1.tv_nsec - t0.tv_nsec) / 1000000; int ok = (n == 5 && std::memcmp(buf, "ready", 5) == 0 && elapsed_ms < 1000); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Reader name tests ───────────────────────────────────────────────── */ static int test_reader_name() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.set_reader_name("test-reader"); sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } static int test_reader_name_long() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.set_reader_name( "this-is-a-very-long-reader-name-that-should-be-truncated-safely!"); sockbus::destroy("test_bus"); return 1; } catch (...) { return 0; } } /* ── Large message / data integrity tests ────────────────────────────── */ static int test_large_message() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus reader("test_bus"); settle(); size_t sz = 32768; auto data = std::make_unique(sz); for (size_t i = 0; i < sz; i++) data[i] = static_cast(i & 0xFF); writer.write(data.get(), sz); usleep(200000); auto buf = std::make_unique(sz); size_t n = reader.read_wait(buf.get(), sz, 2000); int ok = (n == sz && std::memcmp(buf.get(), data.get(), sz) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_pattern_integrity() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t pattern[256]; for (int i = 0; i < 256; i++) pattern[i] = static_cast(i); bus.write(pattern, sizeof(pattern)); settle(); uint8_t out[256] = {}; size_t n = bus.read(out, sizeof(out)); int ok = (n == 256 && std::memcmp(out, pattern, 256) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Multi-bus tests ─────────────────────────────────────────────────── */ static int test_two_buses() { try { sockbus::create("test_bus"); sockbus::create("test_bus2"); settle(); sockbus::Bus a("test_bus"); sockbus::Bus b("test_bus2"); settle(); a.write(std::string_view("bus1")); b.write(std::string_view("bus2")); settle(); uint8_t ba[64] = {}, bb[64] = {}; size_t na = a.read(ba, sizeof(ba)); size_t nb = b.read(bb, sizeof(bb)); int ok = (na == 4 && std::memcmp(ba, "bus1", 4) == 0 && nb == 4 && std::memcmp(bb, "bus2", 4) == 0); sockbus::destroy("test_bus"); sockbus::destroy("test_bus2"); return ok; } catch (...) { return 0; } } static int test_bus_isolation() { try { sockbus::create("test_bus"); sockbus::create("test_bus2"); settle(); sockbus::Bus a("test_bus"); sockbus::Bus b("test_bus2"); settle(); a.write(std::string_view("only-on-a")); settle(); uint8_t buf[64]; size_t nb = b.read(buf, sizeof(buf)); int ok = (nb == 0); sockbus::destroy("test_bus"); sockbus::destroy("test_bus2"); return ok; } catch (...) { return 0; } } static int test_three_buses() { try { sockbus::create("test_bus"); sockbus::create("test_bus2"); sockbus::create("test_bus3"); settle(); sockbus::Bus a("test_bus"); sockbus::Bus b("test_bus2"); sockbus::Bus c("test_bus3"); settle(); a.write(std::string_view("A")); b.write(std::string_view("B")); c.write(std::string_view("C")); settle(); uint8_t ba[4]={}, bb[4]={}, bc[4]={}; size_t na = a.read(ba, sizeof(ba)); size_t nb = b.read(bb, sizeof(bb)); size_t nc = c.read(bc, sizeof(bc)); int ok = (na == 1 && ba[0] == 'A' && nb == 1 && bb[0] == 'B' && nc == 1 && bc[0] == 'C'); sockbus::destroy("test_bus"); sockbus::destroy("test_bus2"); sockbus::destroy("test_bus3"); return ok; } catch (...) { return 0; } } /* ── Address / config tests ──────────────────────────────────────────── */ static int test_literal_addr() { try { sockbus::create("127.0.0.1:19102"); settle(); sockbus::Bus bus("127.0.0.1:19102"); settle(); bus.write(std::string_view("literal")); settle(); uint8_t buf[64] = {}; size_t n = bus.read(buf, sizeof(buf)); int ok = (n == 7 && std::memcmp(buf, "literal", 7) == 0); sockbus::destroy("127.0.0.1:19102"); return ok; } catch (...) { return 0; } } static int test_symbolic_name() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); bus.write(std::string_view("symbolic")); settle(); uint8_t buf[64] = {}; size_t n = bus.read_wait(buf, sizeof(buf), 500); int ok = (n == 8 && std::memcmp(buf, "symbolic", 8) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_unknown_symbolic_name() { try { sockbus::create("no_such_bus_name"); return 0; } catch (...) { return 1; } } /* ── Edge case tests ─────────────────────────────────────────────────── */ static int test_write_after_reader_close() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); { sockbus::Bus reader("test_bus"); settle(); } settle(); size_t w = writer.write(std::string_view("still-alive")); int ok = (w == 11); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_after_writer_close() { try { sockbus::create("test_bus"); settle(); sockbus::Bus reader("test_bus"); { sockbus::Bus writer("test_bus"); settle(); writer.write(std::string_view("goodbye")); settle(); } uint8_t buf[64] = {}; size_t n = reader.read(buf, sizeof(buf)); int ok = (n == 7 && std::memcmp(buf, "goodbye", 7) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_read_extra_returns_zero() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); bus.write(std::string_view("only-one")); settle(); uint8_t buf[64]; size_t n1 = bus.read(buf, sizeof(buf)); size_t n2 = bus.read(buf, sizeof(buf)); int ok = (n1 == 8 && n2 == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_interleaved_read_write() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); settle(); uint8_t buf[64] = {}; bus.write(std::string_view("a")); settle(); size_t n1 = bus.read(buf, sizeof(buf)); if (n1 != 1 || buf[0] != 'a') { sockbus::destroy("test_bus"); return 0; } bus.write(std::string_view("bb")); settle(); size_t n2 = bus.read(buf, sizeof(buf)); if (n2 != 2 || std::memcmp(buf, "bb", 2) != 0) { sockbus::destroy("test_bus"); return 0; } bus.write(std::string_view("ccc")); settle(); size_t n3 = bus.read(buf, sizeof(buf)); int ok = (n3 == 3 && std::memcmp(buf, "ccc", 3) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } static int test_four_readers_broadcast() { try { sockbus::create("test_bus"); settle(); sockbus::Bus writer("test_bus"); sockbus::Bus r0("test_bus"); sockbus::Bus r1("test_bus"); sockbus::Bus r2("test_bus"); sockbus::Bus r3("test_bus"); settle(); writer.write(std::string_view("fan-out")); settle(); sockbus::Bus* readers[] = {&r0, &r1, &r2, &r3}; int ok = 1; for (int i = 0; i < 4; i++) { uint8_t buf[64] = {}; size_t n = readers[i]->read(buf, sizeof(buf)); if (n != 7 || std::memcmp(buf, "fan-out", 7) != 0) ok = 0; } sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Bus::name() test ────────────────────────────────────────────────── */ static int test_bus_name() { try { sockbus::create("test_bus"); settle(); sockbus::Bus bus("test_bus"); int ok = (bus.name() == "test_bus"); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Move semantics test ─────────────────────────────────────────────── */ static int test_move_bus() { try { sockbus::create("test_bus"); settle(); sockbus::Bus a("test_bus"); settle(); a.write(std::string_view("before-move")); settle(); sockbus::Bus b(std::move(a)); uint8_t buf[64] = {}; size_t n = b.read(buf, sizeof(buf)); int ok = (n == 11 && std::memcmp(buf, "before-move", 11) == 0); sockbus::destroy("test_bus"); return ok; } catch (...) { return 0; } } /* ── Main ────────────────────────────────────────────────────────────── */ int main() { setup_config(); std::printf("sockbus unit tests (C++17)\n"); std::printf("%-54s %s\n", "────────────────────────────────────────────────────", "──────"); /* Lifecycle */ RUN(create_destroy); RUN(create_duplicate); RUN(destroy_unknown); RUN(create_empty_name); RUN(destroy_empty_name); RUN(create_custom_size); RUN(broker_reuse); /* Open/close */ RUN(open_no_bus); RUN(open_close); RUN(open_empty_name); RUN(open_multiple_handles); /* Write */ RUN(write_read); RUN(write_null_data); RUN(write_one_byte); RUN(write_exact_max); RUN(msg_too_large); RUN(write_binary_with_nulls); /* Read */ RUN(read_null_buffer); RUN(nonblock_empty); RUN(read_small_buffer); RUN(read_consumes_frame); /* Self-echo / broadcast */ RUN(self_echo); RUN(broadcast); RUN(multiple_writers); RUN(multiple_messages); RUN(rapid_fire); /* Blocking read */ RUN(read_wait_timeout); RUN(read_wait_data); RUN(read_wait_zero_is_poll); RUN(read_wait_null_buffer); RUN(read_wait_immediate); /* Reader name */ RUN(reader_name); RUN(reader_name_long); /* Large message / data integrity */ RUN(large_message); RUN(pattern_integrity); /* Multi-bus */ RUN(two_buses); RUN(bus_isolation); RUN(three_buses); /* Address / config */ RUN(literal_addr); RUN(symbolic_name); RUN(unknown_symbolic_name); /* Edge cases */ RUN(write_after_reader_close); RUN(read_after_writer_close); RUN(read_extra_returns_zero); RUN(interleaved_read_write); RUN(four_readers_broadcast); /* C++17-specific */ RUN(bus_name); RUN(move_bus); std::printf("%-54s %s\n", "────────────────────────────────────────────────────", "──────"); std::printf("%d/%d tests passed\n", tests_passed, tests_run); teardown_config(); return (tests_passed == tests_run) ? 0 : 1; }