Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/win-vhid-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
75 changes: 71 additions & 4 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,94 @@ function(hidapi_add_vdev_test name provider backend)
)
endfunction()

# Define a tier-1 hotplug-API test <name> 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 <backend>. 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 <name> built from
# test_hotplug.c + <provider>. 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.
# <event_timeout_ms> is the per-event wait budget inside the test;
# <ctest_timeout> 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) ----------------
# Built whenever the libusb backend is, so it keeps compiling; at runtime it
# 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 "$<TARGET_FILE_DIR:hidapi_winapi>")
# 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()
Expand All @@ -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()
44 changes: 44 additions & 0 deletions src/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<backend>`** (`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_<backend>`** (`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

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading