diff --git a/.github/workflows/win-vhid-test.yml b/.github/workflows/win-vhid-test.yml index 3d814e758..f4c100e10 100644 --- a/.github/workflows/win-vhid-test.yml +++ b/.github/workflows/win-vhid-test.yml @@ -145,7 +145,7 @@ jobs: shell: pwsh working-directory: build run: | - ctest -C Release -R DeviceIO_winapi --output-on-failure + ctest -C Release -R "_winapi" --output-on-failure - name: Cleanup virtual device if: always() diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index ff17785c6..26e9e6b14 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -48,9 +48,59 @@ function(hidapi_add_vdev_test name provider backend) ) endfunction() +# Define a tier-1 hotplug-API test built from test_hotplug_api.c only +# (NO virtual-device provider: it needs no device and no privileges, so it runs +# in the ordinary CI matrix), linked against the HIDAPI . It +# self-skips (77) when the backend reports hotplug as unsupported at runtime +# (e.g. a libusb without LIBUSB_CAP_HAS_HOTPLUG). +function(hidapi_add_hotplug_api_test name backend) + add_executable(${name} test_hotplug_api.c) + set_target_properties(${name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_link_libraries(${name} PRIVATE ${backend} Threads::Threads) + if(HIDAPI_ENABLE_ASAN AND NOT MSVC) + target_link_options(${name} PRIVATE -fsanitize=address) + endif() + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES + SKIP_RETURN_CODE 77 + TIMEOUT 120 + ) +endfunction() + +# Define a tier-2 (device-backed) hotplug test built from +# test_hotplug.c + . Self-skips when no virtual device can be +# created here, when the backend reports hotplug as unsupported, or when the +# provider cannot toggle device presence (test_virtual_device_unplug/_replug); +# currently only the uhid provider implements presence toggling. +# is the per-event wait budget inside the test; +# bounds the whole run. +function(hidapi_add_hotplug_test name provider backend event_timeout_ms ctest_timeout) + add_executable(${name} test_hotplug.c ${provider}) + set_target_properties(${name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_compile_definitions(${name} PRIVATE + TEST_HOTPLUG_EVENT_TIMEOUT_MS=${event_timeout_ms}) + target_link_libraries(${name} PRIVATE ${backend} Threads::Threads) + if(HIDAPI_ENABLE_ASAN AND NOT MSVC) + target_link_options(${name} PRIVATE -fsanitize=address) + endif() + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES + SKIP_RETURN_CODE 77 + TIMEOUT ${ctest_timeout} + ) +endfunction() + # --- Linux: hidraw backend via /dev/uhid ----------------------------------- if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_hidraw) hidapi_add_vdev_test(DeviceIO_hidraw test_virtual_device_uhid.c hidapi_hidraw) + hidapi_add_hotplug_api_test(HotplugAPI_hidraw hidapi_hidraw) + hidapi_add_hotplug_test(Hotplug_hidraw test_virtual_device_uhid.c hidapi_hidraw 10000 120) endif() # --- Linux: libusb backend via /dev/raw-gadget (+ dummy_hcd) ---------------- @@ -58,22 +108,34 @@ endif() # self-skips unless the raw-gadget virtual device has been set up (CI job). if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_libusb) hidapi_add_vdev_test(DeviceIO_libusb test_virtual_device_rawgadget.c hidapi_libusb) + hidapi_add_hotplug_api_test(HotplugAPI_libusb hidapi_libusb) + # Self-skips until the rawgadget provider implements unplug/replug; the + # generous budgets anticipate the full (virtual) USB stack round trips. + hidapi_add_hotplug_test(Hotplug_libusb test_virtual_device_rawgadget.c hidapi_libusb 30000 300) endif() # --- Windows: winapi backend via a modified vhidmini2 UMDF driver ----------- if(WIN32 AND TARGET hidapi_winapi) hidapi_add_vdev_test(DeviceIO_winapi test_virtual_device_win.c hidapi_winapi) - # HidD_GetPreparsedData / HidP_GetCaps used by the Windows provider. - target_link_libraries(DeviceIO_winapi PRIVATE hid) + hidapi_add_hotplug_api_test(HotplugAPI_winapi hidapi_winapi) + # Self-skips until the vhidmini2 provider implements unplug/replug. + hidapi_add_hotplug_test(Hotplug_winapi test_virtual_device_win.c hidapi_winapi 30000 300) + # hid: HidD_GetPreparsedData / HidP_GetCaps (device caps). + # cfgmgr32: CM_Locate_DevNodeA / CM_Disable_DevNode / CM_Enable_DevNode, used + # by the provider to toggle the root devnode's presence (unplug/replug). + # Set on the target (not via #pragma comment(lib), which MinGW ignores) so + # every winapi toolchain -- MSVC, clang-cl and MinGW -- links it. + target_link_libraries(DeviceIO_winapi PRIVATE hid cfgmgr32) + target_link_libraries(Hotplug_winapi PRIVATE hid cfgmgr32) # Run from the directory holding the hidapi DLL so a shared build can find # it at launch (there is no rpath on Windows). - set_tests_properties(DeviceIO_winapi PROPERTIES + set_tests_properties(DeviceIO_winapi HotplugAPI_winapi Hotplug_winapi PROPERTIES WORKING_DIRECTORY "$") # With ASan (MSVC) the test exe needs the ASan runtime DLL, which lives next # to the MSVC tools; add it to PATH (CMake >= 3.22). if(HIDAPI_ENABLE_ASAN AND MSVC AND NOT CMAKE_VERSION VERSION_LESS "3.22") get_filename_component(MSVC_BUILD_TOOLS_DIR "${CMAKE_LINKER}" DIRECTORY) - set_property(TEST DeviceIO_winapi PROPERTY + set_property(TEST DeviceIO_winapi HotplugAPI_winapi Hotplug_winapi PROPERTY ENVIRONMENT_MODIFICATION "PATH=path_list_append:${MSVC_BUILD_TOOLS_DIR}") endif() endif() @@ -83,6 +145,11 @@ endif() # available (e.g. hosted CI runners); usable locally / on a self-hosted Mac. if(APPLE AND TARGET hidapi_darwin) hidapi_add_vdev_test(DeviceIO_darwin test_virtual_device_mac.c hidapi_darwin) + hidapi_add_hotplug_api_test(HotplugAPI_darwin hidapi_darwin) + # Self-skips until the IOHIDUserDevice provider implements unplug/replug. + hidapi_add_hotplug_test(Hotplug_darwin test_virtual_device_mac.c hidapi_darwin 30000 300) target_link_libraries(DeviceIO_darwin PRIVATE "-framework IOKit" "-framework CoreFoundation") + target_link_libraries(Hotplug_darwin PRIVATE + "-framework IOKit" "-framework CoreFoundation") endif() diff --git a/src/tests/README.md b/src/tests/README.md index 1b6e08e29..0a33eb943 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -22,6 +22,47 @@ command bytes, expected payloads). | Test | What it exercises | |------|-------------------| | `test_device_io.c` | open → write an output report → trigger+read input reports (Feature-report write, then input-report read-back) → close | +| `test_hotplug_api.c` | tier-1 hotplug API contract, no device needed: argument validation, handle properties, implicit init, `hid_exit()` teardown, register/deregister thread churn | +| `test_hotplug.c` | tier-2 hotplug scenarios against a virtual device whose presence is toggled: async delivery, exactly-once ENUMERATE pass, callback-return deregistration, pass-before-live ordering, payloads, filtering, dispatch order, deregistration post-condition, re-entrant registration | + +## Hotplug tests + +The hotplug tests come in two tiers: + +* **Tier 1 — `HotplugAPI_`** (`test_hotplug_api.c`): everything in the + hotplug contract observable *without* a device event. Needs no virtual + device, no privileges, so it runs against **every** backend in the ordinary + per-push CI matrix. Self-skips (77) when the backend reports hotplug as + unsupported at runtime (e.g. a libusb without `LIBUSB_CAP_HAS_HOTPLUG`). +* **Tier 2 — `Hotplug_`** (`test_hotplug.c`): device-backed hotplug + scenarios. On top of a virtual device, the provider must be able to *toggle + the device's presence* (`test_virtual_device_unplug()` / + `test_virtual_device_replug()` in `test_virtual_device.h`). Currently only + the **uhid** provider implements toggling (a `UHID_DESTROY` / + `UHID_CREATE2` pair on the same open `/dev/uhid` fd), so `Hotplug_hidraw` + is the one tier-2 test that actually runs (in `builds.yml`'s ubuntu-cmake + job, like `DeviceIO_hidraw`); the other providers return + `TEST_VDEV_UNAVAILABLE` from the toggle calls and their `Hotplug_*` tests + self-skip everywhere until presence toggling is implemented for them. + +| Test | Runs per-push in `builds.yml` | Notes | +|------|-------------------------------|-------| +| `HotplugAPI_hidraw` | yes (ubuntu-cmake) | | +| `HotplugAPI_libusb` | yes (ubuntu-cmake) | needs libusb hotplug support at runtime | +| `HotplugAPI_winapi` | yes (windows-cmake, MSVC/NMake/ClangCL/MinGW) | | +| `HotplugAPI_darwin` | yes (macos-cmake) | | +| `Hotplug_hidraw` | yes (ubuntu-cmake, via `uhid`) | the only tier-2 test that runs today | +| `Hotplug_libusb` | builds, self-skips | needs rawgadget unplug/replug (future) | +| `Hotplug_winapi` | builds, self-skips | needs driver-side presence toggling (future) | +| `Hotplug_darwin` | builds, self-skips | needs `IOHIDUserDevice` re-creation (future) | + +The tier-2 test is written against strict synchronization rules (hotplug tests +are notoriously flaky otherwise): callbacks only deep-copy the event into a +log under a lock; every expectation is awaited with a deadline-based predicate +poll (never a bare sleep); the *absence* of an event is asserted behind an +**event barrier** — a later event that is provably ordered after the missing +one — never behind a time window; and a missed event within the (generous) +budget is treated as a bug, not retried. ## Providers @@ -90,6 +131,9 @@ cmake -B build -S . -DHIDAPI_WITH_TESTS=ON cmake --build build sudo modprobe uhid sudo ctest --test-dir build -R DeviceIO_hidraw --output-on-failure +sudo ctest --test-dir build -R Hotplug_hidraw --output-on-failure +# tier-1 hotplug API tests need no device and no root: +ctest --test-dir build -R HotplugAPI --output-on-failure ``` On Windows/macOS configure with `-DHIDAPI_WITH_TESTS=ON` and run `ctest`; the diff --git a/src/tests/test_hotplug.c b/src/tests/test_hotplug.c new file mode 100644 index 000000000..c36da97eb --- /dev/null +++ b/src/tests/test_hotplug.c @@ -0,0 +1,1044 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Tier-2 hotplug tests, run against a virtual HID device whose + presence can be toggled (test_virtual_device_unplug/_replug): + asynchronous delivery, the exactly-once ENUMERATE pass, + callback-return deregistration, pass-before-live ordering, + ARRIVED/LEFT payloads, VID/PID filtering, dispatch order, + deregistration post-conditions and re-entrant (in-callback) + registration. + + Synchronization discipline (hotplug tests are notoriously + flaky when built on sleeps): + - callbacks only lock, deep-copy the event into a log, + unlock and return; they never call hid_enumerate/hid_open/ + hid_error(NULL); + - every expectation is awaited with a deadline-based + predicate poll (hp_wait_*), never a bare sleep; + - ABSENCE of an event is asserted behind an event barrier + (a later event that is provably ordered after the missing + one), never behind a time window. + + All assertions filter on the test's own VID/PID/serial: real + devices may be present on the host and may generate events + concurrently. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include + +#include + +#include "test_virtual_device.h" +#include "test_platform.h" + +/* CTest treats this exit code as "skipped" (see SKIP_RETURN_CODE in CMake). */ +#define EXIT_SKIP 77 + +/* Test-unique ids so enumeration/filtering cannot collide with real hardware. + On Linux/macOS the device is created on demand, so the primary uses a PID + distinct from test_device_io.c's 0x9001. On Windows the virtual device is a + single pre-installed static driver (src/tests/windows/driver) whose identity + is fixed, so the primary must match it (PID 0x9001, serial == the driver's + VHIDMINI_SERIAL_NUMBER_STRING); the second device has no counterpart there and + is reported UNAVAILABLE by the Windows provider. */ +#define TEST_VID 0xF1D0 +#if defined(_WIN32) +#define TEST_PID 0x9001 /* the static vhidmini driver's HIDMINI_PID */ +#else +#define TEST_PID 0x9002 +#endif +#define TEST_PID_2 0x9003 /* second device, for the mid-pass stop test */ +#define TEST_SERIAL "HIDAPI-HOTPLUG-TEST" +#define TEST_SERIAL_2 "HIDAPI-HOTPLUG-TEST-2" + +#define ALL_EVENTS (HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT) + +/* Budget for one awaited event/predicate. The uhid provider is fast (10s is + generous); the future rawgadget/win providers go through a full (virtual) + USB stack, so their CMake target overrides this with 30s. */ +#ifndef TEST_HOTPLUG_EVENT_TIMEOUT_MS +#define TEST_HOTPLUG_EVENT_TIMEOUT_MS 30000 +#endif +#define EVENT_TIMEOUT_MS TEST_HOTPLUG_EVENT_TIMEOUT_MS + +#define WAIT_TICK_MS 10 + +static int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf(" CHECK failed: %s (line %d)\n", \ + #cond, __LINE__); \ + fflush(stdout); \ + g_failures++; \ + return -1; \ + } \ + } while (0) + +/* Print a flushed progress marker so a hang is localised on a CTest timeout. */ +static void step(const char *what) +{ + printf(" -> %s\n", what); + fflush(stdout); +} + +static void report(const char *name, int rc) +{ + printf("%s %s\n", rc == 0 ? "PASS" : "FAIL", name); + fflush(stdout); +} + +/* ------------------------------------------------------------------ */ +/* The event log. One global, ordered log shared by every callback: */ +/* cross-callback ordering assertions (dispatch order, barriers) fall */ +/* out of the log order itself. */ + +#define HP_MAX_EVENTS 128 +#define HP_PATH_MAX 256 +#define HP_SERIAL_MAX 64 + +typedef struct hp_event { + int seq; /* global arrival order */ + hid_hotplug_callback_handle handle; /* the callback_handle parameter */ + hid_hotplug_event event; + unsigned short vendor_id; + unsigned short product_id; + char path[HP_PATH_MAX]; + char serial[HP_SERIAL_MAX]; /* narrowed; "" when NULL */ + unsigned long long thread_id; /* thread the callback ran on */ + int next_was_null; /* device->next == NULL held */ +} hp_event; + +static test_mutex g_log_lock; +static hp_event g_events[HP_MAX_EVENTS]; +static int g_event_count; +static int g_event_overflow; +static int g_seq_counter; +static unsigned long long g_main_tid; + +/* Deep-copy the fields the assertions need. Called from the callbacks, with + g_log_lock held for the shortest possible time; the device pointer is only + valid for the duration of the callback. */ +static void hp_record(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event) +{ + test_mutex_lock(&g_log_lock); + if (g_event_count < HP_MAX_EVENTS) { + hp_event *e = &g_events[g_event_count++]; + memset(e, 0, sizeof(*e)); + e->seq = g_seq_counter++; + e->handle = handle; + e->event = event; + e->thread_id = test_thread_id(); + if (device) { + e->vendor_id = device->vendor_id; + e->product_id = device->product_id; + e->next_was_null = (device->next == NULL); + if (device->path) + snprintf(e->path, sizeof(e->path), "%s", device->path); + if (device->serial_number) { + size_t i; + for (i = 0; i + 1 < sizeof(e->serial) && device->serial_number[i]; i++) { + wchar_t wc = device->serial_number[i]; + e->serial[i] = (wc > 0 && wc < 128) ? (char)wc : '?'; + } + e->serial[i] = '\0'; + } + } + } else { + g_event_overflow = 1; + } + test_mutex_unlock(&g_log_lock); +} + +/* Does a logged event match? 0 acts as a wildcard for handle/event/pid; + NULL for serial. A non-zero pid additionally requires the test VID. */ +static int hp_match(const hp_event *e, hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, const char *serial) +{ + if (handle != 0 && e->handle != handle) + return 0; + if (event_mask != 0 && !(e->event & event_mask)) + return 0; + if (pid != 0 && (e->vendor_id != TEST_VID || e->product_id != pid)) + return 0; + if (serial != NULL && strcmp(e->serial, serial) != 0) + return 0; + return 1; +} + +static int hp_count(hid_hotplug_callback_handle handle, int event_mask, + unsigned short pid, const char *serial) +{ + int i, n = 0; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) + if (hp_match(&g_events[i], handle, event_mask, pid, serial)) + n++; + test_mutex_unlock(&g_log_lock); + return n; +} + +/* Copy the first matching event out of the log. Returns 0 when found. */ +static int hp_find_first(hp_event *out, hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, const char *serial) +{ + int i, found = -1; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) { + if (hp_match(&g_events[i], handle, event_mask, pid, serial)) { + *out = g_events[i]; + found = 0; + break; + } + } + test_mutex_unlock(&g_log_lock); + return found; +} + +/* Deadline-based predicate poll: the ONLY way the tests wait. */ +static int hp_wait_count_at_least(hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, + const char *serial, int min_count, + int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + if (hp_count(handle, event_mask, pid, serial) >= min_count) + return 0; + if (test_now_ms() >= deadline) + return -1; + test_sleep_ms(WAIT_TICK_MS); + } +} + +/* Wait for *flag (read under the log lock) to become non-zero. */ +static int hp_wait_flag(const int *flag, int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + int set; + test_mutex_lock(&g_log_lock); + set = *flag; + test_mutex_unlock(&g_log_lock); + if (set) + return 0; + if (test_now_ms() >= deadline) + return -1; + test_sleep_ms(WAIT_TICK_MS); + } +} + +/* Start-of-test reset. Also the global sweep for two invariants every event + must satisfy: never delivered on the registering (main) thread, and never + more events than the log can hold (an overflow would silently weaken the + later absence assertions). */ +static void hp_reset_log(const char *test_name) +{ + int i; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) { + if (g_events[i].thread_id == g_main_tid) { + printf(" INVARIANT failed before %s: an event was " + "delivered on the registering thread\n", test_name); + fflush(stdout); + g_failures++; + break; + } + } + if (g_event_overflow) { + printf(" INVARIANT failed before %s: event log overflow\n", test_name); + fflush(stdout); + g_failures++; + } + g_event_count = 0; + g_event_overflow = 0; + test_mutex_unlock(&g_log_lock); +} + +/* ------------------------------------------------------------------ */ +/* Callbacks. Per the synchronization discipline they only lock, */ +/* deep-copy, append, unlock and return. */ + +/* Plain recorder. */ +static int HID_API_CALL cb_log(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + (void)user_data; + hp_record(callback_handle, device, event); + return 0; +} + +/* Recorder that asks to be deregistered (returns 1) on the first event for + the test's primary device. */ +static int HID_API_CALL cb_return1_on_ours(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + (void)user_data; + hp_record(callback_handle, device, event); + if (device && device->vendor_id == TEST_VID && device->product_id == TEST_PID) + return 1; + return 0; +} + +/* Recorder that asks to be deregistered on its very first event, whichever + device it is for (the ENUMERATE snapshot order is unspecified). */ +static int HID_API_CALL cb_return1_first(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + (void)user_data; + hp_record(callback_handle, device, event); + return 1; +} + +/* T14: signals "entered", stays inside the callback for a while, then signals + "exited". Lets the main thread observe that deregistration blocks until an + in-progress invocation has completed. The context is heap-allocated and + freed right after deregistration returns: if the backend ever invoked the + callback again, ASan would flag the use-after-free below. */ +typedef struct slow_ctx { + int entered; + int exited; +} slow_ctx; + +static int HID_API_CALL cb_slow(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + slow_ctx *ctx = (slow_ctx *)user_data; + hp_record(callback_handle, device, event); + test_mutex_lock(&g_log_lock); + ctx->entered = 1; + test_mutex_unlock(&g_log_lock); + test_sleep_ms(250); + test_mutex_lock(&g_log_lock); + ctx->exited = 1; + test_mutex_unlock(&g_log_lock); + return 0; +} + +/* T15: on the first ARRIVED for the primary device, registers a child + callback WITH ENUMERATE and deregisters itself - both from within the + callback (the hotplug API is documented re-entrant). */ +typedef struct parent_ctx { + int acted; + int child_rc; + hid_hotplug_callback_handle child_handle; + int self_dereg_rc; +} parent_ctx; + +static int HID_API_CALL cb_parent(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + parent_ctx *ctx = (parent_ctx *)user_data; + int act = 0; + + hp_record(callback_handle, device, event); + + if (event == HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED + && device && device->vendor_id == TEST_VID && device->product_id == TEST_PID) { + test_mutex_lock(&g_log_lock); + if (!ctx->acted) { + ctx->acted = 1; + act = 1; + } + test_mutex_unlock(&g_log_lock); + } + + if (act) { + hid_hotplug_callback_handle child = 0; + int rc = hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &child); + int dereg_rc = hid_hotplug_deregister_callback(callback_handle); + test_mutex_lock(&g_log_lock); + ctx->child_rc = rc; + ctx->child_handle = child; + ctx->self_dereg_rc = dereg_rc; + test_mutex_unlock(&g_log_lock); + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Device-presence plumbing */ + +static test_virtual_device *g_vdev; /* primary device (TEST_PID) */ + +/* One hid_enumerate() pass: is a device with this pid+serial visible? + Only ever called from the main thread (HIDAPI's general thread-safety + rule), and never from inside a callback. */ +static int hp_enumerated_now(unsigned short pid, const char *serial) +{ + struct hid_device_info *devs = hid_enumerate(TEST_VID, pid); + struct hid_device_info *cur; + int found = 0; + for (cur = devs; cur; cur = cur->next) { + size_t i; + char narrow[HP_SERIAL_MAX] = ""; + if (!cur->serial_number) + continue; + for (i = 0; i + 1 < sizeof(narrow) && cur->serial_number[i]; i++) { + wchar_t wc = cur->serial_number[i]; + narrow[i] = (wc > 0 && wc < 128) ? (char)wc : '?'; + } + narrow[i] = '\0'; + if (strcmp(narrow, serial) == 0) { + found = 1; + break; + } + } + hid_free_enumeration(devs); + return found; +} + +/* Readiness barrier: poll enumeration until the device is (not) visible. */ +static int hp_wait_enumerated(unsigned short pid, const char *serial, + int present, int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + if (hp_enumerated_now(pid, serial) == present) + return 0; + if (test_now_ms() >= deadline) + return -1; + test_sleep_ms(50); + } +} + +/* Establish a known device state at the start of a test, whatever a previous + (possibly failed) test left behind. */ +static int ensure_present(void) +{ + if (!hp_enumerated_now(TEST_PID, TEST_SERIAL)) + (void)test_virtual_device_replug(g_vdev); + return hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS); +} + +static int ensure_absent(void) +{ + if (hp_enumerated_now(TEST_PID, TEST_SERIAL)) + (void)test_virtual_device_unplug(g_vdev); + return hp_wait_enumerated(TEST_PID, TEST_SERIAL, 0, EVENT_TIMEOUT_MS); +} + +/* ------------------------------------------------------------------ */ +/* T6: events are delivered asynchronously (never on the registering */ +/* thread) and the callback receives the same handle that */ +/* hid_hotplug_register_callback() wrote to *callback_handle. */ +static int t6_async_delivery(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event ev; + + CHECK(ensure_present() == 0); + hp_reset_log("T6"); + + step("register with ENUMERATE while the device is present"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + CHECK(h > 0); + + step("wait for the synthetic ARRIVED"); + CHECK(hp_wait_count_at_least(0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hp_find_first(&ev, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(ev.thread_id != g_main_tid); /* asynchronous delivery */ + CHECK(ev.handle == h); /* handle parameter == *callback_handle */ + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T7: each connection is reported exactly once - by the ENUMERATE */ +/* pass or as a live event, never both. The LEFT of a subsequent */ +/* unplug is the barrier proving no duplicate ARRIVED was in flight. */ +static int t7_exactly_once(void) +{ + hid_hotplug_callback_handle h = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T7"); + + step("register with ENUMERATE while the device is present"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + + step("wait for the synthetic ARRIVED"); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("unplug; the LEFT is the exactly-once barrier"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + + step("replug: the reconnection is one more ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 2, EVENT_TIMEOUT_MS) == 0); + + step("unplug again (barrier for the second ARRIVED)"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 2, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 2); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T8a: a non-zero callback return value deregisters the callback: the */ +/* handle is dead (-1) and no further events reach it. The barrier is */ +/* a second, still-registered callback observing a later event the */ +/* first one must not see. */ +static int t8a_return_deregisters(void) +{ + hid_hotplug_callback_handle h_ret = 0, h_bar = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T8a"); + + step("register the returns-1 callback and a barrier callback"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_return1_on_ours, NULL, &h_ret) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_bar) == 0); + + step("unplug: both callbacks see the LEFT; the first returns 1"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_ret, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("replug: only the barrier callback may see the ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hp_count(h_ret, 0, 0, NULL) == 1); /* exactly the one LEFT */ + step("the handle was already freed by the non-zero return"); + CHECK(hid_hotplug_deregister_callback(h_ret) == -1); + + CHECK(hid_hotplug_deregister_callback(h_bar) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T8b: a non-zero return during the ENUMERATE pass stops the */ +/* remainder of the pass: with TWO matching devices present, the */ +/* callback is invoked exactly once. A later ENUMERATE registration */ +/* observing both devices is the barrier. */ +static int t8b_return_stops_pass(void) +{ + test_virtual_device *vdev2 = NULL; + hid_hotplug_callback_handle h_once = 0, h_probe = 0; + int rc; + + CHECK(ensure_present() == 0); + + step("create the second device"); + rc = test_virtual_device_create(&vdev2, TEST_VID, TEST_PID_2, TEST_SERIAL_2); + if (rc == TEST_VDEV_UNAVAILABLE) { + /* Some providers (raw-gadget: a single dummy_udc.0) can only expose one + device at a time. This sub-test needs two concurrent devices, so skip + it here rather than failing -- it is not counted as a failure. */ + printf(" T8b needs a second concurrent device, unavailable on this " + "provider - skipping this sub-test\n"); + fflush(stdout); + return 0; + } + CHECK(rc == TEST_VDEV_OK && vdev2 != NULL); + if (hp_wait_enumerated(TEST_PID_2, TEST_SERIAL_2, 1, EVENT_TIMEOUT_MS) != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"second device did not enumerate"); + } + + hp_reset_log("T8b"); + + step("register a returns-1-immediately callback with ENUMERATE (both devices match)"); + rc = hid_hotplug_register_callback(TEST_VID, 0, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_return1_first, NULL, &h_once); + if (rc != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"registration failed"); + } + + step("wait for its single snapshot event"); + if (hp_wait_count_at_least(h_once, 0, 0, NULL, 1, EVENT_TIMEOUT_MS) != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"the returns-1 callback never fired"); + } + + step("barrier: a fresh ENUMERATE registration sees both devices"); + rc = hid_hotplug_register_callback(TEST_VID, 0, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h_probe); + if (rc != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"barrier registration failed"); + } + rc = 0; + if (hp_wait_count_at_least(h_probe, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) + rc = -1; + if (hp_wait_count_at_least(h_probe, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID_2, TEST_SERIAL_2, 1, EVENT_TIMEOUT_MS) != 0) + rc = -1; + + if (rc == 0) { + /* Exactly one invocation total; which device is unspecified. */ + if (hp_count(h_once, 0, 0, NULL) != 1) { + printf(" CHECK failed: the returns-1 callback saw %d events " + "(expected 1) (line %d)\n", + hp_count(h_once, 0, 0, NULL), __LINE__); + fflush(stdout); + rc = -1; + } + if (hid_hotplug_deregister_callback(h_once) != -1) { + printf(" CHECK failed: h_once was still registered (line %d)\n", __LINE__); + fflush(stdout); + rc = -1; + } + } + + (void)hid_hotplug_deregister_callback(h_probe); + test_virtual_device_destroy(vdev2); + (void)hp_wait_enumerated(TEST_PID_2, TEST_SERIAL_2, 0, EVENT_TIMEOUT_MS); + if (rc != 0) { + g_failures++; + return -1; + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T9: the ENUMERATE pass is delivered before live events: even when */ +/* the device is unplugged immediately after registration, the LEFT */ +/* must be preceded by the snapshot ARRIVED (same path). */ +static int t9_pass_before_live(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event arrived, left; + + CHECK(ensure_present() == 0); + hp_reset_log("T9"); + + step("register with ENUMERATE and unplug immediately"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + + step("wait for the LEFT"); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("the ARRIVED must already be logged, before the LEFT"); + CHECK(hp_find_first(&arrived, h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&left, h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(arrived.seq < left.seq); + CHECK(strcmp(arrived.path, left.path) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T10: live ARRIVED/LEFT payloads: matching VID/PID/serial on */ +/* arrival; the LEFT carries the same path and intact strings; and */ +/* device->next == NULL on every invocation. */ +static int t10_live_payloads(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event arrived, left; + int i, all_next_null = 1; + + CHECK(ensure_absent() == 0); + hp_reset_log("T10"); + + step("register (no ENUMERATE) while the device is absent"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h) == 0); + + step("plug: live ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_find_first(&arrived, h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(arrived.vendor_id == TEST_VID); + CHECK(arrived.product_id == TEST_PID); + CHECK(strcmp(arrived.serial, TEST_SERIAL) == 0); + CHECK(arrived.path[0] != '\0'); + + step("unplug: live LEFT correlates by path, strings intact"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_find_first(&left, h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(strcmp(left.path, arrived.path) == 0); + CHECK(strcmp(left.serial, TEST_SERIAL) == 0); + CHECK(left.vendor_id == TEST_VID && left.product_id == TEST_PID); + + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) + if (g_events[i].handle == h && !g_events[i].next_was_null) + all_next_null = 0; + test_mutex_unlock(&g_log_lock); + CHECK(all_next_null); /* device->next == NULL on EVERY invocation */ + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T11: without ENUMERATE there is no synthetic ARRIVED, yet the LEFT */ +/* of an already-present device is still delivered. Because the pass */ +/* precedes live events, receiving the LEFT with no prior ARRIVED */ +/* proves no synthetic event was pending (zero-window proof). */ +static int t11_left_without_enumerate(void) +{ + hid_hotplug_callback_handle h = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T11"); + + step("register WITHOUT ENUMERATE while the device is present"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h) == 0); + + step("unplug: the LEFT must still be delivered"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("zero synthetic ARRIVED (the LEFT is the barrier)"); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 0); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T12: VID/PID filtering: exact and vid-only filters and the wildcard */ +/* see the event; a non-matching filter does not. The wildcard */ +/* (registered last, dispatch is in registration order) anchors the */ +/* absence assertion. */ +static int t12_vid_pid_filtering(void) +{ + hid_hotplug_callback_handle h_match = 0, h_vid = 0, h_wrong = 0, h_wild = 0; + + CHECK(ensure_absent() == 0); + hp_reset_log("T12"); + + step("register exact / vid-only / wrong-vid / wildcard callbacks"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_match) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID, 0, ALL_EVENTS, 0, + cb_log, NULL, &h_vid) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID ^ 0x0001, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_wrong) == 0); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, + cb_log, NULL, &h_wild) == 0); + + step("plug the device"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + + step("exact, vid-only and wildcard callbacks see the ARRIVED"); + CHECK(hp_wait_count_at_least(h_match, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_vid, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_wild, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("the non-matching callback saw nothing of our device"); + /* The wildcard is dispatched after h_wrong (registration order), so once + the wildcard has logged the event, h_wrong's turn is provably over. */ + CHECK(hp_count(h_wrong, 0, TEST_PID, TEST_SERIAL) == 0); + + CHECK(hid_hotplug_deregister_callback(h_match) == 0); + CHECK(hid_hotplug_deregister_callback(h_vid) == 0); + CHECK(hid_hotplug_deregister_callback(h_wrong) == 0); + CHECK(hid_hotplug_deregister_callback(h_wild) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T13: one event is dispatched to every matching callback in */ +/* registration order. */ +static int t13_dispatch_order(void) +{ + hid_hotplug_callback_handle h_a = 0, h_b = 0; + hp_event ev_a, ev_b; + + CHECK(ensure_present() == 0); + hp_reset_log("T13"); + + step("register two matching callbacks"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_a) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_b) == 0); + + step("unplug: both see the LEFT"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_a, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_b, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("registration order == dispatch order"); + CHECK(hp_find_first(&ev_a, h_a, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&ev_b, h_b, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(ev_a.seq < ev_b.seq); + + CHECK(hid_hotplug_deregister_callback(h_a) == 0); + CHECK(hid_hotplug_deregister_callback(h_b) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T14: hid_hotplug_deregister_callback() (from a non-event thread) */ +/* returns only after an in-progress invocation has completed; then */ +/* the callback's resources can be freed safely even though more */ +/* events keep flowing (an ASan leg would catch a use-after-free). */ +static int t14_deregister_postcondition(void) +{ + hid_hotplug_callback_handle h_slow = 0, h_bar = 0; + slow_ctx *ctx; + int exited; + + CHECK(ensure_absent() == 0); + hp_reset_log("T14"); + + ctx = (slow_ctx *)calloc(1, sizeof(*ctx)); + CHECK(ctx != NULL); + + step("register the slow callback and a barrier callback"); + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_slow, ctx, &h_slow) != 0) { + free(ctx); + CHECK(!"registration failed"); + } + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_bar) != 0) { + (void)hid_hotplug_deregister_callback(h_slow); + free(ctx); + CHECK(!"barrier registration failed"); + } + + step("plug and wait for the slow callback to enter"); + if (test_virtual_device_replug(g_vdev) != TEST_VDEV_OK + || hp_wait_flag(&ctx->entered, EVENT_TIMEOUT_MS) != 0) { + (void)hid_hotplug_deregister_callback(h_slow); + (void)hid_hotplug_deregister_callback(h_bar); + free(ctx); + CHECK(!"the slow callback never entered"); + } + + step("deregister while the callback is (still) inside its invocation"); + CHECK(hid_hotplug_deregister_callback(h_slow) == 0); + test_mutex_lock(&g_log_lock); + exited = ctx->exited; + test_mutex_unlock(&g_log_lock); + CHECK(exited == 1); /* deregistration waited for the invocation */ + + step("free the callback's resources and keep events flowing"); + free(ctx); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hid_hotplug_deregister_callback(h_bar) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T15: register and deregister from within a callback: on its first */ +/* ARRIVED the parent registers a child callback with ENUMERATE (the */ +/* child must see the device exactly once, via its snapshot) and */ +/* deregisters itself. */ +static int t15_reentrant_registration(void) +{ + static parent_ctx ctx; /* static: zeroed, outlives any late invocation */ + hid_hotplug_callback_handle h_parent = 0, h_child = 0; + int child_rc, self_dereg_rc; + + CHECK(ensure_absent() == 0); + hp_reset_log("T15"); + memset(&ctx, 0, sizeof(ctx)); + + step("register the parent callback"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_parent, &ctx, &h_parent) == 0); + + step("plug: the parent registers the child and deregisters itself"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_flag(&ctx.acted, EVENT_TIMEOUT_MS) == 0); + + test_mutex_lock(&g_log_lock); + child_rc = ctx.child_rc; + h_child = ctx.child_handle; + self_dereg_rc = ctx.self_dereg_rc; + test_mutex_unlock(&g_log_lock); + CHECK(child_rc == 0); + CHECK(h_child > 0); + CHECK(self_dereg_rc == 0); /* deregistering itself, mid-callback, works */ + + step("the child sees the device exactly once (via its snapshot)"); + CHECK(hp_wait_count_at_least(h_child, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("unplug (barrier for the exactly-once assertion)"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_child, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h_child, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 1); + + step("the parent saw only its one ARRIVED and its handle is dead"); + CHECK(hp_count(h_parent, 0, 0, NULL) == 1); + CHECK(hid_hotplug_deregister_callback(h_parent) == -1); + + CHECK(hid_hotplug_deregister_callback(h_child) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ + +int main(void) +{ + int rc; + hid_hotplug_callback_handle probe = 0; + + g_main_tid = test_thread_id(); + test_mutex_init(&g_log_lock); + + if (hid_init() != 0) { + printf("hid_init() failed\n"); + return EXIT_FAILURE; + } + + step("probe hotplug support"); + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &probe) != 0) { + printf("hotplug reported unsupported here - skipping\n"); + hid_exit(); + return EXIT_SKIP; + } + (void)hid_hotplug_deregister_callback(probe); + + step("create virtual device"); + rc = test_virtual_device_create(&g_vdev, TEST_VID, TEST_PID, TEST_SERIAL); + if (rc == TEST_VDEV_UNAVAILABLE) { + printf("virtual device unavailable on this host - skipping\n"); + hid_exit(); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK || !g_vdev) { + printf("failed to create virtual device (rc=%d)\n", rc); + hid_exit(); + return EXIT_FAILURE; + } + + /* Probed before waiting for enumeration so that providers without + presence toggling skip instantly instead of after a full wait. */ + step("probe unplug/replug support"); + rc = test_virtual_device_unplug(g_vdev); + if (rc == TEST_VDEV_UNAVAILABLE) { + printf("this provider cannot toggle device presence - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK + || hp_wait_enumerated(TEST_PID, TEST_SERIAL, 0, EVENT_TIMEOUT_MS) != 0 + || test_virtual_device_replug(g_vdev) != TEST_VDEV_OK) { + printf("unplug/replug probe failed\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + return EXIT_FAILURE; + } + + /* The readiness barrier doubling as the presence probe: a virtual + device that never enumerates means this host cannot run the test + (same skip semantics as the device-I/O test). */ + step("wait for the device to enumerate"); + if (hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) { + printf("virtual device did not enumerate - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + return EXIT_SKIP; + } + + printf("running hotplug tests...\n"); + fflush(stdout); + + printf("T6: asynchronous delivery + handle parameter\n"); + report("T6 async_delivery", t6_async_delivery()); + printf("T7: exactly-once (ENUMERATE pass vs live events)\n"); + report("T7 exactly_once", t7_exactly_once()); + printf("T8a: non-zero callback return deregisters\n"); + report("T8a return_deregisters", t8a_return_deregisters()); + printf("T8b: non-zero return stops the rest of the ENUMERATE pass\n"); + report("T8b return_stops_pass", t8b_return_stops_pass()); + printf("T9: ENUMERATE pass delivered before live events\n"); + report("T9 pass_before_live", t9_pass_before_live()); + printf("T10: live ARRIVED/LEFT payloads\n"); + report("T10 live_payloads", t10_live_payloads()); + printf("T11: LEFT without ENUMERATE (zero-window proof)\n"); + report("T11 left_without_enumerate", t11_left_without_enumerate()); + printf("T12: VID/PID filtering\n"); + report("T12 vid_pid_filtering", t12_vid_pid_filtering()); + printf("T13: dispatch in registration order\n"); + report("T13 dispatch_order", t13_dispatch_order()); + printf("T14: deregistration post-condition\n"); + report("T14 deregister_postcondition", t14_deregister_postcondition()); + printf("T15: register/deregister from within a callback\n"); + report("T15 reentrant_registration", t15_reentrant_registration()); + + hp_reset_log("(final sweep)"); /* global invariants over the last test */ + + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + + printf("%s hotplug (%d failed checks)\n", + g_failures == 0 ? "PASS" : "FAIL", g_failures); + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/tests/test_hotplug_api.c b/src/tests/test_hotplug_api.c new file mode 100644 index 000000000..ef47734b7 --- /dev/null +++ b/src/tests/test_hotplug_api.c @@ -0,0 +1,384 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Tier-1 hotplug API tests: argument validation, callback-handle + properties, implicit initialization, hid_exit() teardown and + register/deregister thread-safety. + + These tests need NO device (virtual or real) and no privileges, + so they run against every backend in the ordinary CI matrix. + They only exercise the parts of the hotplug contract that are + observable without a device event; the device-backed scenarios + live in test_hotplug.c. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include + +#include + +#include "test_platform.h" + +/* CTest treats this exit code as "skipped" (see SKIP_RETURN_CODE in CMake). */ +#define EXIT_SKIP 77 + +#define ALL_EVENTS (HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT) + +/* Tier-1 runs with no device churn, so ABSENCE of callback invocations is + checked with a short bounded settle window (there is no event to use as a + barrier when the expectation is "no events at all"). */ +#define SETTLE_MS 1000 + +/* How long the two churn threads of T16 keep registering/deregistering. */ +#define CHURN_MS 2000 + +static int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf(" CHECK failed: %s (line %d)\n", \ + #cond, __LINE__); \ + fflush(stdout); \ + g_failures++; \ + return -1; \ + } \ + } while (0) + +/* Print a flushed progress marker so a hang is localised on a CTest timeout. */ +static void step(const char *what) +{ + printf(" -> %s\n", what); + fflush(stdout); +} + +static void report(const char *name, int rc) +{ + printf("%s %s\n", rc == 0 ? "PASS" : "FAIL", name); + fflush(stdout); +} + +/* ------------------------------------------------------------------ */ +/* Shared callback state */ + +static test_mutex g_lock; +static int g_cb_invocations; /* every invocation of cb_record */ +static int g_exit_returned; /* set by T5 right after hid_exit() returns */ +static int g_fired_after_exit; /* cb_record ran after g_exit_returned was set */ + +static int HID_API_CALL cb_record(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void)callback_handle; + (void)device; + (void)event; + (void)user_data; + test_mutex_lock(&g_lock); + g_cb_invocations++; + if (g_exit_returned) + g_fired_after_exit = 1; + test_mutex_unlock(&g_lock); + return 0; +} + +static int HID_API_CALL cb_noop(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void)callback_handle; + (void)device; + (void)event; + (void)user_data; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T4 doubles as the support probe: hid_hotplug_register_callback() as + the very FIRST library call must initialize the library implicitly + and succeed. If it fails, this backend/host has no hotplug support + (e.g. a libusb without LIBUSB_CAP_HAS_HOTPLUG) and the whole test is + skipped: the libusb backend checks the capability before validating + arguments, so not even T1 is meaningful without support. */ +static int t4_implicit_init_probe(int *supported) +{ + hid_hotplug_callback_handle handle = -123; + int rc; + + *supported = 0; + + step("register as the very first library call"); + rc = hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &handle); + if (rc != 0) { + printf(" hotplug reported unsupported here (rc=%d) - skipping\n", rc); + fflush(stdout); + return 0; + } + *supported = 1; + + CHECK(handle > 0); + step("deregister the probe callback"); + CHECK(hid_hotplug_deregister_callback(handle) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T1: invalid registration arguments -> -1, *callback_handle zeroed, + and a retrievable (non-NULL) global error string. The exact error + text is backend-specific, so only its existence is asserted. */ + +static int t1_check_invalid(unsigned short vid, unsigned short pid, + int events, int flags, hid_hotplug_callback_fn cb) +{ + hid_hotplug_callback_handle handle = 12345; /* poisoned: must be zeroed */ + int rc = hid_hotplug_register_callback(vid, pid, events, flags, cb, NULL, &handle); + CHECK(rc == -1); + CHECK(handle == 0); + CHECK(hid_error(NULL) != NULL); + return 0; +} + +static int t1_arg_validation(void) +{ + step("NULL callback"); + if (t1_check_invalid(0, 0, ALL_EVENTS, 0, NULL) != 0) + return -1; + + step("events == 0"); + if (t1_check_invalid(0, 0, 0, 0, cb_noop) != 0) + return -1; + + step("unknown events bits"); + if (t1_check_invalid(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | (1 << 10), 0, cb_noop) != 0) + return -1; + + step("unknown flags bits"); + if (t1_check_invalid(0, 0, ALL_EVENTS, (1 << 10), cb_noop) != 0) + return -1; + + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T2: handles are positive, never 0, and not reused while the library + remains initialized (a later registration gets a larger handle). */ +static int t2_handle_properties(hid_hotplug_callback_handle *out_stale) +{ + hid_hotplug_callback_handle h1 = 0, h2 = 0; + + step("register/deregister twice"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h1) == 0); + CHECK(h1 > 0); + CHECK(hid_hotplug_deregister_callback(h1) == 0); + + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h2) == 0); + CHECK(h2 > 0); + CHECK(h2 > h1); /* handles are not reused while initialized */ + CHECK(hid_hotplug_deregister_callback(h2) == 0); + + *out_stale = h2; /* a genuine but no-longer-registered handle for T3 */ + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T3: deregistering 0, negative, never-issued and already-deregistered + handles fails with -1, sets an error string and leaves a + still-registered callback untouched. */ +static int t3_stale_handles(hid_hotplug_callback_handle stale) +{ + hid_hotplug_callback_handle live = 0; + + step("register a live callback"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &live) == 0); + CHECK(live > 0); + + step("deregister invalid handles"); + CHECK(hid_hotplug_deregister_callback(0) == -1); + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(-1) == -1); + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(live + 1000) == -1); /* never issued */ + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(stale) == -1); /* already deregistered */ + CHECK(hid_error(NULL) != NULL); + + step("the live callback is unaffected"); + CHECK(hid_hotplug_deregister_callback(live) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T5: hid_exit() with callbacks still registered returns (a hang is + caught by the CTest timeout), invalidates the handles, and no + callback fires after it returned. Then the register->immediate-exit + teardown race is stressed in a loop. */ +static int t5_hid_exit_teardown(void) +{ + hid_hotplug_callback_handle ha = 0, hb = 0; + int i; + + step("register two callbacks"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_record, NULL, &ha) == 0); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_record, NULL, &hb) == 0); + + step("hid_exit() with callbacks still registered"); + CHECK(hid_exit() == 0); + test_mutex_lock(&g_lock); + g_exit_returned = 1; + test_mutex_unlock(&g_lock); + + step("old handles are invalid after re-init"); + CHECK(hid_init() == 0); + CHECK(hid_hotplug_deregister_callback(ha) == -1); + CHECK(hid_hotplug_deregister_callback(hb) == -1); + + step("no callback fires after hid_exit returned (settle window)"); + test_sleep_ms(SETTLE_MS); + test_mutex_lock(&g_lock); + i = g_fired_after_exit; + test_mutex_unlock(&g_lock); + CHECK(i == 0); + + step("register -> immediate hid_exit stress loop"); + for (i = 0; i < 50; i++) { + hid_hotplug_callback_handle h = 0; + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_record, NULL, &h) == 0); + CHECK(h > 0); + CHECK(hid_exit() == 0); + } + test_mutex_lock(&g_lock); + g_exit_returned = 0; + test_mutex_unlock(&g_lock); + CHECK(hid_init() == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T16: two threads register/deregister wildcard callbacks concurrently + (the hotplug API is documented thread-safe). Pass = no crash, no + hang (join timeout), no failed call. The threads never call + hid_error(NULL): the global error string is the one part of the + hotplug API the application must serialize itself. */ + +typedef struct churn_ctx { + volatile int stop; + long iterations; + long failures; +} churn_ctx; + +static void churn_thread_fn(void *arg) +{ + churn_ctx *ctx = (churn_ctx *)arg; + + while (!ctx->stop) { + hid_hotplug_callback_handle h = 0; + if (hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h) != 0) { + ctx->failures++; + continue; + } + if (h <= 0) + ctx->failures++; + if (hid_hotplug_deregister_callback(h) != 0) + ctx->failures++; + ctx->iterations++; + } +} + +static int t16_thread_churn(void) +{ + test_thread threads[2]; + churn_ctx ctxs[2]; + int i; + + memset(ctxs, 0, sizeof(ctxs)); + + step("start two register/deregister churn threads"); + CHECK(test_thread_start(&threads[0], churn_thread_fn, &ctxs[0]) == 0); + if (test_thread_start(&threads[1], churn_thread_fn, &ctxs[1]) != 0) { + ctxs[0].stop = 1; + (void)test_thread_join_timeout(&threads[0], 10000); + CHECK(!"failed to start the second churn thread"); + } + + test_sleep_ms(CHURN_MS); + ctxs[0].stop = 1; + ctxs[1].stop = 1; + + step("join the churn threads"); + CHECK(test_thread_join_timeout(&threads[0], 30000) == 0); + CHECK(test_thread_join_timeout(&threads[1], 30000) == 0); + + for (i = 0; i < 2; i++) { + printf(" thread %d: %ld iterations, %ld failures\n", + i, ctxs[i].iterations, ctxs[i].failures); + fflush(stdout); + CHECK(ctxs[i].failures == 0); + CHECK(ctxs[i].iterations > 0); + } + return 0; +} + +/* ------------------------------------------------------------------ */ + +int main(void) +{ + hid_hotplug_callback_handle stale = 0; + int supported = 0; + int rc; + + test_mutex_init(&g_lock); + + /* NOTE: no hid_init() here on purpose: T4 requires that the hotplug + registration is the very first library call. */ + + printf("running hotplug API tests...\n"); + fflush(stdout); + + printf("T4: implicit init (register as first library call)\n"); + fflush(stdout); + rc = t4_implicit_init_probe(&supported); + if (!supported) { + test_mutex_destroy(&g_lock); + return EXIT_SKIP; + } + report("T4 implicit_init", rc); + + printf("T1: registration argument validation\n"); + fflush(stdout); + report("T1 arg_validation", t1_arg_validation()); + + printf("T2: callback handle properties\n"); + fflush(stdout); + report("T2 handle_properties", t2_handle_properties(&stale)); + + printf("T3: stale/unknown handle deregistration\n"); + fflush(stdout); + report("T3 stale_handles", t3_stale_handles(stale)); + + printf("T5: hid_exit teardown with registered callbacks\n"); + fflush(stdout); + report("T5 hid_exit_teardown", t5_hid_exit_teardown()); + + printf("T16: register/deregister thread churn\n"); + fflush(stdout); + report("T16 thread_churn", t16_thread_churn()); + + hid_exit(); + test_mutex_destroy(&g_lock); + + printf("%s hotplug_api (%d failed checks)\n", + g_failures == 0 ? "PASS" : "FAIL", g_failures); + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/tests/test_platform.h b/src/tests/test_platform.h index c06a8857b..3c9312193 100644 --- a/src/tests/test_platform.h +++ b/src/tests/test_platform.h @@ -6,8 +6,8 @@ Copyright 2026. - Test support: tiny cross-platform helpers (threads, timing) - so the HIDAPI unit tests stay platform-neutral. + Test support: tiny cross-platform helpers (threads, mutexes, + timing) so the HIDAPI unit tests stay platform-neutral. The contents of this file may be used by anyone for any reason without any conditions and may be used as a @@ -21,11 +21,12 @@ #include #else #include + #include #include #endif /* Monotonic milliseconds for measuring elapsed time. */ -static long long test_now_ms(void) +static inline long long test_now_ms(void) { #ifdef _WIN32 return (long long)GetTickCount64(); @@ -36,7 +37,7 @@ static long long test_now_ms(void) #endif } -static void test_sleep_ms(int ms) +static inline void test_sleep_ms(int ms) { #ifdef _WIN32 Sleep((DWORD)ms); @@ -48,6 +49,64 @@ static void test_sleep_ms(int ms) #endif } +/* An id of the calling thread, usable for equality comparison only. */ +static inline unsigned long long test_thread_id(void) +{ +#ifdef _WIN32 + return (unsigned long long)GetCurrentThreadId(); +#else + /* pthread_t is opaque; the tests only ever compare ids for (in)equality, + and on every platform HIDAPI supports pthread_t is an integer or a + pointer, so the cast preserves the identity the tests care about. */ + return (unsigned long long)(uintptr_t)pthread_self(); +#endif +} + +/* A plain (non-recursive) mutex. */ +typedef struct test_mutex { +#ifdef _WIN32 + CRITICAL_SECTION cs; +#else + pthread_mutex_t mutex; +#endif +} test_mutex; + +static inline void test_mutex_init(test_mutex *m) +{ +#ifdef _WIN32 + InitializeCriticalSection(&m->cs); +#else + pthread_mutex_init(&m->mutex, NULL); +#endif +} + +static inline void test_mutex_destroy(test_mutex *m) +{ +#ifdef _WIN32 + DeleteCriticalSection(&m->cs); +#else + pthread_mutex_destroy(&m->mutex); +#endif +} + +static inline void test_mutex_lock(test_mutex *m) +{ +#ifdef _WIN32 + EnterCriticalSection(&m->cs); +#else + pthread_mutex_lock(&m->mutex); +#endif +} + +static inline void test_mutex_unlock(test_mutex *m) +{ +#ifdef _WIN32 + LeaveCriticalSection(&m->cs); +#else + pthread_mutex_unlock(&m->mutex); +#endif +} + /* A joinable thread running void fn(void*). Results are communicated through * the user's arg (this matches how the tests use a context struct). */ typedef struct test_thread { @@ -62,14 +121,14 @@ typedef struct test_thread { } test_thread; #ifdef _WIN32 -static DWORD WINAPI test__thread_entry(LPVOID p) +static inline DWORD WINAPI test__thread_entry(LPVOID p) { test_thread *t = (test_thread *)p; t->fn(t->arg); return 0; } #else -static void *test__thread_entry(void *p) +static inline void *test__thread_entry(void *p) { test_thread *t = (test_thread *)p; t->fn(t->arg); @@ -79,7 +138,7 @@ static void *test__thread_entry(void *p) #endif /* Returns 0 on success, -1 on failure. */ -static int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) +static inline int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) { t->fn = fn; t->arg = arg; @@ -93,7 +152,7 @@ static int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) } /* Join with a timeout. Returns 0 if the thread finished, -1 on timeout. */ -static int test_thread_join_timeout(test_thread *t, int timeout_ms) +static inline int test_thread_join_timeout(test_thread *t, int timeout_ms) { #ifdef _WIN32 DWORD r = WaitForSingleObject(t->handle, (DWORD)timeout_ms); diff --git a/src/tests/test_virtual_device.h b/src/tests/test_virtual_device.h index c326c2312..ecc9dfaec 100644 --- a/src/tests/test_virtual_device.h +++ b/src/tests/test_virtual_device.h @@ -95,6 +95,26 @@ hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeou /* Destroy the virtual device and free all resources. */ void test_virtual_device_destroy(test_virtual_device *dev); +/* + * Make the device disappear from the system (as if physically unplugged) + * WITHOUT destroying the test_virtual_device context: the context stays valid + * and the device can be re-plugged later with test_virtual_device_replug(). + * Used by the hotplug tests to generate disconnect/reconnect events. + * + * Returns TEST_VDEV_OK on success, TEST_VDEV_UNAVAILABLE when this provider + * cannot toggle device presence (the caller should then skip the test), or + * TEST_VDEV_ERROR on a hard failure. + */ +int test_virtual_device_unplug(test_virtual_device *dev); + +/* + * Make an unplugged device reappear, with the same VID/PID/serial (the + * platform device path MAY differ from the previous appearance). Only valid + * after a successful test_virtual_device_unplug(). Same return codes as + * test_virtual_device_unplug(). + */ +int test_virtual_device_replug(test_virtual_device *dev); + /* * Trigger a pre-recorded scenario on the device by sending the given command * as the first byte of a Feature report, using the ordinary public HIDAPI diff --git a/src/tests/test_virtual_device_mac.c b/src/tests/test_virtual_device_mac.c index 1241cb65b..d5288c65e 100644 --- a/src/tests/test_virtual_device_mac.c +++ b/src/tests/test_virtual_device_mac.c @@ -399,3 +399,17 @@ void test_virtual_device_destroy(test_virtual_device *dev) pthread_mutex_destroy(&dev->lock); free(dev); } + +/* Unplug/replug (device-presence toggling for the hotplug tests) is not + * implemented for this provider yet; the hotplug tests self-skip here. */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + (void)dev; + return TEST_VDEV_UNAVAILABLE; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + (void)dev; + return TEST_VDEV_UNAVAILABLE; +} diff --git a/src/tests/test_virtual_device_rawgadget.c b/src/tests/test_virtual_device_rawgadget.c index 5b2851c16..185e7af13 100644 --- a/src/tests/test_virtual_device_rawgadget.c +++ b/src/tests/test_virtual_device_rawgadget.c @@ -627,39 +627,82 @@ static void *int_in_thread_fn(void *arg) return NULL; } -int test_virtual_device_create(test_virtual_device **out_dev, - unsigned short vendor_id, - unsigned short product_id, - const char *serial) +/* rg_unplug()/rg_plug() are the repeatable "presence toggle" machinery factored + out of destroy()/create(): a plug (re)binds the gadget to the dummy_hcd UDC + and starts the workers (USB attach -> libusb ARRIVED); an unplug stops the + workers and closes the fd (USB detach -> libusb LEFT). Neither touches the + mutex/cond or the identity fields (vendor_id/product_id/serial), so the same + dev struct survives an unplug/replug cycle with its identity intact. */ + +/* The teardown half of the "plug": stop the worker threads and unbind the + gadget from the UDC. Closing the fd performs the USB detach. Leaves the + mutex/cond and the dev struct intact so the same dev can be replugged. + Idempotent: safe to call when already unplugged (fd == -1, no threads). */ +static void rg_unplug(struct test_virtual_device *dev) { - struct test_virtual_device *dev; - struct usb_raw_init init; - int rc; + dev->stop = 1; + pthread_mutex_lock(&dev->lock); + pthread_cond_broadcast(&dev->cond); + pthread_mutex_unlock(&dev->lock); - if (!out_dev) - return TEST_VDEV_ERROR; - *out_dev = NULL; + /* The ep0 thread is parked in the blocking EVENT_FETCH ioctl (and the + int_in thread may be in EP_WRITE); interrupt them with SIGUSR1 until the + ep0 thread reports it has left its loop, so the joins below don't hang. */ + { + int spins = 0; + while (dev->ep0_started && !dev->ep0_exited && spins++ < 500) { + pthread_kill(dev->ep0_thread, SIGUSR1); + if (dev->int_in_started) + pthread_kill(dev->int_in_thread, SIGUSR1); + sleep_ms(10); + } + } - dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); - if (!dev) - return TEST_VDEV_ERROR; + if (dev->int_in_started) { + pthread_join(dev->int_in_thread, NULL); + dev->int_in_started = 0; + } + if (dev->ep0_started) { + pthread_join(dev->ep0_thread, NULL); + dev->ep0_started = 0; + } + if (dev->fd >= 0) { + close(dev->fd); + dev->fd = -1; + } +} + +/* (Re-)bind the gadget to the dummy_hcd UDC and start the worker threads. This + is the "plug": open + INIT + RUN performs the USB attach and the ep0 thread + answers enumeration. Returns TEST_VDEV_UNAVAILABLE when there is no raw-gadget + node or no dummy_hcd UDC to bind (INIT/RUN failure, e.g. the UDC is already + bound by another gadget). On any failure it cleans up like the old create() + fail path and leaves dev in the unplugged state (fd == -1, threads stopped). */ +static int rg_plug(struct test_virtual_device *dev) +{ + struct usb_raw_init init; + int rc; + + /* Reset every per-plug field so a 2nd/3rd plug behaves exactly like the + first. The signal handler being (re)installed is harmless, and ep0_exited + MUST be 0 here so rg_unplug's SIGUSR1 spin drives the *new* ep0 thread. + The mutex/cond and identity (vendor/product/serial) are intentionally + left untouched -- they persist across the unplug/replug cycle. */ + dev->stop = 0; dev->fd = -1; dev->int_in_ep = -1; dev->int_in_addr = 0x81; + dev->configured = 0; + dev->ep0_started = 0; + dev->int_in_started = 0; + dev->ep0_exited = 0; dev->pending = TEST_VDEV_CMD_NONE; - dev->vendor_id = vendor_id; - dev->product_id = product_id; - snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); - pthread_mutex_init(&dev->lock, NULL); - pthread_cond_init(&dev->cond, NULL); dev->fd = open("/dev/raw-gadget", O_RDWR); if (dev->fd < 0) { int e = errno; - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; if (e == ENOENT || e == EACCES || e == EPERM || e == ENODEV) return TEST_VDEV_UNAVAILABLE; return TEST_VDEV_ERROR; @@ -672,11 +715,9 @@ int test_virtual_device_create(test_virtual_device **out_dev, init.speed = USB_SPEED_HIGH; if (ioctl(dev->fd, USB_RAW_IOCTL_INIT, &init) < 0 || ioctl(dev->fd, USB_RAW_IOCTL_RUN, 0) < 0) { - /* No dummy_hcd UDC present -> nothing to emulate on; skip. */ + /* No dummy_hcd UDC to bind (absent, or already in use) -> skip. */ close(dev->fd); - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; return TEST_VDEV_UNAVAILABLE; } @@ -692,29 +733,51 @@ int test_virtual_device_create(test_virtual_device **out_dev, goto fail_threads; dev->int_in_started = 1; - *out_dev = dev; return TEST_VDEV_OK; fail_threads: - dev->stop = 1; - pthread_mutex_lock(&dev->lock); - pthread_cond_broadcast(&dev->cond); - pthread_mutex_unlock(&dev->lock); - if (dev->ep0_started) { - int spins = 0; - while (!dev->ep0_exited && spins++ < 500) { - pthread_kill(dev->ep0_thread, SIGUSR1); - sleep_ms(10); - } - pthread_join(dev->ep0_thread, NULL); - } - close(dev->fd); - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + /* Stop+join whatever started and close the fd; rg_unplug leaves dev in the + unplugged state (fd == -1, *_started == 0), ready for a later replug. */ + rg_unplug(dev); return TEST_VDEV_ERROR; } +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + int rc; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + + /* Identity + lifetime state: these outlive any unplug/replug. The per-plug + fields are (re)initialised by rg_plug. */ + dev->vendor_id = vendor_id; + dev->product_id = product_id; + snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); + pthread_mutex_init(&dev->lock, NULL); + pthread_cond_init(&dev->cond, NULL); + + rc = rg_plug(dev); + if (rc != TEST_VDEV_OK) { + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return rc; + } + + *out_dev = dev; + return TEST_VDEV_OK; +} + hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) { wchar_t wserial[64]; @@ -771,40 +834,26 @@ void test_virtual_device_destroy(test_virtual_device *dev) { if (!dev) return; - - dev->stop = 1; - pthread_mutex_lock(&dev->lock); - pthread_cond_broadcast(&dev->cond); - pthread_mutex_unlock(&dev->lock); - - /* The ep0 thread is parked in the blocking EVENT_FETCH ioctl (and the - int_in thread may be in EP_WRITE); interrupt them with SIGUSR1 until the - ep0 thread reports it has left its loop, so the joins below don't hang. */ - { - int spins = 0; - while (dev->ep0_started && !dev->ep0_exited && spins++ < 500) { - pthread_kill(dev->ep0_thread, SIGUSR1); - if (dev->int_in_started) - pthread_kill(dev->int_in_thread, SIGUSR1); - sleep_ms(10); - } - } - - if (dev->int_in_started) { - pthread_join(dev->int_in_thread, NULL); - dev->int_in_started = 0; - } - if (dev->ep0_started) { - pthread_join(dev->ep0_thread, NULL); - dev->ep0_started = 0; - } - - if (dev->fd >= 0) { - close(dev->fd); - dev->fd = -1; - } - + rg_unplug(dev); pthread_cond_destroy(&dev->cond); pthread_mutex_destroy(&dev->lock); free(dev); } + +/* Device-presence toggling for the hotplug tests: unplug detaches the gadget + * (USB disconnect -> libusb LEFT) while keeping dev alive; replug re-attaches it + * with the same VID/PID/serial (USB connect -> libusb ARRIVED). */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + if (!dev) + return TEST_VDEV_ERROR; + rg_unplug(dev); + return TEST_VDEV_OK; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + if (!dev) + return TEST_VDEV_ERROR; + return rg_plug(dev); +} diff --git a/src/tests/test_virtual_device_uhid.c b/src/tests/test_virtual_device_uhid.c index a86fd1da5..9b6dbd52a 100644 --- a/src/tests/test_virtual_device_uhid.c +++ b/src/tests/test_virtual_device_uhid.c @@ -206,6 +206,33 @@ static void *pump_thread_fn(void *arg) return NULL; } +/* Announce the device to the kernel (UHID_CREATE2 on the open uhid fd). + * Returns 0 on success, -1 on failure (with errno set by write()). Used both + * by the initial create() and by test_virtual_device_replug(): the kernel + * allows a new UHID_CREATE2 on the same fd after a UHID_DESTROY. */ +static int uhid_write_create2(struct test_virtual_device *dev) +{ + struct uhid_event ev; + ssize_t written; + + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_CREATE2; + snprintf((char *)ev.u.create2.name, sizeof(ev.u.create2.name), "HIDAPI Test Device"); + snprintf((char *)ev.u.create2.uniq, sizeof(ev.u.create2.uniq), "%s", dev->serial); + memcpy(ev.u.create2.rd_data, k_report_descriptor, sizeof(k_report_descriptor)); + ev.u.create2.rd_size = (uint16_t)sizeof(k_report_descriptor); + ev.u.create2.bus = 0x03; /* BUS_USB */ + ev.u.create2.vendor = dev->vendor_id; + ev.u.create2.product = dev->product_id; + ev.u.create2.version = 0; + ev.u.create2.country = 0; + + pthread_mutex_lock(&dev->write_lock); + written = write(dev->fd, &ev, sizeof(ev)); + pthread_mutex_unlock(&dev->write_lock); + return written < 0 ? -1 : 0; +} + int test_virtual_device_create(test_virtual_device **out_dev, unsigned short vendor_id, unsigned short product_id, @@ -239,19 +266,7 @@ int test_virtual_device_create(test_virtual_device **out_dev, return TEST_VDEV_ERROR; } - memset(&ev, 0, sizeof(ev)); - ev.type = UHID_CREATE2; - snprintf((char *)ev.u.create2.name, sizeof(ev.u.create2.name), "HIDAPI Test Device"); - snprintf((char *)ev.u.create2.uniq, sizeof(ev.u.create2.uniq), "%s", dev->serial); - memcpy(ev.u.create2.rd_data, k_report_descriptor, sizeof(k_report_descriptor)); - ev.u.create2.rd_size = (uint16_t)sizeof(k_report_descriptor); - ev.u.create2.bus = 0x03; /* BUS_USB */ - ev.u.create2.vendor = vendor_id; - ev.u.create2.product = product_id; - ev.u.create2.version = 0; - ev.u.create2.country = 0; - - if (write(dev->fd, &ev, sizeof(ev)) < 0) { + if (uhid_write_create2(dev) != 0) { int e = errno; close(dev->fd); pthread_mutex_destroy(&dev->write_lock); @@ -321,6 +336,37 @@ hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeou } } +int test_virtual_device_unplug(test_virtual_device *dev) +{ + struct uhid_event ev; + ssize_t written; + + if (!dev || dev->fd < 0) + return TEST_VDEV_ERROR; + + /* UHID_DESTROY unregisters the HID device from the kernel (the hidraw + node disappears) but keeps the uhid fd usable: a later UHID_CREATE2 on + the same fd brings the device back. The event pump keeps running; it + simply sees no events while the device is unplugged. */ + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + + pthread_mutex_lock(&dev->write_lock); + written = write(dev->fd, &ev, sizeof(ev)); + pthread_mutex_unlock(&dev->write_lock); + return written < 0 ? TEST_VDEV_ERROR : TEST_VDEV_OK; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + if (!dev || dev->fd < 0) + return TEST_VDEV_ERROR; + + /* Same ids and serial as the original appearance; the kernel assigns a + fresh hidraw node, so the HIDAPI path may differ. */ + return uhid_write_create2(dev) != 0 ? TEST_VDEV_ERROR : TEST_VDEV_OK; +} + int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, unsigned char command) { diff --git a/src/tests/test_virtual_device_win.c b/src/tests/test_virtual_device_win.c index 6ad0961e5..e297edea4 100644 --- a/src/tests/test_virtual_device_win.c +++ b/src/tests/test_virtual_device_win.c @@ -31,9 +31,27 @@ #include #include #include +#include #include #include #include +#include + +/* + * The virtual-HID devnode is located by its INF hardware id, not a fixed instance + * path. The CI job installs it with `devcon install VhidminiUm.inf + * "root\VhidminiUm"`, but PnP derives the devnode's *instance id* from the driver's + * setup class (Class=HIDClass in the INF -> observed instance ROOT\HIDCLASS\0000), + * not from the hardware id, so the instance path is not knowable a priori. + * locate_vhid_devnode() instead scans the ROOT enumerator for the devnode whose + * hardware id contains this token (matched case-insensitively). + * + * Presence toggling (unplug/replug) disables/enables that devnode via cfgmgr32: + * disabling it tears down the HIDClass child PDO so the GUID_DEVINTERFACE_HID + * interface disappears (the winapi backend sees a removal); enabling it re-creates + * the interface (an arrival). + */ +#define VHID_HARDWARE_ID_MATCH "VHIDMINIUM" struct test_virtual_device { unsigned short vendor_id; @@ -42,6 +60,115 @@ struct test_virtual_device { ULONG feature_len; /* FeatureReportByteLength of the opened device */ }; +/* Case-insensitive: does haystack contain needle (needle already uppercase)? */ +static int contains_ci_upper(const char *haystack, const char *needle_upper) +{ + size_t nlen = strlen(needle_upper); + const char *p; + + if (nlen == 0) + return 1; + for (p = haystack; *p != '\0'; ++p) { + size_t i = 0; + while (i < nlen && p[i] != '\0' && + (char)toupper((unsigned char)p[i]) == needle_upper[i]) + ++i; + if (i == nlen) + return 1; + } + return 0; +} + +/* Locate the root-enumerated virtual-HID devnode by matching its INF hardware id + (VHID_HARDWARE_ID_MATCH), robust to the PnP-generated instance path and index. + Every devnode under the ROOT enumerator is scanned - enabled or disabled, since + a disabled root devnode is still enumerated and "configured" - so both unplug's + disable and replug's re-enable resolve the same node. Returns CR_SUCCESS with + *out_devinst set; CR_NO_SUCH_DEVNODE if no such devnode exists (driver/device + not installed here); otherwise the failing CONFIGRET. */ +static CONFIGRET locate_vhid_devnode(DEVINST *out_devinst) +{ + CONFIGRET cr; + ULONG list_len = 0; + char *list; + char *inst; + + cr = CM_Get_Device_ID_List_SizeA(&list_len, "ROOT", + CM_GETIDLIST_FILTER_ENUMERATOR); + if (cr != CR_SUCCESS) + return cr; + if (list_len < 2) + return CR_NO_SUCH_DEVNODE; + + list = (char *)malloc(list_len); + if (!list) + return CR_OUT_OF_MEMORY; + + cr = CM_Get_Device_ID_ListA("ROOT", list, list_len, + CM_GETIDLIST_FILTER_ENUMERATOR); + if (cr != CR_SUCCESS) { + free(list); + return cr; + } + + /* The list is a REG_MULTI_SZ of instance ids; walk each one. */ + for (inst = list; *inst != '\0'; inst += strlen(inst) + 1) { + DEVINST devinst; + char hwids[512]; + char *h; + ULONG hwlen = (ULONG)sizeof(hwids); + + if (CM_Locate_DevNodeA(&devinst, inst, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) + continue; + if (CM_Get_DevNode_Registry_PropertyA(devinst, CM_DRP_HARDWAREID, NULL, + hwids, &hwlen, 0) != CR_SUCCESS) + continue; + /* CM_DRP_HARDWAREID is itself a REG_MULTI_SZ; match any of its ids. */ + for (h = hwids; *h != '\0'; h += strlen(h) + 1) { + if (contains_ci_upper(h, VHID_HARDWARE_ID_MATCH)) { + *out_devinst = devinst; + free(list); + return CR_SUCCESS; + } + } + } + + free(list); + return CR_NO_SUCH_DEVNODE; +} + +/* Find the HID child PDO of the vhidmini function devnode. Presence toggling acts + on this leaf (not the function device): disabling/enabling it raises the HID + interface removal/arrival the winapi backend watches, while leaving the UMDF + host running - disabling the function device instead re-creates the child in a + non-started state, so its HID interface never comes back. Prefers the child + whose instance id is under the HID enumerator; falls back to the first child. */ +static CONFIGRET find_hid_child(DEVINST func, DEVINST *out_child) +{ + DEVINST child, first; + CONFIGRET cr; + + cr = CM_Get_Child(&child, func, 0); + if (cr != CR_SUCCESS) + return cr; /* CR_NO_SUCH_DEVNODE if the function device has no child */ + + first = child; /* fallback: the function device's first child */ + for (;;) { + char cid[MAX_DEVICE_ID_LEN]; + + if (CM_Get_Device_IDA(child, cid, (ULONG)sizeof(cid), 0) == CR_SUCCESS && + strncmp(cid, "HID\\", 4) == 0) { + *out_child = child; + return CR_SUCCESS; + } + if (CM_Get_Sibling(&child, child, 0) != CR_SUCCESS) + break; + } + + *out_child = first; + return CR_SUCCESS; +} + int test_virtual_device_create(test_virtual_device **out_dev, unsigned short vendor_id, unsigned short product_id, @@ -53,6 +180,18 @@ int test_virtual_device_create(test_virtual_device **out_dev, return TEST_VDEV_ERROR; *out_dev = NULL; + /* Windows cannot create HID devices on the fly; the CI job pre-installs a + single static vhidmini device whose identity is fixed (see + src/tests/windows/driver). Report UNAVAILABLE for any requested device that + is not actually present here - e.g. the mid-pass-stop test's second device - + so such tests skip cleanly instead of waiting for one that can never appear. */ + { + struct hid_device_info *infos = hid_enumerate(vendor_id, product_id); + if (!infos) + return TEST_VDEV_UNAVAILABLE; + hid_free_enumeration(infos); + } + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); if (!dev) return TEST_VDEV_ERROR; @@ -141,6 +280,100 @@ int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, void test_virtual_device_destroy(test_virtual_device *dev) { - /* The harness uninstalls the driver/device after the test. */ + /* The harness (CI) uninstalls the driver/device after the test. But if a + test unplugged (disabled the HID child) and exited before replugging it, + re-enable the child best-effort so a later run on the same host is not left + with a disabled device. Locating/enabling an absent, already-enabled, or + access-denied node is harmless, so the CONFIGRETs are intentionally ignored + here. */ + DEVINST func, child; + if (locate_vhid_devnode(&func) == CR_SUCCESS && + find_hid_child(func, &child) == CR_SUCCESS) + (void)CM_Enable_DevNode(child, 0); free(dev); } + +/* Unplug = disable the HID child PDO. Its GUID_DEVINTERFACE_HID interface + * disappears and the winapi backend's PnP notification fires a removal (the test + * then sees the device LEFT / gone from hid_enumerate), while the UMDF function + * device keeps running. This is also the hotplug test's capability probe, so a + * function devnode that cannot be located (driver/device not installed) or a + * child that cannot be disabled for lack of elevation (CR_ACCESS_DENIED) returns + * UNAVAILABLE, which makes the test skip cleanly instead of failing. */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + DEVINST func, child; + CONFIGRET cr; + + (void)dev; /* the devnode is installed out-of-band by the CI job */ + + cr = locate_vhid_devnode(&func); + if (cr == CR_NO_SUCH_DEVNODE) { + fprintf(stderr, "[win-vdev] no ROOT devnode with hardware id '%s' " + "(driver/device not installed) -> hotplug test skips\n", + VHID_HARDWARE_ID_MATCH); + return TEST_VDEV_UNAVAILABLE; /* driver/device not installed here */ + } + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] locate failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + cr = find_hid_child(func, &child); + if (cr != CR_SUCCESS) + return TEST_VDEV_OK; /* no HID child -> already absent, nothing to disable */ + + cr = CM_Disable_DevNode(child, 0); + if (cr == CR_SUCCESS) + return TEST_VDEV_OK; + if (cr == CR_ACCESS_DENIED) { + fprintf(stderr, "[win-vdev] CM_Disable_DevNode(child) -> CR_ACCESS_DENIED " + "(not elevated) -> hotplug test skips\n"); + return TEST_VDEV_UNAVAILABLE; /* not elevated -> skip, don't fail */ + } + fprintf(stderr, "[win-vdev] CM_Disable_DevNode(child) failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; +} + +/* Replug = re-enable the HID child PDO disabled by unplug(). Its HID interface is + * re-created, so the backend sees an arrival (the device is back in + * hid_enumerate). Failure here is a hard error, not a skip: if unplug() disabled + * the child we must be able to re-enable it. */ +int test_virtual_device_replug(test_virtual_device *dev) +{ + DEVINST func, child; + CONFIGRET cr; + + (void)dev; + + cr = locate_vhid_devnode(&func); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] replug locate failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + cr = find_hid_child(func, &child); + if (cr != CR_SUCCESS) { + /* The child devnode is gone entirely (not merely disabled); ask the + function device to re-report it, then retry. */ + (void)CM_Reenumerate_DevNode(func, CM_REENUMERATE_SYNCHRONOUS); + cr = find_hid_child(func, &child); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] replug: no HID child to enable: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + } + + cr = CM_Enable_DevNode(child, 0); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] CM_Enable_DevNode(child) failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + return TEST_VDEV_OK; +} diff --git a/src/tests/windows/driver/common.h b/src/tests/windows/driver/common.h index 9b14cc6d4..2ed4cbb74 100644 --- a/src/tests/windows/driver/common.h +++ b/src/tests/windows/driver/common.h @@ -39,7 +39,9 @@ Module Name: #define MAXIMUM_STRING_LENGTH (126 * sizeof(WCHAR)) #define VHIDMINI_MANUFACTURER_STRING L"UMDF Virtual hidmini device Manufacturer string" #define VHIDMINI_PRODUCT_STRING L"UMDF Virtual hidmini device Product string" -#define VHIDMINI_SERIAL_NUMBER_STRING L"UMDF Virtual hidmini device Serial Number string" +/* Must equal test_hotplug.c's TEST_SERIAL: the device-backed hotplug test + (Hotplug_winapi) matches this single static device by serial number. */ +#define VHIDMINI_SERIAL_NUMBER_STRING L"HIDAPI-HOTPLUG-TEST" #define VHIDMINI_DEVICE_STRING L"UMDF Virtual hidmini device" #define VHIDMINI_DEVICE_STRING_INDEX 5 #include