Skip to content

Commit a0792ee

Browse files
committed
ci: build and run the virtual HID device tests
- builds.yml: build the test on Linux/Windows/macOS and run it where possible (ubuntu-cmake loads uhid and runs DeviceIO_hidraw; Windows/macOS/libusb build and self-skip), keeping the per-push matrix green. - win-vhid-test.yml: build, self-sign and install the vhidmini2 driver on a hosted windows-latest runner and run DeviceIO_winapi against it. - libusb-vhid-test.yml (+ .github/vmrun-libusb.sh): the hosted kernel has no USB gadget subsystem, so run DeviceIO_libusb inside a virtme-ng VM that boots a generic kernel (building dummy_hcd out-of-tree and loading raw_gadget) against a real virtual USB device. Both privileged jobs run only via workflow_dispatch or a 'ci-virtual-device' pull-request label, so they stay out of the per-push matrix. Assisted-by: Claude:claude-opus-4.8
1 parent d42ec7c commit a0792ee

4 files changed

Lines changed: 246 additions & 2 deletions

File tree

.github/vmrun-libusb.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/sh
2+
# Guest-side commands for the libusb virtual-device test, run inside a virtme-ng
3+
# VM (see .github/workflows/libusb-vhid-test.yml). Kept as a file so a complex
4+
# command line doesn't have to survive vng's argument parser.
5+
#
6+
# Runs as root in the guest, with the host filesystem mounted, cwd at the
7+
# workspace root (which contains the host-built 'build' tree).
8+
set -x
9+
10+
# The guest's modules.dep may be trimmed; regenerate it so dummy_hcd /
11+
# raw_gadget (and their dependencies) resolve from the overlaid /lib/modules.
12+
depmod -a || true
13+
modprobe dummy_hcd || true
14+
modprobe raw_gadget || true
15+
ls -l /dev/raw-gadget || true
16+
17+
ctest --test-dir build --output-on-failure
18+
rc=$?
19+
20+
echo "=== diag ==="
21+
lsmod | grep -E "raw_gadget|dummy_hcd|udc" || true
22+
ls -l /sys/bus/usb/devices/ 2>/dev/null || true
23+
dmesg | tail -40 || true
24+
25+
echo "VNG_CTEST_EXIT=${rc}"

.github/workflows/builds.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
- name: Configure CMake
4545
run: |
4646
rm -rf build install
47-
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}"
47+
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}"
4848
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}"
4949
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}"
5050
- name: Build CMake Shared
@@ -56,6 +56,14 @@ jobs:
5656
- name: Build CMake Framework
5757
working-directory: build/framework
5858
run: make install
59+
- name: Run virtual-device tests (IOHIDUserDevice self-skips on hosted CI)
60+
working-directory: build/shared
61+
run: |
62+
# The macOS virtual device needs the com.apple.developer.hid.virtual.device
63+
# entitlement and interactive user consent, neither available on a hosted
64+
# runner, so DeviceIO_darwin self-skips (CTest code 77). This still
65+
# verifies the provider builds and the test runs/links.
66+
ASAN_OPTIONS=detect_leaks=0 ctest --output-on-failure
5967
- name: Check artifacts
6068
uses: andstor/file-existence-action@v2
6169
with:
@@ -122,14 +130,27 @@ jobs:
122130
- name: Configure CMake
123131
run: |
124132
rm -rf build install
125-
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}"
133+
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}"
126134
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}"
127135
- name: Build CMake Shared
128136
working-directory: build/shared
129137
run: make install
130138
- name: Build CMake Static
131139
working-directory: build/static
132140
run: make install
141+
- name: Run virtual-device tests (uhid -> hidraw; raw-gadget self-skips)
142+
working-directory: build/shared
143+
run: |
144+
# uhid ships in the 'extra' modules package on the runner kernel.
145+
# Everything here is best-effort: if a virtual device can't be provided
146+
# the matching test self-skips (CTest code 77) instead of failing.
147+
sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true
148+
sudo modprobe uhid || true
149+
ls -l /dev/uhid || echo "/dev/uhid not present"
150+
# Run as root so the test can create /dev/uhid and open the resulting
151+
# /dev/hidrawN. LeakSanitizer off (udev keeps allocations alive at
152+
# exit); ASan use-after-free / overflow detection stays active.
153+
sudo env "ASAN_OPTIONS=detect_leaks=0" ctest --output-on-failure
133154
- name: Check artifacts
134155
uses: andstor/file-existence-action@v2
135156
with:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Linux libusb Virtual HID Device Test (manual)
2+
3+
# Runs the device-I/O test against the HIDAPI *libusb* backend using a real
4+
# virtual USB HID device (USB Raw Gadget on top of dummy_hcd).
5+
#
6+
# The hosted ubuntu-latest (azure) kernel is built without the USB gadget
7+
# subsystem, so raw_gadget/dummy_hcd can't be loaded (or even built) there. We
8+
# therefore run the test inside a lightweight VM (virtme-ng + QEMU) booting a
9+
# *generic* Ubuntu kernel, whose linux-modules-extra ships dummy_hcd and
10+
# raw_gadget. The VM shares the host filesystem, so it runs the binaries built
11+
# on the host. The same approach works locally and on WSL2 (which also lacks
12+
# those modules in its default kernel).
13+
#
14+
# Runs on demand (workflow_dispatch) or on a PR labelled 'ci-virtual-device'.
15+
16+
on:
17+
workflow_dispatch:
18+
pull_request:
19+
types: [opened, reopened, labeled, synchronize]
20+
21+
jobs:
22+
libusb-rawgadget:
23+
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device')
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
with:
28+
path: hidapisrc
29+
30+
- name: Install deps + a gadget-capable generic kernel + virtme-ng
31+
run: |
32+
set -eux
33+
sudo apt-get update
34+
sudo apt-get install -y \
35+
build-essential cmake libudev-dev libusb-1.0-0-dev \
36+
qemu-system-x86 virtme-ng linux-image-generic
37+
# raw_gadget ships in the generic kernel's modules-extra; install that
38+
# plus headers (needed to build dummy_hcd, which Ubuntu doesn't package).
39+
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
40+
echo "generic kernel: ${KVER}"
41+
sudo apt-get install -y "linux-modules-extra-${KVER}" "linux-headers-${KVER}"
42+
find "/lib/modules/${KVER}" \( -name 'raw_gadget*' -o -name 'dummy_hcd*' \) || true
43+
44+
- name: Build dummy_hcd for the generic kernel (Ubuntu ships no package)
45+
run: |
46+
set -eux
47+
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
48+
KMAJ=$(echo "${KVER}" | grep -oE '^[0-9]+\.[0-9]+')
49+
mkdir -p dummyhcd
50+
# Ubuntu packages no dummy_hcd; build it from the upstream source that
51+
# matches the generic kernel's major version (xairy's copy tracks newer
52+
# kernels and won't compile against an older one).
53+
curl -fsSL -o dummyhcd/dummy_hcd.c \
54+
"https://raw.githubusercontent.com/torvalds/linux/v${KMAJ}/drivers/usb/gadget/udc/dummy_hcd.c"
55+
printf 'obj-m += dummy_hcd.o\n' > dummyhcd/Makefile
56+
make -C "/lib/modules/${KVER}/build" M="${PWD}/dummyhcd" modules
57+
sudo install -m 0644 "${PWD}/dummyhcd/dummy_hcd.ko" \
58+
"/lib/modules/${KVER}/kernel/drivers/usb/gadget/udc/"
59+
sudo depmod -a "${KVER}"
60+
61+
- name: Build HIDAPI + tests (libusb backend)
62+
run: |
63+
cmake -B build -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo \
64+
-DHIDAPI_WITH_LIBUSB=ON -DHIDAPI_WITH_HIDRAW=OFF -DHIDAPI_WITH_TESTS=ON
65+
cmake --build build
66+
67+
- name: Run DeviceIO_libusb inside a VM (generic kernel + raw_gadget)
68+
run: |
69+
set -eux
70+
# The generic kernel just installed (has dummy_hcd + raw_gadget modules).
71+
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
72+
KIMG="/boot/vmlinuz-${KVER}"
73+
echo "guest kernel: ${KVER} (${KIMG})"
74+
test -e "${KIMG}"
75+
# Ubuntu installs the kernel image as 0600 root:root, but vng reads it
76+
# as the (non-root) runner user; make it readable.
77+
sudo chmod a+r "${KIMG}"
78+
# Make sure KVM is usable for acceleration (else vng falls back to TCG).
79+
sudo chmod 666 /dev/kvm 2>/dev/null || true
80+
# Boot that kernel in a VM (KVM if /dev/kvm is usable, else TCG). The
81+
# guest runs as root with the host fs mounted; load the gadget modules
82+
# and run the test against the host build tree. vng exit-code
83+
# propagation varies, so derive pass/fail from a sentinel line.
84+
# Boot the kernel in a VM and run the guest script via --exec (vng's
85+
# native flag; passing a complex command after '--' gets mangled). The
86+
# script loads dummy_hcd/raw_gadget and runs the test (see it for why).
87+
vng -v --pwd -r "${KIMG}" --exec "bash hidapisrc/.github/vmrun-libusb.sh" 2>&1 | tee vng.log
88+
echo "--- VM run result ---"
89+
grep -q "VNG_CTEST_EXIT=0" vng.log
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
name: Windows Virtual HID Device Test (manual)
2+
3+
# Builds, self-signs and installs a modified vhidmini2 UMDF2 driver on a hosted
4+
# windows-latest runner, then runs the backend-agnostic device-I/O test against
5+
# that real virtual HID device (winapi backend). It installs a kernel driver, so
6+
# it is not part of the per-push CI matrix; run it on demand from the Actions
7+
# tab, or by adding the 'ci-virtual-device' label to a pull request.
8+
9+
on:
10+
workflow_dispatch:
11+
# Also runs automatically on a pull request that carries the
12+
# 'ci-virtual-device' label (the job below is gated on that label).
13+
pull_request:
14+
types: [opened, reopened, labeled, synchronize]
15+
16+
jobs:
17+
win-vhid:
18+
# workflow_dispatch always runs; on a PR, only when the label is present.
19+
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device')
20+
runs-on: windows-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
# The hosted runner ships the Windows SDK but not the WDK (no UMDF headers),
25+
# so the WDK must be obtained. Cache a self-contained offline installer
26+
# layout (the components, not just the bootstrapper) so it is downloaded only
27+
# once; later runs restore it from the cache and install offline.
28+
- name: Cache the WDK installer layout
29+
id: wdk-cache
30+
uses: actions/cache@v4
31+
with:
32+
path: wdk-layout
33+
key: wdk-layout-26100.6584
34+
35+
- name: Download WDK installer layout (cache miss only)
36+
if: steps.wdk-cache.outputs.cache-hit != 'true'
37+
shell: pwsh
38+
run: |
39+
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2335869" -OutFile "$env:RUNNER_TEMP\wdksetup.exe"
40+
# /layout downloads a complete offline copy into wdk-layout (cached).
41+
Start-Process -FilePath "$env:RUNNER_TEMP\wdksetup.exe" -ArgumentList '/layout', "$PWD\wdk-layout", '/quiet', '/ceip', 'off' -Wait
42+
43+
- name: Build vhidmini2 (UMDF2)
44+
shell: pwsh
45+
run: |
46+
$proj = "src\tests\windows\driver\VhidminiUm.vcxproj"
47+
$inc = "C:\Program Files (x86)\Windows Kits\10\Include"
48+
# Use a preinstalled WDK if a future image ships one (kit with UMDF
49+
# headers); otherwise install from the cached offline layout.
50+
$ver = Get-ChildItem $inc -Directory -ErrorAction SilentlyContinue |
51+
Where-Object { Test-Path (Join-Path $_.FullName 'wdf\umdf') } |
52+
Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name
53+
if ($ver) {
54+
Write-Host "Using preinstalled WDK ($ver)."
55+
} else {
56+
Write-Host "Installing WDK from the cached offline layout ..."
57+
Start-Process -FilePath "wdk-layout\wdksetup.exe" -ArgumentList '/quiet', '/norestart', '/ceip', 'off' -Wait
58+
$ver = "10.0.26100.0"
59+
}
60+
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
61+
cmd /c "call `"$vcvars`" && msbuild $proj /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=$ver /v:minimal"
62+
if ($LASTEXITCODE -ne 0) { throw "vhidmini2 build failed (exit $LASTEXITCODE)." }
63+
64+
- name: Trust the driver's test certificate
65+
shell: pwsh
66+
run: |
67+
$cer = "src/tests/windows/driver/x64/Release/VhidminiUm.cer"
68+
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
69+
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher | Out-Null
70+
Write-Host "Imported test cert into Root and TrustedPublisher."
71+
72+
- name: Install virtual HID device (devcon, root-enumerated)
73+
shell: pwsh
74+
run: |
75+
$devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue |
76+
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName
77+
Write-Host "devcon: $devcon"
78+
$inf = (Resolve-Path "src/tests/windows/driver/x64/Release/VhidminiUm/VhidminiUm.inf").Path
79+
Write-Host "inf: $inf"
80+
& $devcon install $inf "root\VhidminiUm"
81+
Write-Host "devcon exit: $LASTEXITCODE"
82+
83+
- name: HID devices present (diagnostic)
84+
shell: pwsh
85+
run: |
86+
Start-Sleep -Seconds 3
87+
Get-PnpDevice -Class HIDClass -ErrorAction SilentlyContinue |
88+
Select-Object Status, FriendlyName, InstanceId | Format-Table -AutoSize
89+
90+
- name: Build HIDAPI + tests
91+
shell: pwsh
92+
run: |
93+
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DHIDAPI_WITH_TESTS=ON
94+
cmake --build build --config Release
95+
96+
- name: Run device-I/O test against the virtual device
97+
shell: pwsh
98+
working-directory: build
99+
run: |
100+
ctest -C Release -R DeviceIO_winapi --output-on-failure
101+
102+
- name: Cleanup virtual device
103+
if: always()
104+
shell: pwsh
105+
run: |
106+
$devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue |
107+
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName
108+
if ($devcon) { & $devcon remove "root\VhidminiUm" 2>$null }
109+
Write-Host "cleanup done"

0 commit comments

Comments
 (0)