Skip to content

Commit 1cdc8d5

Browse files
committed
tests: add a self-skipping FreeBSD virtual-device provider + CI
FreeBSD has no userspace facility to create a virtual HID/USB device (no dummy_hcd/raw-gadget analogue; cuse(3) can't make a libusb-visible device; usb_template(4) needs a hardware UDC; the planned usrhid(4) isn't in-tree), so a runnable virtual-HID test isn't possible on FreeBSD today. Add test_virtual_device_freebsd.c, which self-skips (CTest code 77) like the macOS and raw-gadget providers do when their device is unavailable, so the device-I/O test still builds and runs (skips) on FreeBSD. It is the drop-in point for a real implementation once a mechanism exists. - src/tests/CMakeLists.txt: FreeBSD libusb arm. - CMakeLists.txt: offer HIDAPI_WITH_TESTS and build src/tests on FreeBSD. - .github/workflows/bsd-vhid-test.yml: manual / 'ci-virtual-device'-labelled job that builds + runs the test on a real FreeBSD VM (vmactions). - src/tests/README.md: document FreeBSD (self-skip) and why NetBSD/OpenBSD have no provider. Assisted-by: Claude:claude-opus-4.8
1 parent a1833a1 commit 1cdc8d5

5 files changed

Lines changed: 201 additions & 5 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: FreeBSD Virtual HID Device Test (manual)
2+
3+
# Builds HIDAPI (libusb backend) + the device-I/O test on a real FreeBSD VM and
4+
# runs ctest. FreeBSD has no userspace facility to create a virtual HID device
5+
# (see src/tests/test_virtual_device_freebsd.c), so DeviceIO_libusb self-skips
6+
# (CTest code 77) - this job verifies the harness and the libusb backend build
7+
# and run on FreeBSD, and is the place an end-to-end test would live once a
8+
# mechanism (a real USB Device Controller, or usrhid(4)) is available.
9+
#
10+
# Runs on demand (workflow_dispatch) or on a PR labelled 'ci-virtual-device'.
11+
# The FreeBSD VM runs under QEMU on the Linux runner, so this is slow; hence it
12+
# is kept out of the per-push matrix.
13+
14+
on:
15+
workflow_dispatch:
16+
pull_request:
17+
types: [opened, reopened, labeled, synchronize]
18+
19+
jobs:
20+
freebsd-vhid:
21+
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device')
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- name: Build + run device-I/O test on FreeBSD
26+
uses: vmactions/freebsd-vm@v1
27+
with:
28+
usesh: true
29+
prepare: |
30+
pkg install -y cmake ninja libiconv pkgconf
31+
run: |
32+
cmake -GNinja -B build -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON
33+
cmake --build build
34+
cd build
35+
ctest --output-on-failure

CMakeLists.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ endif()
7272

7373
if(WIN32)
7474
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD})
75-
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE)
76-
# Linux and macOS have virtual-device based tests (uhid / raw-gadget /
77-
# IOHIDUserDevice). Off by default: they need a virtual device at runtime
78-
# and otherwise self-skip.
75+
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE OR CMAKE_SYSTEM_NAME MATCHES "FreeBSD")
76+
# Linux/macOS/FreeBSD have virtual-device based tests (uhid / raw-gadget /
77+
# IOHIDUserDevice; FreeBSD self-skips - no userspace HID-create facility).
78+
# Off by default: they need a virtual device at runtime and otherwise skip.
7979
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF)
8080
else()
8181
set(HIDAPI_WITH_TESTS OFF)
@@ -96,7 +96,7 @@ if(HIDAPI_BUILD_HIDTEST)
9696
add_subdirectory(hidtest)
9797
endif()
9898

99-
if(HIDAPI_WITH_TESTS AND (WIN32 OR CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE))
99+
if(HIDAPI_WITH_TESTS AND (WIN32 OR CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE OR CMAKE_SYSTEM_NAME MATCHES "FreeBSD"))
100100
add_subdirectory(src/tests)
101101
endif()
102102

src/tests/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_libusb)
5252
hidapi_add_vdev_test(DeviceIO_libusb test_virtual_device_rawgadget.c hidapi_libusb)
5353
endif()
5454

55+
# --- FreeBSD: libusb backend ------------------------------------------------
56+
# FreeBSD has no userspace facility to create a virtual HID/USB device (no
57+
# dummy_hcd/raw-gadget analogue; the planned usrhid(4) is not yet in-tree), so
58+
# this provider self-skips. It keeps the test building on FreeBSD and is the
59+
# drop-in point for a real implementation (see test_virtual_device_freebsd.c).
60+
if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD" AND TARGET hidapi_libusb)
61+
hidapi_add_vdev_test(DeviceIO_libusb test_virtual_device_freebsd.c hidapi_libusb)
62+
endif()
63+
5564
# --- Windows: winapi backend via a modified vhidmini2 UMDF driver -----------
5665
if(WIN32 AND TARGET hidapi_winapi)
5766
hidapi_add_vdev_test(DeviceIO_winapi test_virtual_device_win.c hidapi_winapi)

src/tests/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ command bytes, expected payloads).
3131
| Linux / libusb | `test_virtual_device_rawgadget.c` | `/dev/raw-gadget` + `dummy_hcd` | builds in `builds.yml`; runs in the manual `libusb-vhid-test` job |
3232
| Windows / winapi | `test_virtual_device_win.c` + `windows/driver/` | modified vhidmini2 UMDF2 driver | builds in `builds.yml`; runs in the manual `win-vhid-test` job |
3333
| macOS / darwin | `test_virtual_device_mac.c` | `IOHIDUserDevice` (IOKit) | builds + runs in `builds.yml` (macos-cmake) |
34+
| FreeBSD / libusb | `test_virtual_device_freebsd.c` | none yet (self-skips) | builds + runs (skips) in the manual `bsd-vhid-test` job |
3435

3536
Whenever a virtual device cannot be created or does not enumerate, the test
3637
returns CTest's **skip** code (77) instead of failing, so ordinary builds on any
@@ -54,6 +55,13 @@ for the per-push CI matrix:
5455
Accessibility (TCC) consent prompt, neither available on a hosted runner, so
5556
the provider self-skips there. It is meant to actually run on a developer
5657
machine or a self-hosted runner where consent has been granted.
58+
* **FreeBSD / other BSDs** — no BSD currently provides a userspace way to
59+
*create* a HID device: FreeBSD's `cuse(3)` can't produce a libusb-visible USB
60+
device, `usb_template(4)` needs a hardware USB Device Controller (no
61+
`dummy_hcd` analogue), and the planned `usrhid(4)` is not yet in-tree;
62+
NetBSD/OpenBSD `uhid` are consumer-only. So the FreeBSD provider always
63+
self-skips today and exists as the drop-in point for a future mechanism (a
64+
real Device Controller, or `usrhid`). NetBSD and OpenBSD have no provider yet.
5765

5866
## Running locally
5967

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*******************************************************
2+
HIDAPI - Multi-Platform library for
3+
communication with HID devices.
4+
5+
FreeBSD implementation of the virtual HID device test interface.
6+
7+
The contents of this file may be used by anyone for any
8+
reason without any conditions and may be used as a
9+
starting point for your own applications which use HIDAPI.
10+
********************************************************/
11+
12+
/*
13+
* STATUS: self-skipping.
14+
*
15+
* Unlike Linux, FreeBSD currently has no userspace facility to create a HID
16+
* device that the HIDAPI backends would enumerate, so this provider returns
17+
* TEST_VDEV_UNAVAILABLE and the device-I/O test is reported as skipped (CTest
18+
* code 77) rather than failed - mirroring how the macOS and Linux raw-gadget
19+
* providers self-skip when their device can't be created.
20+
*
21+
* Why there is no virtual device today:
22+
*
23+
* - HIDAPI on FreeBSD uses the libusb backend, and FreeBSD's libusb
24+
* (libusb20 / ugen20) only enumerates *real* kernel-backed USB devices: it
25+
* lists /dev/usb via USB_READ_DIR and validates each /dev/ugenX.Y with
26+
* USB_GET_PLUGTIME / USB_GET_DEVICEINFO. A character device synthesized
27+
* with cuse(3) has no kernel usb_device behind it, so those ioctls fail and
28+
* it is invisible to libusb - cuse cannot fake a libusb-visible USB device.
29+
*
30+
* - FreeBSD's device-side USB stack (usb_template(4), "USB device mode") can
31+
* present a real virtual USB device, but it needs a hardware USB Device
32+
* Controller (an OTG/client port on an embedded board); there is no
33+
* software/dummy UDC equivalent to Linux's dummy_hcd on FreeBSD, so it is
34+
* unusable on amd64 servers / CI VMs.
35+
*
36+
* - The FreeBSD 13+ hid/hidraw stack (hidbus, usbhid, iichid, hidraw) is
37+
* consumer-side only; the planned userspace HID *creator* "usrhid(4)" (the
38+
* analogue of Linux /dev/uhid) is not yet in-tree.
39+
*
40+
* This file is the drop-in point for a real implementation once a mechanism is
41+
* available:
42+
* (a) On a self-hosted FreeBSD host with a USB Device Controller: implement
43+
* create()/destroy() via usb_template(4) / libusbgx, mirroring
44+
* test_virtual_device_rawgadget.c (same vendor-defined report descriptor;
45+
* on a SET_REPORT whose first byte is TEST_VDEV_CMD_EMIT_A/B, push the
46+
* matching canned report on the interrupt IN endpoint).
47+
* (b) If/when usrhid(4) lands: port test_virtual_device_uhid.c to its ioctl
48+
* protocol (intended to follow the Linux uhid protocol).
49+
* open_hidapi()/trigger()/destroy() below already work unchanged.
50+
*/
51+
52+
#include "test_virtual_device.h"
53+
54+
#include <stdlib.h>
55+
#include <string.h>
56+
#include <time.h>
57+
#include <wchar.h>
58+
59+
struct test_virtual_device {
60+
unsigned short vendor_id;
61+
unsigned short product_id;
62+
char serial[64];
63+
};
64+
65+
int test_virtual_device_create(test_virtual_device **out_dev,
66+
unsigned short vendor_id,
67+
unsigned short product_id,
68+
const char *serial)
69+
{
70+
(void)vendor_id;
71+
(void)product_id;
72+
(void)serial;
73+
74+
if (out_dev)
75+
*out_dev = NULL;
76+
77+
/* No userspace virtual-HID/USB-device mechanism on FreeBSD (see top of
78+
file): report "unavailable" so the test self-skips instead of failing.
79+
A real implementation would allocate the struct, bring up the device,
80+
store the ids/serial, set *out_dev and return TEST_VDEV_OK. */
81+
return TEST_VDEV_UNAVAILABLE;
82+
}
83+
84+
hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms)
85+
{
86+
/* Backend-agnostic open-by-enumeration (same as every other provider);
87+
reachable only once create() actually produces a device. */
88+
wchar_t wserial[64];
89+
int waited = 0;
90+
size_t i;
91+
92+
if (!dev)
93+
return NULL;
94+
95+
for (i = 0; i + 1 < (sizeof(wserial) / sizeof(wserial[0])) && dev->serial[i]; i++)
96+
wserial[i] = (wchar_t)(unsigned char)dev->serial[i];
97+
wserial[i] = L'\0';
98+
99+
for (;;) {
100+
struct hid_device_info *infos = hid_enumerate(dev->vendor_id, dev->product_id);
101+
struct hid_device_info *cur;
102+
struct hid_device_info *first = NULL;
103+
struct hid_device_info *match = NULL;
104+
hid_device *h = NULL;
105+
106+
for (cur = infos; cur; cur = cur->next) {
107+
if (!first)
108+
first = cur;
109+
if (cur->serial_number && wcscmp(cur->serial_number, wserial) == 0) {
110+
match = cur;
111+
break;
112+
}
113+
}
114+
if (match || first)
115+
h = hid_open_path((match ? match : first)->path);
116+
hid_free_enumeration(infos);
117+
if (h)
118+
return h;
119+
120+
if (waited >= timeout_ms)
121+
return NULL;
122+
{
123+
struct timespec ts = { 0, 50 * 1000000L };
124+
nanosleep(&ts, NULL);
125+
}
126+
waited += 50;
127+
}
128+
}
129+
130+
int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle,
131+
unsigned char command)
132+
{
133+
unsigned char feature[1 + TEST_VDEV_REPORT_SIZE];
134+
(void)dev;
135+
memset(feature, 0, sizeof(feature));
136+
feature[0] = 0x00; /* Report ID (the device has no numbered reports) */
137+
feature[1] = command; /* scenario command, first byte of the payload */
138+
return hid_send_feature_report(handle, feature, sizeof(feature));
139+
}
140+
141+
void test_virtual_device_destroy(test_virtual_device *dev)
142+
{
143+
free(dev);
144+
}

0 commit comments

Comments
 (0)