Test the input seqlock and the IPC magic/version handshake
New protocol_test covers two previously-untested cross-process contracts: - publish_pads/read_pads seqlock: a concurrent writer + reader run for ~700 ms; the reader (2.5M snapshots) never observes a torn snapshot, and read_pads returns false (bounded, no hang) when the sequence is stuck odd mid-write. - IpcClient::connect refuses a section with a mismatched version or a bad magic, and accepts a matching header -- the ABI safety net that only had happy-path coverage before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -114,8 +114,6 @@ From an in-depth review pass. Each item is fixed test-first (a failing test, the
|
|||||||
as its own commit; "verify" items are confirmed real before any change, and dropped if not.
|
as its own commit; "verify" items are confirmed real before any change, and dropped if not.
|
||||||
|
|
||||||
Test coverage:
|
Test coverage:
|
||||||
- **Seqlock reader paths** — torn-read retry, odd-sequence skip, attempt-exhaustion → false.
|
|
||||||
- **Version/magic mismatch rejection** — negative test that the hook refuses a bad version.
|
|
||||||
- **Dedicated hook tests** — `focus_spoof`, `vk_hook` (present), `d3d9_hook`.
|
- **Dedicated hook tests** — `focus_spoof`, `vk_hook` (present), `d3d9_hook`.
|
||||||
- **Misc units** — `SharedMemory` RAII/move, `wav` malformed input, `tool_paths` resolution, injector
|
- **Misc units** — `SharedMemory` RAII/move, `wav` malformed input, `tool_paths` resolution, injector
|
||||||
bitness check; strengthen the `audio_ring` overrun-at-seam case.
|
bitness check; strengthen the `audio_ring` overrun-at-seam case.
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ add_executable(log_ring_test log_ring_test.cpp)
|
|||||||
target_link_libraries(log_ring_test PRIVATE coop_common)
|
target_link_libraries(log_ring_test PRIVATE coop_common)
|
||||||
add_test(NAME log_ring_test COMMAND log_ring_test)
|
add_test(NAME log_ring_test COMMAND log_ring_test)
|
||||||
|
|
||||||
|
# Unit test for the input seqlock (publish_pads/read_pads: torn-free concurrent reads + bounded
|
||||||
|
# give-up) and the hook's IpcClient magic/version handshake rejection. Includes the hook-side header.
|
||||||
|
add_executable(protocol_test protocol_test.cpp)
|
||||||
|
target_include_directories(protocol_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
|
||||||
|
target_link_libraries(protocol_test PRIVATE coop_common)
|
||||||
|
add_test(NAME protocol_test COMMAND protocol_test)
|
||||||
|
|
||||||
# Unit test for the audio mixer math (decode/sum/soft-clip/encode). Header-only.
|
# Unit test for the audio mixer math (decode/sum/soft-clip/encode). Header-only.
|
||||||
add_executable(audio_mix_test audio_mix_test.cpp)
|
add_executable(audio_mix_test audio_mix_test.cpp)
|
||||||
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
||||||
@@ -328,6 +335,7 @@ coop_output_subdir(tests
|
|||||||
hook_install_test
|
hook_install_test
|
||||||
mkb_ring_test
|
mkb_ring_test
|
||||||
log_ring_test
|
log_ring_test
|
||||||
|
protocol_test
|
||||||
mkb_map_test
|
mkb_map_test
|
||||||
audio_mix_test
|
audio_mix_test
|
||||||
tone_analysis_test
|
tone_analysis_test
|
||||||
|
|||||||
128
tests/protocol_test.cpp
Normal file
128
tests/protocol_test.cpp
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// 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 <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#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<bool> stop{false};
|
||||||
|
std::atomic<long long> torn{0};
|
||||||
|
std::atomic<long long> 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<SharedBlock>();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user