// Wire codec tests: pure C++ QtTest over protocol/XenoWire (no sockets, no // QML engine). Pins the 17-byte little-endian header layout from // notes/daemon-contract.md, stream-splitting behavior, bounds checks, and // every tag-specific payload round trip. #include #include "XenoWire.h" using namespace xeno::wire; class TestWireCodec : public QObject { Q_OBJECT private slots: // Header format pinning. void headerSizeIsSeventeen(); void headerIsLittleEndianOnTheWire(); // Frame round trips. void roundTripRecoversAllHeaderFields(); void emptyPayloadEncodesToHeaderOnly(); // Stream splitting (the real socket case). void twoConcatenatedFramesDecodeInOrder(); void partialTrailingFrameStaysInBuf(); // Protocol guard. void oversizedPayloadLenRejectedAndBufUntouched(); // Payload codecs. void bytesPayloadRoundTrip(); void resizePayloadRoundTrip(); void exitPayloadRoundTrip(); void splitHelpersRejectShortPayloads(); }; // Hand-built byte layout expectation: streamId@0, seq@4, tag@12, // payloadLen@13. This pins the wire format against accidental field reorder. void TestWireCodec::headerSizeIsSeventeen() // NOLINT(readability-convert-member-functions-to-static) { QCOMPARE(kHeaderSize, 17); QCOMPARE(kMaxPayload, quint32(4 * 1024 * 1024)); Header h; h.streamId = 1; h.seq = 2; h.tag = quint8(Tag::Exit); h.payloadLen = 4; QByteArray buf; encode(buf, h, exitPayload(7)); QCOMPARE(buf.size(), 21); QCOMPARE(int(quint8(buf[12])), int(Tag::Exit)); QCOMPARE(int(quint8(buf[13])), 4); // payloadLen little-endian byte 0 QCOMPARE(int(quint8(buf[14])), 0); } void TestWireCodec::headerIsLittleEndianOnTheWire() // NOLINT(readability-convert-member-functions-to-static) { Header h; h.streamId = 0x0A0B0C0Du; h.seq = quint64(0x0102030405060708ULL); h.tag = 0x2A; h.payloadLen = 0; QByteArray buf; encode(buf, h, {}); QCOMPARE(buf.size(), kHeaderSize); // seq occupies offsets 4..11; little-endian puts the low byte first. QCOMPARE(quint8(buf[4]), quint8(0x08)); QCOMPARE(quint8(buf[5]), quint8(0x07)); QCOMPARE(quint8(buf[6]), quint8(0x06)); QCOMPARE(quint8(buf[7]), quint8(0x05)); QCOMPARE(quint8(buf[8]), quint8(0x04)); QCOMPARE(quint8(buf[9]), quint8(0x03)); QCOMPARE(quint8(buf[10]), quint8(0x02)); QCOMPARE(quint8(buf[11]), quint8(0x01)); // streamId likewise low byte first at offset 0. QCOMPARE(quint8(buf[0]), quint8(0x0D)); QCOMPARE(quint8(buf[3]), quint8(0x0A)); } void TestWireCodec::roundTripRecoversAllHeaderFields() // NOLINT(readability-convert-member-functions-to-static) { Header h; h.streamId = 0xDEADBEEFu; h.seq = quint64(1) << 40; // > 32 bits, exercises the u64 field h.tag = quint8(Tag::Bytes); const QByteArray payload("hello pty bytes"); QByteArray buf; encode(buf, h, payload); QVector frames; QVERIFY(decode(buf, frames)); QCOMPARE(frames.size(), 1); QCOMPARE(frames[0].header.streamId, quint32(0xDEADBEEFu)); QCOMPARE(frames[0].header.seq, h.seq); QCOMPARE(frames[0].header.tag, quint8(Tag::Bytes)); QCOMPARE(frames[0].header.payloadLen, quint32(payload.size())); QCOMPARE(frames[0].payload, payload); QVERIFY(buf.isEmpty()); } void TestWireCodec::emptyPayloadEncodesToHeaderOnly() // NOLINT(readability-convert-member-functions-to-static) { Header h; h.streamId = 5; h.seq = 6; h.tag = quint8(Tag::Resize); QByteArray buf; encode(buf, h, {}); QCOMPARE(buf.size(), kHeaderSize); QVector frames; QVERIFY(decode(buf, frames)); QCOMPARE(frames.size(), 1); QVERIFY(frames[0].payload.isEmpty()); QCOMPARE(frames[0].header.payloadLen, quint32(0)); } void TestWireCodec::twoConcatenatedFramesDecodeInOrder() // NOLINT(readability-convert-member-functions-to-static) { QByteArray buf; Header a; a.streamId = 1; a.seq = 100; a.tag = quint8(Tag::Bytes); encode(buf, a, bytesPayload(7, "first")); Header b; b.streamId = 1; b.seq = 101; b.tag = quint8(Tag::Exit); encode(buf, b, exitPayload(-1)); QVector frames; QVERIFY(decode(buf, frames)); QCOMPARE(frames.size(), 2); QCOMPARE(frames[0].header.seq, quint64(100)); QCOMPARE(frames[0].header.tag, quint8(Tag::Bytes)); QCOMPARE(frames[1].header.seq, quint64(101)); QCOMPARE(frames[1].header.tag, quint8(Tag::Exit)); QVERIFY(buf.isEmpty()); } void TestWireCodec::partialTrailingFrameStaysInBuf() // NOLINT(readability-convert-member-functions-to-static) { Header h; h.streamId = 3; h.seq = 9; h.tag = quint8(Tag::Bytes); QByteArray full; encode(full, h, QByteArrayLiteral("complete frame payload")); // Feed a split mid-header and mid-payload across three decode calls. const int cut1 = 10; // inside the header const int cut2 = kHeaderSize + 4; // inside the payload QByteArray buf = full.left(cut1); QVector frames; QVERIFY(decode(buf, frames)); QVERIFY(frames.isEmpty()); QCOMPARE(buf.size(), cut1); buf.append(full.mid(cut1, cut2 - cut1)); QVERIFY(decode(buf, frames)); QVERIFY(frames.isEmpty()); QCOMPARE(buf, full.left(cut2)); // whole partial payload buffered, not yet decodable buf.append(full.mid(cut2)); QVERIFY(decode(buf, frames)); QCOMPARE(frames.size(), 1); QCOMPARE(frames[0].payload, QByteArrayLiteral("complete frame payload")); QVERIFY(buf.isEmpty()); } void TestWireCodec::oversizedPayloadLenRejectedAndBufUntouched() // NOLINT(readability-convert-member-functions-to-static) { // Hand-build a header claiming a payload larger than kMaxPayload. QByteArray buf; buf.resize(kHeaderSize); qToLittleEndian(quint32(1), buf.data() + 0); qToLittleEndian(quint64(2), buf.data() + 4); buf[12] = char(1); qToLittleEndian(kMaxPayload + 1, buf.data() + 13); const QByteArray before = buf; QVector frames; QVERIFY(!decode(buf, frames)); QVERIFY(frames.isEmpty()); QCOMPARE(buf, before); // untouched, so the caller can inspect it } void TestWireCodec::bytesPayloadRoundTrip() // NOLINT(readability-convert-member-functions-to-static) { const QByteArray data("pty output \x00 with nul \xFF bytes", 22); QByteArray payload = bytesPayload(1234567890123456789ULL, data); QCOMPARE(payload.size(), 8 + 22); quint64 sentUs = 0; QByteArray out; QVERIFY(splitBytesPayload(payload, &sentUs, &out)); QCOMPARE(sentUs, quint64(1234567890123456789ULL)); QCOMPARE(out, data); // Empty data: stamp only. payload = bytesPayload(42, {}); QVERIFY(splitBytesPayload(payload, &sentUs, &out)); QCOMPARE(sentUs, quint64(42)); QVERIFY(out.isEmpty()); QCOMPARE(payload.size(), 8); } void TestWireCodec::resizePayloadRoundTrip() // NOLINT(readability-convert-member-functions-to-static) { QByteArray payload = resizePayload(132, 41); QCOMPARE(payload.size(), 8); int cols = 0; int rows = 0; QVERIFY(splitResizePayload(payload, &cols, &rows)); QCOMPARE(cols, 132); QCOMPARE(rows, 41); } void TestWireCodec::exitPayloadRoundTrip() // NOLINT(readability-convert-member-functions-to-static) { for (const int code : {0, 1, -1, 127, -127}) { QByteArray payload = exitPayload(code); QCOMPARE(payload.size(), 4); int decoded = 0; QVERIFY(splitExitPayload(payload, &decoded)); QCOMPARE(decoded, code); } // Pin the negative-code wire representation: two's complement little-endian. const QByteArray payload = exitPayload(-1); QCOMPARE(quint8(payload[0]), 0xFF); QCOMPARE(quint8(payload[1]), 0xFF); QCOMPARE(quint8(payload[2]), 0xFF); QCOMPARE(quint8(payload[3]), 0xFF); } void TestWireCodec::splitHelpersRejectShortPayloads() // NOLINT(readability-convert-member-functions-to-static) { quint64 u = 0; int a = 0; QByteArray data; QVERIFY(!splitBytesPayload({}, &u, &data)); QVERIFY(!splitBytesPayload(QByteArray(7, '\0'), &u, &data)); QVERIFY(!splitResizePayload({}, &a, &a)); QVERIFY(!splitResizePayload(QByteArray(7, '\0'), &a, &a)); QVERIFY(!splitExitPayload({}, &a)); QVERIFY(!splitExitPayload(QByteArray(3, '\0'), &a)); } QTEST_GUILESS_MAIN(TestWireCodec) #include "tst_wirecodec.moc"