#include #include #include #include #include #include uint32_t mock_millis = 100; using Bytes = std::vector; using Socket = std::shared_ptr; static constexpr uint32_t A = 0xc0a80114; static constexpr uint32_t B = 0xc0a8011e; static Bytes framed(std::initializer_list frames) { Bytes result; for (const auto& frame : frames) { #if !ETHERNET_RAW_LINE result.insert(result.end(), {'>', uint8_t(frame.size()), uint8_t(frame.size() >> 8)}); #endif result.insert(result.end(), frame.begin(), frame.end()); #if ETHERNET_RAW_LINE result.insert(result.end(), {'\r', '\n'}); #endif } return result; } static Bytes command(const Bytes& payload) { #if ETHERNET_RAW_LINE Bytes data = payload; data.push_back('\n'); #else Bytes data{'<', uint8_t(payload.size()), uint8_t(payload.size() >> 8)}; data.insert(data.end(), payload.begin(), payload.end()); #endif return data; } struct Transport : SerialEthernetInterface { bool connected = false; bool io_unavailable = false; bool pause_after_write = false; bool pause_after_read = false; bool pause_after_available = false; bool replenish_read = false; size_t read_guard = 0; Bytes append_after_read; int available_calls = 0; int write_calls = 0; int disconnect_calls = 0; Socket client; Socket next_client; bool isIOUnavailable() const override { return io_unavailable; } void loop() override { serviceIOAvailability(); } bool isConnected() const override { return isEnabled() && connected; } void disconnectClient() override { ++disconnect_calls; connected = false; if (client) client->connected = false; } int available() override { assert(!io_unavailable); ++available_calls; if (pause_after_available) { io_unavailable = true; pause_after_available = false; } return client ? int(client->received.size()) : 0; } int read() override { assert(!io_unavailable); if (!client || client->received.empty()) return -1; assert((read_guard == 0 || client->read_count < read_guard) && "Ethernet RX invocation exceeded bounded read guard"); const int value = client->received.front(); client->received.pop_front(); ++client->read_count; if (replenish_read) client->received.push_back(uint8_t(value)); if (!append_after_read.empty()) { client->received.insert(client->received.end(), append_after_read.begin(), append_after_read.end()); append_after_read.clear(); } if (pause_after_read) { io_unavailable = true; pause_after_read = false; } return value; } size_t write(const uint8_t* src, size_t size) override { assert(!io_unavailable); ++write_calls; if (!isConnected() || !client) return 0; size = std::min(size, client->write_limit); client->sent.insert(client->sent.end(), src, src + size); if (pause_after_write) { io_unavailable = true; pause_after_write = false; } return size; } Socket connect(uint32_t ip, const Bytes& input = {}) { next_client = std::make_shared(IPAddress(ip)); next_client->received.insert(next_client->received.end(), input.begin(), input.end()); disconnectClient(); onClientConnected(ip); // Same ordering as both hardware driver loops. client = next_client; next_client.reset(); connected = true; return client; } }; struct Fixture { Transport transport; MultiSerialInterface routes; uint8_t input[MAX_FRAME_SIZE] = {}; int callbacks = 0; bool pending_operation = false; Fixture() { assert(routes.addInterface(InterfaceType::Ethernet, &transport)); routes.enable(); transport.setSessionChangedCallback([](void* context) { auto& f = *static_cast(context); assert(!f.transport.isConnected()); if (f.transport.next_client) { assert(f.transport.next_client->sent.empty()); assert(f.transport.next_client->read_count == 0); } ++f.callbacks; f.pending_operation = false; f.routes.forgetReplyRouteForDisconnected(&f.transport); }, this); } size_t tick() { return routes.checkRecvFrame(input); } void enqueue(const Bytes& data) { assert(transport.writeFrame(data.data(), data.size()) == data.size()); } void drain(const Socket& socket) { socket->write_limit = std::numeric_limits::max(); for (int i = 0; i < 20; ++i) tick(); } }; int main(int argc, char** argv) { assert(argc == 2); const int scenario = std::atoi(argv[1]); Fixture f; const Bytes reply{0x01, 0x21, 0x22, 0x23}; const Bytes other{0x02, 0x31}; const Bytes push{0x80, 0x41, 0x42}; auto socket = f.transport.connect(A); if (scenario <= 2) { f.enqueue(reply); socket->write_limit = scenario == 0 ? 2 : scenario == 1 ? 4 : 0; f.tick(); assert(socket->sent.size() == socket->write_limit); f.drain(socket); assert(socket->sent == framed({reply})); } else if (scenario == 3 || scenario == 4) { f.enqueue(push); socket->write_limit = 2; f.tick(); if (scenario == 4) { f.enqueue(Bytes{0x81, 0x51}); f.enqueue(Bytes{0x88, 0x61}); } f.enqueue(reply); if (scenario == 4) f.enqueue(other); f.drain(socket); assert(socket->sent == (scenario == 4 ? framed({push, reply, other, {0x81, 0x51}}) : framed({push, reply}))); } else if (scenario == 5) { Bytes maximum(MAX_FRAME_SIZE, 0x45); maximum[0] = 0x80; f.enqueue(maximum); socket->write_limit = framed({maximum}).size() - 1; f.tick(); assert(socket->sent.size() == socket->write_limit); f.enqueue(reply); f.drain(socket); assert(socket->sent == framed({maximum, reply})); } else if (scenario == 6 || scenario == 7) { f.pending_operation = true; f.enqueue(reply); socket->write_limit = 2; f.tick(); auto replacement = f.transport.connect(scenario == 6 ? A : B); assert(!socket->connected); assert(f.callbacks == (scenario == 6 ? 0 : 1)); assert(f.pending_operation == (scenario == 6)); f.drain(replacement); assert(replacement->sent == (scenario == 6 ? framed({reply}) : Bytes{})); if (scenario == 7) { auto returned = f.transport.connect(A); f.drain(returned); assert(returned->sent.empty()); // No extra held queue on Ethernet. } } else if (scenario == 8) { f.pending_operation = true; f.enqueue(reply); f.transport.disconnectClient(); f.transport.onClientDisconnected(); assert(f.callbacks == 1 && !f.pending_operation); assert(!f.transport.hasPendingIO()); mock_millis += 60000; auto returned = f.transport.connect(A); f.drain(returned); assert(returned->sent == framed({reply}) && f.callbacks == 1); } else if (scenario == 9) { const Bytes request = command(Bytes{0x04}); socket->received.insert(socket->received.end(), request.begin(), request.end()); assert(f.tick() == 1); assert(f.routes.captureReplyRoute() == &f.transport); f.routes.lockReplyRoute(); f.pending_operation = true; auto replacement = f.transport.connect(B, command(Bytes{0x05})); assert(!f.pending_operation && f.callbacks == 1); assert(f.routes.captureReplyRoute() == nullptr); assert(replacement->sent.empty() && replacement->read_count == 0); assert(f.tick() == 1 && f.input[0] == 0x05); assert(f.routes.captureReplyRoute() == &f.transport); } else if (scenario == 10) { f.enqueue(reply); f.pending_operation = true; f.transport.disable(); f.transport.disable(); assert(!socket->connected && !f.pending_operation && f.callbacks == 1); assert(!f.transport.hasPendingIO()); f.transport.enable(); auto replacement = f.transport.connect(A); f.drain(replacement); assert(replacement->sent.empty()); } else if (scenario == 11) { #if ETHERNET_RAW_LINE socket->received = std::deque(MAX_FRAME_SIZE + 1, 'a'); socket->received.push_back('\n'); #else socket->received = {'<', 0xff, 0xff}; #endif const Bytes tail = command(Bytes{0x71}); socket->received.insert(socket->received.end(), tail.begin(), tail.end()); assert(f.tick() == 0); assert(!socket->connected && f.callbacks == 1); assert(!f.transport.isReadBusy()); for (int i = 0; i < 5; ++i) assert(f.tick() == 0); f.transport.connect(A, command(Bytes{0x72})); assert(f.tick() == 1 && f.input[0] == 0x72); } else if (scenario == 12 || scenario == 14) { if (scenario == 14) mock_millis = UINT32_MAX - 2000; Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0); assert(f.transport.isReadBusy()); mock_millis += 4999; assert(f.tick() == 0 && socket->connected); mock_millis += 1; #if ETHERNET_RAW_LINE // Human terminal input is not subject to the binary frame deadline. assert(f.tick() == 0 && socket->connected); mock_millis += 60000; socket->received.push_back('\n'); assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(f.callbacks == 0 && !f.transport.isReadBusy()); #else assert(f.tick() == 0 && !socket->connected); assert(f.callbacks == 1 && !f.transport.isReadBusy()); #endif } else if (scenario == 13) { Bytes maximum(MAX_FRAME_SIZE, 'a'); const Bytes bytes = command(maximum); for (size_t i = 0; i < bytes.size(); ++i) { socket->received.push_back(bytes[i]); assert(f.tick() == (i + 1 == bytes.size() ? MAX_FRAME_SIZE : 0)); } for (int i = 0; i < MAX_FRAME_SIZE; ++i) assert(f.input[i] == 'a'); assert(!f.transport.isReadBusy() && socket->connected); const Bytes next = command(Bytes{'b'}); socket->received.insert(socket->received.end(), next.begin(), next.end()); assert(f.tick() == 1 && f.input[0] == 'b'); } else if (scenario == 15) { const Bytes data = command(reply); socket->received.insert(socket->received.end(), data.begin(), data.end()); assert(f.transport.checkRecvFrame(nullptr) == 0); assert(!socket->connected && f.callbacks == 1); } else if (scenario == 16) { f.enqueue(reply); socket->write_limit = 2; assert(f.tick() == 0); const Bytes prefix = socket->sent; assert(prefix.size() == 2); const Bytes request = command(other); socket->received.insert(socket->received.end(), request.begin(), request.end()); const int available_calls = f.transport.available_calls; const int write_calls = f.transport.write_calls; const int disconnect_calls = f.transport.disconnect_calls; f.transport.io_unavailable = true; assert(f.transport.isConnected()); assert(f.transport.hasPendingIO()); assert(f.transport.isWriteBusy()); for (int i = 0; i < 10; ++i) { mock_millis += 10000; assert(f.tick() == 0); } assert(f.transport.available_calls == available_calls); assert(f.transport.write_calls == write_calls); assert(f.transport.disconnect_calls == disconnect_calls); assert(socket->sent == prefix && socket->read_count == 0); assert(f.callbacks == 0 && f.transport.isConnected()); f.transport.io_unavailable = false; assert(!f.transport.isWriteBusy()); socket->write_limit = 100; assert(f.tick() == 0); assert(socket->sent == framed({reply})); assert(f.tick() == other.size()); assert(f.input[0] == other[0] && f.callbacks == 0); } else if (scenario == 17 || scenario == 18 || scenario == 19 || scenario == 24) { if (scenario == 19) mock_millis = UINT32_MAX - 3000; if (scenario == 24) mock_millis = UINT32_MAX - 1999; Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); mock_millis += scenario == 18 ? 5000 : 2000; f.transport.io_unavailable = true; const int disconnect_calls = f.transport.disconnect_calls; assert(f.tick() == 0 && socket->connected); // Repeated polls must preserve the first pause boundary, including a // boundary at millis()==0 and a pause that crosses the 32-bit rollover. for (int i = 0; i < 3; ++i) { mock_millis += 4000; assert(f.tick() == 0 && socket->connected); } assert(f.transport.disconnect_calls == disconnect_calls); f.transport.io_unavailable = false; #if !ETHERNET_RAW_LINE if (scenario == 18) { assert(f.tick() == 0 && !socket->connected); assert(f.callbacks == 1 && !f.transport.isReadBusy()); } else { assert(f.tick() == 0 && socket->connected); mock_millis += 1499; assert(f.tick() == 0 && socket->connected); // A second long pause must preserve the 1501ms remaining, not grant a // new five-second budget on each DHCP/renewal worker operation. f.transport.io_unavailable = true; assert(f.tick() == 0 && socket->connected); mock_millis += 20000; assert(f.tick() == 0 && socket->connected); f.transport.io_unavailable = false; assert(f.tick() == 0 && socket->connected); mock_millis += 1500; assert(f.tick() == 0 && socket->connected); mock_millis += 1; assert(f.tick() == 0 && !socket->connected); assert(f.callbacks == 1 && !f.transport.isReadBusy()); } #else // Human terminal input has no binary framing deadline, even when already // older than five seconds at the start of worker ownership. mock_millis += 60000; socket->received.push_back('\n'); assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(f.callbacks == 0 && !f.transport.isReadBusy()); #endif } else if (scenario >= 20 && scenario <= 23) { Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); f.enqueue(reply); socket->write_limit = 2; assert(f.tick() == 0 && socket->sent.size() == 2); mock_millis += 2000; f.transport.io_unavailable = true; assert(f.tick() == 0); mock_millis += 20000; Socket replacement; if (scenario == 20) { f.transport.disconnectClient(); f.transport.onClientDisconnected(); replacement = f.transport.connect(A); assert(f.callbacks == 1); } else if (scenario == 21) { f.transport.disable(); f.transport.enable(); replacement = f.transport.connect(A); assert(f.callbacks == 1); } else { replacement = f.transport.connect(scenario == 22 ? A : B); assert(f.callbacks == (scenario == 22 ? 0 : 1)); } assert(!f.transport.isReadBusy()); assert(f.tick() == 0 && replacement->connected); mock_millis += 20000; assert(f.tick() == 0 && replacement->connected); f.transport.io_unavailable = false; f.drain(replacement); assert(replacement->sent == ((scenario == 20 || scenario == 22) ? framed({reply}) : Bytes{})); // The replacement's frame must receive its own full timeout and must not // inherit any bytes or suspended time from the displaced stream. replacement->received.insert(replacement->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); mock_millis += 4999; assert(f.tick() == 0 && replacement->connected); replacement->received.push_back( #if ETHERNET_RAW_LINE '\n' #else 'b' #endif ); assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); } else if (scenario == 25) { f.transport.io_unavailable = true; assert(f.tick() == 0); mock_millis += 20000; assert(f.tick() == 0); f.transport.io_unavailable = false; Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); mock_millis += 4999; assert(f.tick() == 0 && socket->connected); mock_millis += 1; #if ETHERNET_RAW_LINE socket->received.push_back('\n'); assert(f.tick() == 2 && f.callbacks == 0); #else assert(f.tick() == 0 && !socket->connected && f.callbacks == 1); #endif } else if (scenario == 26) { Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); f.enqueue(reply); socket->write_limit = 2; mock_millis += 2000; f.transport.pause_after_write = true; assert(f.tick() == 0 && socket->sent.size() == 2); assert(f.transport.io_unavailable); mock_millis += 12000; f.transport.io_unavailable = false; socket->write_limit = 100; assert(f.tick() == 0 && socket->connected); assert(socket->sent == framed({reply})); mock_millis += 2999; socket->received.push_back( #if ETHERNET_RAW_LINE '\n' #else 'b' #endif ); assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(f.callbacks == 0); } else if (scenario == 27 || scenario == 28) { const Bytes request = command(Bytes{'a', 'b'}); socket->received.insert(socket->received.end(), request.begin(), request.end()); if (scenario == 27) f.transport.pause_after_read = true; else f.transport.pause_after_available = true; assert(f.tick() == 0 && socket->connected); assert(f.transport.io_unavailable); assert(socket->read_count == (scenario == 27 ? 1U : 0U)); assert(f.transport.isReadBusy() == (scenario == 27)); mock_millis += 12000; f.transport.io_unavailable = false; assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(f.callbacks == 0 && !f.transport.isReadBusy()); } else if (scenario == 29) { Bytes partial = command(Bytes{'a', 'b'}); partial.pop_back(); socket->received.insert(socket->received.end(), partial.begin(), partial.end()); assert(f.tick() == 0 && f.transport.isReadBusy()); // Exercise the actual MultiSerial routing path: another interface owns a // multi-frame response, so Ethernet receives loop() service but no frame // polls until that producer releases its route. Transport other_transport; assert(f.routes.addInterface(InterfaceType::USB, &other_transport)); other_transport.enable(); auto other_socket = other_transport.connect(B, command(Bytes{0x07})); assert(f.tick() == 1 && f.input[0] == 0x07); assert(f.routes.captureReplyRoute() == &other_transport); f.routes.lockReplyRoute(); mock_millis += 2000; f.transport.io_unavailable = true; f.routes.loop(); // Main-side ownership handoff, not a frame poll. const int available_calls = f.transport.available_calls; for (int i = 0; i < 3; ++i) { mock_millis += 4000; f.routes.loop(); assert(f.tick() == 0 && socket->connected); } assert(f.transport.available_calls == available_calls); assert(f.transport.isReadBusy() && f.callbacks == 0); f.transport.io_unavailable = false; f.routes.loop(); // Main-side completion releases the preserved budget. f.routes.unlockReplyRoute(); mock_millis += 2999; assert(f.tick() == 0 && socket->connected); mock_millis += 1; #if ETHERNET_RAW_LINE socket->received.push_back('\n'); assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(f.callbacks == 0); #else assert(f.tick() == 0 && !socket->connected && f.callbacks == 1); #endif assert(other_socket->connected); } else if (scenario >= 30 && scenario <= 35) { const bool replenish = scenario <= 32; static constexpr size_t budgets[] = {1, 7, 257}; const size_t budget = budgets[(scenario - 30) % 3]; const Bytes request = command(Bytes{'a', 'b'}); #if ETHERNET_RAW_LINE const uint8_t junk = '\n'; #else const uint8_t junk = 'x'; #endif socket->received.assign(budget, junk); f.transport.read_guard = 4096; // The live-availability negative control must terminate. f.transport.replenish_read = replenish; if (!replenish) f.transport.append_after_read = request; assert(f.tick() == 0 && "new arrivals must wait for the next mesh loop"); assert(socket->read_count == budget && f.transport.available_calls == 1); assert(socket->received.size() == (replenish ? budget : request.size())); assert(socket->connected && !f.transport.isReadBusy() && f.callbacks == 0); f.transport.replenish_read = false; if (replenish) socket->received.insert(socket->received.end(), request.begin(), request.end()); // Nothing was dropped: existing idle bytes and a newly arrived complete // command remain in wire order and parse on the following mesh loop. assert(f.tick() == 2 && f.input[0] == 'a' && f.input[1] == 'b'); assert(socket->received.empty() && f.transport.available_calls == 2); } else if (scenario >= 36 && scenario <= 39) { const bool zero_available = scenario >= 38; mock_millis = (scenario & 1) ? UINT32_MAX - 1000U : 0; const Bytes request = command(Bytes{'a', 'b'}); socket->received.push_back(request[0]); if (!zero_available) f.transport.pause_after_read = true; assert(f.tick() == 0 && f.transport.isReadBusy()); if (zero_available) { mock_millis += 2000; f.transport.pause_after_available = true; assert(f.tick() == 0); } assert(f.transport.io_unavailable && socket->read_count == 1); const int available_calls = f.transport.available_calls; mock_millis += 12000; assert(f.tick() == 0 && f.transport.available_calls == available_calls); f.transport.io_unavailable = false; f.transport.loop(); mock_millis += zero_available ? 2999 : 4999; socket->received.insert(socket->received.end(), request.begin() + 1, request.end()); assert((f.tick() == 2 && socket->connected && f.input[0] == 'a' && f.input[1] == 'b' && f.callbacks == 0) && "worker handoff must preserve remaining frame deadline"); } else { assert(false && "unknown scenario"); } std::printf("PASS: Ethernet session scenario %d\n", scenario); }