// Unit test for the cross-process input contract in coop/protocol.hpp + the hook's IPC handshake: // - the seqlock (publish_pads / read_pads): a concurrent reader never sees a torn snapshot, and a // writer stuck mid-update (odd sequence) makes read_pads give up (returns false) rather than hang; // - IpcClient::connect refuses a section whose magic or version doesn't match (the ABI safety net). #include #include #include #include #include #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" #include "ipc_client.hpp" // hook-side IpcClient (header-only) using namespace coop; using coop::hook::IpcClient; namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); if (!ok) { ++g_failures; } } } // namespace int main() { // --- Seqlock: concurrent reader never sees a torn snapshot --------------------------------- { SharedBlock block{}; block.sequence.store(0, std::memory_order_relaxed); std::atomic stop{false}; std::atomic torn{0}; std::atomic reads{0}; // Writer: publish all slots carrying the SAME generation in `packet`. A torn read would mix two // generations, so the slots' packet values would disagree. std::thread writer([&] { std::uint32_t gen = 1; while (!stop.load(std::memory_order_relaxed)) { CoopPadState pads[kMaxPads]; for (auto& p : pads) { p = CoopPadState{}; p.connected = 1; p.packet = gen; } publish_pads(block, pads, kMaxPads); ++gen; } }); std::thread reader([&] { while (!stop.load(std::memory_order_relaxed)) { CoopPadState out[kMaxPads]; std::uint32_t count = 0; if (read_pads(block, out, count)) { reads.fetch_add(1, std::memory_order_relaxed); for (std::uint32_t i = 1; i < kMaxPads; ++i) { if (out[i].packet != out[0].packet) { torn.fetch_add(1, std::memory_order_relaxed); break; } } } } }); std::this_thread::sleep_for(std::chrono::milliseconds(700)); stop.store(true, std::memory_order_relaxed); writer.join(); reader.join(); std::printf(" seqlock reads=%lld torn=%lld\n", reads.load(), torn.load()); check(reads.load() > 1000, "seqlock: reader got many snapshots"); check(torn.load() == 0, "seqlock: never a torn snapshot under concurrent writes"); } // --- Seqlock: a writer stuck mid-update (odd sequence) makes read_pads give up ---------------- { SharedBlock block{}; block.sequence.store(1, std::memory_order_relaxed); // odd = write in progress, never completed CoopPadState out[kMaxPads]; std::uint32_t count = 0; check(!read_pads(block, out, count), "seqlock: read_pads returns false when stuck mid-write (bounded, no hang)"); } // --- Handshake: IpcClient::connect rejects a wrong magic / version ---------------------------- { const unsigned long pid = GetCurrentProcessId(); // per-pid section name; no game needed SharedMemory shm; const bool created = shm.create(shared_memory_name(pid), sizeof(SharedBlock)); check(created, "handshake: created a section"); auto* block = shm.as(); // Wrong version. block->version = kProtocolVersion - 1; block->magic = kProtocolMagic; { IpcClient c; check(!c.connect(/*attempts=*/3, /*delay_ms=*/1), "handshake: connect refuses a mismatched version"); } // Wrong magic. block->version = kProtocolVersion; block->magic = kProtocolMagic ^ 0xDEAD; { IpcClient c; check(!c.connect(3, 1), "handshake: connect refuses a bad magic"); } // Correct header -> connects. block->magic = kProtocolMagic; { IpcClient c; check(c.connect(10, 1), "handshake: connect accepts a matching magic + version"); } } std::printf(g_failures == 0 ? "PASS protocol_test\n" : "FAILED protocol_test (%d)\n", g_failures); return g_failures == 0 ? 0 : 1; }