Skip to content
Merged
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
156 changes: 156 additions & 0 deletions .github/workflows/win-vhid-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Windows Virtual HID Device Test (manual)

# Builds and self-signs a modified vhidmini2 UMDF2 driver, installs it on a
# hosted runner, then runs the backend-agnostic device-I/O test against that
# real virtual HID device (winapi backend). It installs a 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.
#
# The work is split across two runners because no single hosted image can do
# both today:
# - build-driver runs on windows-2022: the cached WDK (10.0.26100.6584)
# ships DriverKit build tasks only up to MSBuild 17 (VS2022), which the
# windows-2025* images no longer carry (VS2026 / MSBuild 18 -> MSB4062).
# - win-vhid runs on windows-latest (Server 2025): the driver package's INF
# depends on MsHidUmdf.inf semantics introduced in build 22000, so it does
# not install on Server 2022 (build 20348).
# Revisit once a WDK with 18.0 build tasks is cached here - then both jobs can
# run on windows-latest again (and could be merged back into one).

on:
workflow_dispatch:
# Also runs automatically on a pull request that carries the
# 'ci-virtual-device' label (the build-driver job is gated on that label;
# win-vhid follows via needs:).
pull_request:
types: [opened, reopened, labeled, synchronize]

jobs:
build-driver:
# 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-2022
steps:
- uses: actions/checkout@v4

# Sets up the MSVC developer environment (cl/msbuild on PATH) by locating
# the installed Visual Studio (no hard-coded path or edition).
- name: Set up MSVC developer environment
uses: TheMrMilchmann/setup-msvc-dev@v4
with:
arch: x64

# 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"
}
# msbuild comes from the MSVC developer environment set up above.
msbuild $proj /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=$ver /v:minimal
if ($LASTEXITCODE -ne 0) { throw "vhidmini2 build failed (exit $LASTEXITCODE)." }

- name: Stage driver package (+ devcon) for the test job
shell: pwsh
run: |
$out = "src/tests/windows/driver/x64/Release"
# devcon.exe ships with the WDK just installed; the test runner has no
# WDK, so carry the tool along with the driver package.
$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 (-not $devcon) { throw "devcon.exe not found in the installed WDK." }
Copy-Item $devcon $out\
Get-ChildItem -Recurse $out | Select-Object FullName

- uses: actions/upload-artifact@v4
with:
name: vhidmini-driver
path: src/tests/windows/driver/x64/Release/
if-no-files-found: error

win-vhid:
needs: build-driver
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: vhidmini-driver
path: driver-pkg

- name: Trust the driver's test certificate
shell: pwsh
run: |
$cer = "driver-pkg/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 = (Resolve-Path "driver-pkg/devcon.exe").Path
$inf = (Resolve-Path "driver-pkg/VhidminiUm/VhidminiUm.inf").Path
Write-Host "devcon: $devcon"
Write-Host "inf: $inf"
& $devcon install $inf "root\VhidminiUm"
Write-Host "devcon exit: $LASTEXITCODE"
if ($LASTEXITCODE -ne 0) { throw "devcon install failed (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 = "driver-pkg/devcon.exe"
if (Test-Path $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
Loading
Loading