Skip to content
Draft
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
25 changes: 25 additions & 0 deletions .github/vmrun-libusb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh
# Guest-side commands for the libusb virtual-device test, run inside a virtme-ng
# VM (see .github/workflows/libusb-vhid-test.yml). Kept as a file so a complex
# command line doesn't have to survive vng's argument parser.
#
# Runs as root in the guest, with the host filesystem mounted, cwd at the
# workspace root (which contains the host-built 'build' tree).
set -x

# The guest's modules.dep may be trimmed; regenerate it so dummy_hcd /
# raw_gadget (and their dependencies) resolve from the overlaid /lib/modules.
depmod -a || true
modprobe dummy_hcd || true
modprobe raw_gadget || true
ls -l /dev/raw-gadget || true

ctest --test-dir build --output-on-failure
rc=$?

echo "=== diag ==="
lsmod | grep -E "raw_gadget|dummy_hcd|udc" || true
ls -l /sys/bus/usb/devices/ 2>/dev/null || true
dmesg | tail -40 || true

echo "VNG_CTEST_EXIT=${rc}"
25 changes: 23 additions & 2 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Configure CMake
run: |
rm -rf build install
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON -DHIDAPI_WITH_TESTS=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/framework -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/framework -DCMAKE_FRAMEWORK=ON -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
- name: Build CMake Shared
Expand All @@ -56,6 +56,14 @@ jobs:
- name: Build CMake Framework
working-directory: build/framework
run: make install
- name: Run virtual-device tests (IOHIDUserDevice self-skips on hosted CI)
working-directory: build/shared
run: |
# The macOS virtual device needs the com.apple.developer.hid.virtual.device
# entitlement and interactive user consent, neither available on a hosted
# runner, so DeviceIO_darwin self-skips (CTest code 77). This still
# verifies the provider builds and the test runs/links.
ASAN_OPTIONS=detect_leaks=0 ctest --output-on-failure
- name: Check artifacts
uses: andstor/file-existence-action@v2
with:
Expand Down Expand Up @@ -122,14 +130,27 @@ jobs:
- name: Configure CMake
run: |
rm -rf build install
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}"
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON -DHIDAPI_WITH_TESTS=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}"
cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}"
- name: Build CMake Shared
working-directory: build/shared
run: make install
- name: Build CMake Static
working-directory: build/static
run: make install
- name: Run virtual-device tests (uhid -> hidraw; raw-gadget self-skips)
working-directory: build/shared
run: |
# uhid ships in the 'extra' modules package on the runner kernel.
# Everything here is best-effort: if a virtual device can't be provided
# the matching test self-skips (CTest code 77) instead of failing.
sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true
sudo modprobe uhid || true
ls -l /dev/uhid || echo "/dev/uhid not present"
# Run as root so the test can create /dev/uhid and open the resulting
# /dev/hidrawN. LeakSanitizer off (udev keeps allocations alive at
# exit); ASan use-after-free / overflow detection stays active.
sudo env "ASAN_OPTIONS=detect_leaks=0" ctest --output-on-failure
- name: Check artifacts
uses: andstor/file-existence-action@v2
with:
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/libusb-vhid-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Linux libusb Virtual HID Device Test (manual)

# Runs the device-I/O test against the HIDAPI *libusb* backend using a real
# virtual USB HID device (USB Raw Gadget on top of dummy_hcd).
#
# The hosted ubuntu-latest (azure) kernel is built without the USB gadget
# subsystem, so raw_gadget/dummy_hcd can't be loaded (or even built) there. We
# therefore run the test inside a lightweight VM (virtme-ng + QEMU) booting a
# *generic* Ubuntu kernel, whose linux-modules-extra ships dummy_hcd and
# raw_gadget. The VM shares the host filesystem, so it runs the binaries built
# on the host. The same approach works locally and on WSL2 (which also lacks
# those modules in its default kernel).
#
# Runs on demand (workflow_dispatch) or on a PR labelled 'ci-virtual-device'.

on:
workflow_dispatch:
pull_request:
types: [opened, reopened, labeled, synchronize]

jobs:
libusb-rawgadget:
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: hidapisrc

- name: Install deps + a gadget-capable generic kernel + virtme-ng
run: |
set -eux
sudo apt-get update
sudo apt-get install -y \
build-essential cmake libudev-dev libusb-1.0-0-dev \
qemu-system-x86 virtme-ng linux-image-generic
# raw_gadget ships in the generic kernel's modules-extra; install that
# plus headers (needed to build dummy_hcd, which Ubuntu doesn't package).
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
echo "generic kernel: ${KVER}"
sudo apt-get install -y "linux-modules-extra-${KVER}" "linux-headers-${KVER}"
find "/lib/modules/${KVER}" \( -name 'raw_gadget*' -o -name 'dummy_hcd*' \) || true

- name: Build dummy_hcd for the generic kernel (Ubuntu ships no package)
run: |
set -eux
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
KMAJ=$(echo "${KVER}" | grep -oE '^[0-9]+\.[0-9]+')
mkdir -p dummyhcd
# Ubuntu packages no dummy_hcd; build it from the upstream source that
# matches the generic kernel's major version (xairy's copy tracks newer
# kernels and won't compile against an older one).
curl -fsSL -o dummyhcd/dummy_hcd.c \
"https://raw.githubusercontent.com/torvalds/linux/v${KMAJ}/drivers/usb/gadget/udc/dummy_hcd.c"
printf 'obj-m += dummy_hcd.o\n' > dummyhcd/Makefile
make -C "/lib/modules/${KVER}/build" M="${PWD}/dummyhcd" modules
sudo install -m 0644 "${PWD}/dummyhcd/dummy_hcd.ko" \
"/lib/modules/${KVER}/kernel/drivers/usb/gadget/udc/"
sudo depmod -a "${KVER}"

- name: Build HIDAPI + tests (libusb backend)
run: |
cmake -B build -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DHIDAPI_WITH_LIBUSB=ON -DHIDAPI_WITH_HIDRAW=OFF -DHIDAPI_WITH_TESTS=ON
cmake --build build

- name: Run DeviceIO_libusb inside a VM (generic kernel + raw_gadget)
run: |
set -eux
# The generic kernel just installed (has dummy_hcd + raw_gadget modules).
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
KIMG="/boot/vmlinuz-${KVER}"
echo "guest kernel: ${KVER} (${KIMG})"
test -e "${KIMG}"
# Ubuntu installs the kernel image as 0600 root:root, but vng reads it
# as the (non-root) runner user; make it readable.
sudo chmod a+r "${KIMG}"
# Make sure KVM is usable for acceleration (else vng falls back to TCG).
sudo chmod 666 /dev/kvm 2>/dev/null || true
# Boot that kernel in a VM (KVM if /dev/kvm is usable, else TCG). The
# guest runs as root with the host fs mounted; load the gadget modules
# and run the test against the host build tree. vng exit-code
# propagation varies, so derive pass/fail from a sentinel line.
# Boot the kernel in a VM and run the guest script via --exec (vng's
# native flag; passing a complex command after '--' gets mangled). The
# script loads dummy_hcd/raw_gadget and runs the test (see it for why).
vng -v --pwd -r "${KIMG}" --exec "bash hidapisrc/.github/vmrun-libusb.sh" 2>&1 | tee vng.log
echo "--- VM run result ---"
grep -q "VNG_CTEST_EXIT=0" vng.log
109 changes: 109 additions & 0 deletions .github/workflows/win-vhid-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Windows Virtual HID Device Test (manual)

# Builds, self-signs and installs a modified vhidmini2 UMDF2 driver on a hosted
# windows-latest runner, then runs the backend-agnostic device-I/O test against
# that real virtual HID device (winapi backend). It installs a kernel driver, so
# it is not part of the per-push CI matrix; run it on demand from the Actions
# tab, or by adding the 'ci-virtual-device' label to a pull request.

on:
workflow_dispatch:
# Also runs automatically on a pull request that carries the
# 'ci-virtual-device' label (the job below is gated on that label).
pull_request:
types: [opened, reopened, labeled, synchronize]

jobs:
win-vhid:
# workflow_dispatch always runs; on a PR, only when the label is present.
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device')
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

# The hosted runner ships the Windows SDK but not the WDK (no UMDF headers),
# so the WDK must be obtained. Cache a self-contained offline installer
# layout (the components, not just the bootstrapper) so it is downloaded only
# once; later runs restore it from the cache and install offline.
- name: Cache the WDK installer layout
id: wdk-cache
uses: actions/cache@v4
with:
path: wdk-layout
key: wdk-layout-26100.6584

- name: Download WDK installer layout (cache miss only)
if: steps.wdk-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2335869" -OutFile "$env:RUNNER_TEMP\wdksetup.exe"
# /layout downloads a complete offline copy into wdk-layout (cached).
Start-Process -FilePath "$env:RUNNER_TEMP\wdksetup.exe" -ArgumentList '/layout', "$PWD\wdk-layout", '/quiet', '/ceip', 'off' -Wait

- name: Build vhidmini2 (UMDF2)
shell: pwsh
run: |
$proj = "src\tests\windows\driver\VhidminiUm.vcxproj"
$inc = "C:\Program Files (x86)\Windows Kits\10\Include"
# Use a preinstalled WDK if a future image ships one (kit with UMDF
# headers); otherwise install from the cached offline layout.
$ver = Get-ChildItem $inc -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path (Join-Path $_.FullName 'wdf\umdf') } |
Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name
if ($ver) {
Write-Host "Using preinstalled WDK ($ver)."
} else {
Write-Host "Installing WDK from the cached offline layout ..."
Start-Process -FilePath "wdk-layout\wdksetup.exe" -ArgumentList '/quiet', '/norestart', '/ceip', 'off' -Wait
$ver = "10.0.26100.0"
}
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cmd /c "call `"$vcvars`" && msbuild $proj /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=$ver /v:minimal"
if ($LASTEXITCODE -ne 0) { throw "vhidmini2 build failed (exit $LASTEXITCODE)." }

- name: Trust the driver's test certificate
shell: pwsh
run: |
$cer = "src/tests/windows/driver/x64/Release/VhidminiUm.cer"
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher | Out-Null
Write-Host "Imported test cert into Root and TrustedPublisher."

- name: Install virtual HID device (devcon, root-enumerated)
shell: pwsh
run: |
$devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName
Write-Host "devcon: $devcon"
$inf = (Resolve-Path "src/tests/windows/driver/x64/Release/VhidminiUm/VhidminiUm.inf").Path
Write-Host "inf: $inf"
& $devcon install $inf "root\VhidminiUm"
Write-Host "devcon exit: $LASTEXITCODE"

- name: HID devices present (diagnostic)
shell: pwsh
run: |
Start-Sleep -Seconds 3
Get-PnpDevice -Class HIDClass -ErrorAction SilentlyContinue |
Select-Object Status, FriendlyName, InstanceId | Format-Table -AutoSize

- name: Build HIDAPI + tests
shell: pwsh
run: |
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DHIDAPI_WITH_TESTS=ON
cmake --build build --config Release

- name: Run device-I/O test against the virtual device
shell: pwsh
working-directory: build
run: |
ctest -C Release -R DeviceIO_winapi --output-on-failure

- name: Cleanup virtual device
if: always()
shell: pwsh
run: |
$devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName
if ($devcon) { & $devcon remove "root\VhidminiUm" 2>$null }
Write-Host "cleanup done"
10 changes: 9 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ if(HIDAPI_ENABLE_ASAN)
endif()

if(WIN32)
# so far only Windows has tests
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD})
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE)
# Linux and macOS have virtual-device based tests (uhid / raw-gadget /
# IOHIDUserDevice). Off by default: they need a virtual device at runtime
# and otherwise self-skip.
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF)
else()
set(HIDAPI_WITH_TESTS OFF)
endif()
Expand All @@ -92,6 +96,10 @@ if(HIDAPI_BUILD_HIDTEST)
add_subdirectory(hidtest)
endif()

if(HIDAPI_WITH_TESTS AND (WIN32 OR CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE))
add_subdirectory(src/tests)
endif()

if(HIDAPI_ENABLE_ASAN)
if(NOT MSVC)
# MSVC doesn't recognize those options, other compilers - requiring it
Expand Down
80 changes: 80 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Backend-generic HIDAPI (unit-)tests, run against a virtual HID device.
#
# The tests are written against the public HIDAPI API and the backend-agnostic
# test_virtual_device interface (test_virtual_device.h), so the same test runs
# against any backend for which a virtual-device provider exists. Each provider
# implements the pre-recorded "scenario" protocol: the test triggers a scenario
# by sending a Feature report, and the device replays a canned input report.
#
# Providers:
# - Linux / hidraw : test_virtual_device_uhid.c (kernel /dev/uhid)
# - Linux / libusb : test_virtual_device_rawgadget.c (/dev/raw-gadget + dummy_hcd)
# - Windows / winapi : test_virtual_device_win.c (modified vhidmini2 UMDF2 driver)
# - macOS / darwin : test_virtual_device_mac.c (IOHIDUserDevice)
#
# The libusb (raw-gadget), Windows and macOS virtual devices need privileged
# out-of-band setup (kernel modules / a signed driver / an entitlement) that is
# only performed by the dedicated CI jobs. Whenever the virtual device cannot be
# created or does not enumerate, the test returns CTest's SKIP code (77) instead
# of failing, so ordinary builds on any host stay green.

find_package(Threads REQUIRED)

# Define a device-I/O test <name> built from test_device_io.c + <provider>,
# linked against the HIDAPI <backend>, and registered with CTest (it self-skips
# via return code 77 when no virtual device is present).
function(hidapi_add_vdev_test name provider backend)
add_executable(${name} test_device_io.c ${provider})
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 60
)
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)
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)
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)
# 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
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
ENVIRONMENT_MODIFICATION "PATH=path_list_append:${MSVC_BUILD_TOOLS_DIR}")
endif()
endif()

# --- macOS: IOKit backend via IOHIDUserDevice ------------------------------
# Self-skips where the virtual-device entitlement / user consent isn't
# 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)
target_link_libraries(DeviceIO_darwin PRIVATE
"-framework IOKit" "-framework CoreFoundation")
endif()
Loading
Loading