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:
@@ -268,6 +268,21 @@ target_link_libraries(d3d9_hook_test PRIVATE
|
||||
dxgi)
|
||||
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
|
||||
# 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
|
||||
@@ -386,6 +401,7 @@ coop_output_subdir(tests
|
||||
present_hook_test
|
||||
dx12_present_hook_test
|
||||
d3d9_hook_test
|
||||
focus_spoof_test
|
||||
opengl_hook_test
|
||||
mock_game_test
|
||||
audio_verify_test
|
||||
|
||||
113
tests/focus_spoof_test.cpp
Normal file
113
tests/focus_spoof_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user