Add focus_spoof_test (and close the dedicated-hook-tests item)

focus_spoof_test plays game + host: it creates a top-level window (which
find_main_window picks up), installs the spoof, and checks that
GetForegroundWindow / GetActiveWindow / GetFocus now report that window, that the
status flag flips on install and off on removal, and that a ClipCursor request is
swallowed while cursor release is the default. Skips if the window can't be
enumerated.

With this and d3d9_hook_test, the dedicated-hook-tests item is done. vk_hook's
present path is intentionally not given a separate in-process test: it's exercised
end to end by mock_game_test's vk-inject path ("hook captured Vulkan present
calls"), and the off-thread VkCapture it drives has vk_capture_perf_test -- a
standalone in-process Vulkan swapchain harness would just duplicate that at high
cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 02:12:42 +02:00
parent c9d071b203
commit e096a9f2e2
3 changed files with 129 additions and 3 deletions

View File

@@ -113,9 +113,6 @@ default** and covers anything the hooked path doesn't.
From an in-depth review pass. Each item is fixed test-first (a failing test, then the fix) and lands From an in-depth review pass. Each item is fixed test-first (a failing test, then the fix) and lands
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:
- **Dedicated hook tests** — `focus_spoof`, `vk_hook` (present). (`d3d9_hook` done.)
Features: Features:
- **Static CRT (`/MT`) for `coop_hook.dll`** (x64 + x86) so it loads in games without the VC++ redist. - **Static CRT (`/MT`) for `coop_hook.dll`** (x64 + x86) so it loads in games without the VC++ redist.
- **Stale Vulkan-layer registration cleanup** — remove a leftover `HKCU` implicit-layer entry from a - **Stale Vulkan-layer registration cleanup** — remove a leftover `HKCU` implicit-layer entry from a

View File

@@ -268,6 +268,21 @@ target_link_libraries(d3d9_hook_test PRIVATE
dxgi) dxgi)
add_test(NAME d3d9_hook_test COMMAND d3d9_hook_test) add_test(NAME d3d9_hook_test COMMAND d3d9_hook_test)
# In-process self-test for the focus-spoof subsystem: installs the spoof on the test's own top-level
# window and checks the focus-query APIs report it foreground/active/focused, that removal restores
# them, and that a ClipCursor request is swallowed while cursor release is active.
add_executable(focus_spoof_test
focus_spoof_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/focus_spoof.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(focus_spoof_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(focus_spoof_test PRIVATE
coop_common
safetyhook::safetyhook
user32)
add_test(NAME focus_spoof_test COMMAND focus_spoof_test)
# Comprehensive capture/audio/hook stress test against coop_mock_game: launches the # Comprehensive capture/audio/hook stress test against coop_mock_game: launches the
# animated, frame-numbered A/V game, injects coop_hook.dll, decodes captured frame # animated, frame-numbered A/V game, injects coop_hook.dll, decodes captured frame
# numbers (DX11 + DX12) to assert a monotonic/advancing mirror, checks audio capture, and # numbers (DX11 + DX12) to assert a monotonic/advancing mirror, checks audio capture, and
@@ -386,6 +401,7 @@ coop_output_subdir(tests
present_hook_test present_hook_test
dx12_present_hook_test dx12_present_hook_test
d3d9_hook_test d3d9_hook_test
focus_spoof_test
opengl_hook_test opengl_hook_test
mock_game_test mock_game_test
audio_verify_test audio_verify_test

113
tests/focus_spoof_test.cpp Normal file
View File

@@ -0,0 +1,113 @@
// In-process self-test for the focus-spoof subsystem (hook/src/focus_spoof.cpp). It plays game +
// host: creates a top-level window (which find_main_window picks up), installs the spoof, and checks
// that the focus-query APIs now report that window as foreground/active/focused even though the test
// isn't actually the foreground app -- and that removal restores them. Also checks the cursor-release
// gating: with clipping disallowed (the default), a ClipCursor request is swallowed. No game, no Steam.
#include <cstdio>
#include <windows.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
#include "ipc_client.hpp"
#include "focus_spoof.hpp"
using namespace coop;
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;
}
}
void pump()
{
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
} // namespace
int main()
{
SharedMemory shm;
if (!shm.create(shared_memory_name(GetCurrentProcessId()), sizeof(SharedBlock)))
{
std::printf("FAIL: create shared memory\n");
return 1;
}
auto* block = shm.as<SharedBlock>();
block->version = kProtocolVersion;
block->sequence.store(0, std::memory_order_relaxed);
block->magic = kProtocolMagic; // allow_cursor_clip stays 0 (zero-filled) -> release the cursor
hook::IpcClient ipc;
check(ipc.connect(10, 5), "IPC client connect");
// A real top-level window for find_main_window to pick up.
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"coop_focus_test";
RegisterClassExW(&wc);
HWND win = CreateWindowExW(0, wc.lpszClassName, L"CoopFocusTest", WS_OVERLAPPEDWINDOW, 0, 0, 320, 240, nullptr,
nullptr, wc.hInstance, nullptr);
ShowWindow(win, SW_SHOW);
pump();
// Install, retrying a few times in case the window isn't enumerable yet.
bool installed = false;
for (int i = 0; i < 20 && !installed; ++i)
{
installed = hook::install_focus_spoof(ipc);
if (!installed)
{
pump();
Sleep(20);
}
}
if (!installed)
{
std::printf("SKIP: focus spoof could not find the test window\n");
DestroyWindow(win);
UnregisterClassW(wc.lpszClassName, wc.hInstance);
return 0;
}
// The spoof must report OUR window as foreground/active/focused, even though the test process is
// not the real foreground app (so a coincidental match is unlikely).
check(GetForegroundWindow() == win, "GetForegroundWindow spoofed to the game window");
check(GetActiveWindow() == win, "GetActiveWindow spoofed to the game window");
check(GetFocus() == win, "GetFocus spoofed to the game window");
check(block->status.focus_spoof != 0, "status reports focus spoof active");
// Cursor release: with clipping disallowed (default), a ClipCursor request must be swallowed.
RECT want{10, 10, 50, 50};
ClipCursor(&want); // goes through the hook
RECT got{};
GetClipCursor(&got);
check(!(got.left == want.left && got.top == want.top && got.right == want.right && got.bottom == want.bottom),
"ClipCursor swallowed while cursor release is active (clip not applied)");
// Removal: the spoof is torn down. Check the deterministic signal (the status flag) rather than
// GetForegroundWindow's value -- a shown test window may legitimately BE the real foreground, so
// the un-hooked API can still return it.
hook::remove_focus_spoof();
check(block->status.focus_spoof == 0, "status reports focus spoof inactive after removal");
ClipCursor(nullptr); // tidy up
DestroyWindow(win);
UnregisterClassW(wc.lpszClassName, wc.hInstance);
std::printf(g_failures == 0 ? "FOCUS SPOOF TEST PASS\n" : "FOCUS SPOOF TEST FAILED (%d)\n", g_failures);
return g_failures == 0 ? 0 : 1;
}