#include #include #include #include "secsgem/secs2/item.hpp" #include "secsgem/secs2/message.hpp" #include "secsgem/secsi/block.hpp" #include "secsgem/secsi/header.hpp" #include "secsgem/secsi/protocol.hpp" using namespace secsgem::secsi; namespace s2 = secsgem::secs2; // ---- Header -------------------------------------------------------------- TEST_CASE("SECS-I header round-trip preserves all bit fields") { Header h; h.device_id = 0x1234; h.r_bit = true; h.stream = 1; h.function = 3; h.w_bit = true; h.block_number = 5; h.end_block = false; h.system_bytes = 0xDEADBEEF; auto bytes = h.encode(); Header back = Header::decode(bytes.data()); CHECK(back == h); } TEST_CASE("SECS-I header packs R/W/E bits into the right slots") { Header h; h.device_id = 0x7FFF; // all 15 bits set h.r_bit = true; h.stream = 0x7F; h.w_bit = false; h.function = 0xAB; h.block_number = 0x7FFF; h.end_block = true; auto b = h.encode(); CHECK(b[0] == 0xFF); // R=1 + top 7 bits of device id CHECK(b[1] == 0xFF); // bottom 8 bits of device id CHECK(b[2] == 0x7F); // W=0, stream=0x7F CHECK(b[3] == 0xAB); CHECK(b[4] == 0xFF); // E=1 + top 7 bits of block number CHECK(b[5] == 0xFF); } // ---- Block encode / decode ----------------------------------------------- TEST_CASE("SECS-I block: encode + decode round-trip with checksum") { Header h; h.stream = 1; h.function = 1; h.w_bit = true; h.block_number = 1; h.end_block = true; Block in; in.header = h; in.body = {0x01, 0x06, 0x00}; // small payload auto bytes = in.encode(); REQUIRE(bytes.size() == 1 + kHeaderSize + in.body.size() + 2); CHECK(bytes[0] == kHeaderSize + in.body.size()); std::size_t consumed = 0; Block out = Block::decode(bytes.data(), bytes.size(), consumed); CHECK(consumed == bytes.size()); CHECK(out.header == in.header); CHECK(out.body == in.body); } TEST_CASE("SECS-I block: decode rejects checksum mismatch") { Header h; Block in{h, {0x11, 0x22}}; auto bytes = in.encode(); bytes.back() ^= 0xFF; // corrupt the low byte of the checksum std::size_t consumed = 0; CHECK_THROWS_AS(Block::decode(bytes.data(), bytes.size(), consumed), BlockError); } TEST_CASE("SECS-I block: decode rejects short input") { Header h; Block in{h, {0x55}}; auto bytes = in.encode(); std::size_t consumed = 0; CHECK_THROWS_AS(Block::decode(bytes.data(), bytes.size() - 1, consumed), BlockError); } TEST_CASE("SECS-I block: encode rejects oversize body") { Header h; Block in; in.header = h; in.body.assign(kMaxBlockBody + 1, 0xAA); CHECK_THROWS_AS(in.encode(), BlockError); } // ---- Multi-block split + assemble --------------------------------------- TEST_CASE("SECS-I split_message: short message fits in one block") { auto msg = s2::Message(1, 3, true, s2::Item::ascii("ok")); Header tmpl; tmpl.device_id = 7; auto blocks = split_message(msg, tmpl); REQUIRE(blocks.size() == 1); CHECK(blocks[0].header.block_number == 1); CHECK(blocks[0].header.end_block); CHECK(blocks[0].header.stream == 1); CHECK(blocks[0].header.function == 3); CHECK(blocks[0].header.w_bit); } TEST_CASE("SECS-I split_message: long body produces sequential blocks") { // Make a body that exceeds kMaxBlockBody. ASCII payload is ~equal to // body bytes after the 2-byte item header. std::string big(kMaxBlockBody * 3 - 5, 'X'); auto msg = s2::Message(7, 3, true, s2::Item::ascii(big)); Header tmpl; auto blocks = split_message(msg, tmpl); REQUIRE(blocks.size() >= 3); for (std::size_t i = 0; i < blocks.size(); ++i) { CHECK(blocks[i].header.block_number == i + 1); const bool last = (i + 1 == blocks.size()); CHECK(blocks[i].header.end_block == last); } // Reassembly returns the same body. auto reassembled = assemble_message(blocks); REQUIRE(reassembled.has_value()); CHECK(reassembled->stream == 7); CHECK(reassembled->function == 3); REQUIRE(reassembled->body.has_value()); } TEST_CASE("SECS-I assemble_message: rejects gaps and mid-message E-bit") { auto msg = s2::Message(1, 1, true, s2::Item::ascii("hello")); Header tmpl; auto blocks = split_message(msg, tmpl); // Gap in block numbering. std::vector gapped = blocks; if (!gapped.empty()) gapped[0].header.block_number = 99; CHECK_FALSE(assemble_message(gapped).has_value()); // E-bit mid-stream. if (blocks.size() >= 2) { std::vector mid_e = blocks; mid_e[0].header.end_block = true; CHECK_FALSE(assemble_message(mid_e).has_value()); } } // ---- Protocol FSM -------------------------------------------------------- namespace { // Test harness: pipe two Protocol instances back-to-back. Each "tick" // flushes one peer's outbox into the other's inbox until quiescence. struct Pair { Protocol a{Role::Master}; Protocol b{Role::Slave}; std::vector a_out, b_out; std::vector a_inbox, b_inbox; std::vector a_delivered, b_delivered; std::vector errors; void feed_a(const Event& ev) { a.on_event(ev, a_out); } void feed_b(const Event& ev) { b.on_event(ev, b_out); } // Drain `out` into the peer's inbox (transmits) and capture delivered // blocks / errors. Timer actions are ignored — tests fire timers // explicitly when they want to exercise timeouts. void drain(std::vector& out, std::vector& peer_inbox, std::vector& delivered) { for (auto& act : out) { if (auto* t = std::get_if(&act)) { peer_inbox.insert(peer_inbox.end(), t->bytes.begin(), t->bytes.end()); } else if (auto* d = std::get_if(&act)) { delivered.push_back(std::move(d->block)); } else if (auto* e = std::get_if(&act)) { errors.push_back(e->reason); } } out.clear(); } // Step the simulation until both inboxes are empty and no peer has // pending events. Bytes flow A -> b_inbox -> B, B -> a_inbox -> A. void tick() { bool progress = true; int safety = 1000; while (progress && safety-- > 0) { progress = false; drain(a_out, b_inbox, a_delivered); drain(b_out, a_inbox, b_delivered); while (!a_inbox.empty()) { uint8_t byte = a_inbox.front(); a_inbox.erase(a_inbox.begin()); a.on_event(EventByte{byte}, a_out); progress = true; } while (!b_inbox.empty()) { uint8_t byte = b_inbox.front(); b_inbox.erase(b_inbox.begin()); b.on_event(EventByte{byte}, b_out); progress = true; } if (!a_out.empty() || !b_out.empty()) progress = true; drain(a_out, b_inbox, a_delivered); drain(b_out, a_inbox, b_delivered); } } }; Block make_block(uint8_t stream, uint8_t function, const std::vector& body = {}) { Block b; b.header.stream = stream; b.header.function = function; b.header.block_number = 1; b.header.end_block = true; b.header.w_bit = true; b.body = body; return b; } } // namespace TEST_CASE("SECS-I protocol: idle starts in Idle, ENQ from peer triggers EOT") { Protocol p(Role::Master); std::vector out; CHECK(p.state() == Protocol::State::Idle); p.on_event(EventByte{kENQ}, out); CHECK(p.state() == Protocol::State::RecvEotSent); // First action must be a transmit containing EOT. REQUIRE_FALSE(out.empty()); auto* t = std::get_if(&out[0]); REQUIRE(t); REQUIRE(t->bytes.size() == 1); CHECK(t->bytes[0] == kEOT); } TEST_CASE("SECS-I protocol: back-to-back send delivers block to peer") { Pair pp; pp.feed_a(EventSend{make_block(1, 1, {0xAA, 0xBB})}); pp.tick(); REQUIRE(pp.b_delivered.size() == 1); CHECK(pp.b_delivered[0].header.stream == 1); CHECK(pp.b_delivered[0].header.function == 1); CHECK(pp.b_delivered[0].body == std::vector{0xAA, 0xBB}); CHECK(pp.errors.empty()); CHECK(pp.a.state() == Protocol::State::Idle); CHECK(pp.b.state() == Protocol::State::Idle); } TEST_CASE("SECS-I protocol: bidirectional exchange") { Pair pp; pp.feed_a(EventSend{make_block(1, 13, {0x01})}); pp.tick(); REQUIRE(pp.b_delivered.size() == 1); pp.feed_b(EventSend{make_block(1, 14, {0x02})}); pp.tick(); REQUIRE(pp.a_delivered.size() == 1); CHECK(pp.a_delivered[0].header.function == 14); } TEST_CASE("SECS-I protocol: NAK triggers retry; RTY exhaustion aborts") { Protocol p(Role::Master, Timers{.rty = 2}); std::vector out; // Begin send. p.on_event(EventSend{make_block(1, 1, {0x00})}, out); CHECK(p.state() == Protocol::State::SendEnqSent); // Peer clears us. out.clear(); p.on_event(EventByte{kEOT}, out); CHECK(p.state() == Protocol::State::SendAwaitAck); // Peer NAKs — first retry consumes one RTY (rty was 2, retry decrements // first then resends, so after this we have 1 retry left). out.clear(); p.on_event(EventByte{kNAK}, out); CHECK(p.state() == Protocol::State::SendEnqSent); CHECK(p.rty_remaining() == 1); // Walk through another NAK. p.on_event(EventByte{kEOT}, out); out.clear(); p.on_event(EventByte{kNAK}, out); CHECK(p.state() == Protocol::State::SendEnqSent); CHECK(p.rty_remaining() == 0); // One more NAK -> exhaustion -> abort. p.on_event(EventByte{kEOT}, out); out.clear(); p.on_event(EventByte{kNAK}, out); // After abort: queue cleared, state Idle, error raised. CHECK(p.state() == Protocol::State::Idle); bool saw_err = std::any_of(out.begin(), out.end(), [](const Action& a) { return std::holds_alternative(a); }); CHECK(saw_err); } TEST_CASE("SECS-I protocol: contention — slave yields when peer ENQs") { Protocol slave(Role::Slave); std::vector out; // Slave wants to send. slave.on_event(EventSend{make_block(1, 1)}, out); CHECK(slave.state() == Protocol::State::SendEnqSent); // Peer ENQs at the same time. out.clear(); slave.on_event(EventByte{kENQ}, out); // Slave yields: now in RecvEotSent, has emitted EOT. CHECK(slave.state() == Protocol::State::RecvEotSent); bool saw_eot = std::any_of(out.begin(), out.end(), [](const Action& a) { if (auto* t = std::get_if(&a)) return !t->bytes.empty() && t->bytes[0] == kEOT; return false; }); CHECK(saw_eot); } TEST_CASE("SECS-I protocol: T2 timeout during send triggers retry") { Protocol p(Role::Master); std::vector out; p.on_event(EventSend{make_block(1, 1)}, out); REQUIRE(p.state() == Protocol::State::SendEnqSent); const auto rty_before = p.rty_remaining(); out.clear(); p.on_event(EventTimeout{Timer::T2}, out); CHECK(p.state() == Protocol::State::SendEnqSent); CHECK(p.rty_remaining() == rty_before - 1); }