Skip to content

Commit 149f49a

Browse files
Add SDL DualSense tests and helpers
Add CI/Codecov/Stars badges to README and update wording to mention generic and DualSense gamepads. In tests, add several standard includes and introduce SDL_GameController RAII alias and a SdlGamepadConsumerCase struct. Add helper functions (configure_sdl_hidapi_hints, create_sdl_gamepad, wait_for_sdl_controller_input, expect_sdl_joystick_profile) and two reusable test runners (run_sdl_uhid_joystick_test, run_sdl_dualsense_controller_test). Refactor the existing SDL UHID gamepad test to use the new runner and add dedicated DualSense USB and Bluetooth tests, plus no-op stubs for non-Linux builds.
1 parent 0c068c5 commit 149f49a

3 files changed

Lines changed: 196 additions & 33 deletions

File tree

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# libvirtualhid
22

3+
[![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/libvirtualhid/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/libvirtualhid/actions/workflows/ci.yml?query=branch%3Amaster)
4+
[![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/libvirtualhid?token=2MeMpktxpv&style=for-the-badge&logo=codecov&label=codecov)](https://codecov.io/gh/LizardByte/libvirtualhid)
5+
[![GitHub stars](https://img.shields.io/github/stars/lizardbyte/libvirtualhid.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/libvirtualhid)
6+
37
`libvirtualhid` is a planned cross-platform C++ library for creating virtual HID
48
input devices for remote streaming hosts and similar low-latency input
59
applications.
@@ -345,7 +349,7 @@ third-party/googletest/ GoogleTest submodule
345349
- [x] Support output report callbacks for rumble and profile-specific feedback.
346350
- [x] Add X11/XTest fallback support for keyboard and mouse only.
347351
- [x] Add examples and integration tests that validate virtual device visibility
348-
through SDL2 for gamepads and libinput for keyboard/mouse.
352+
through SDL2 for generic and DualSense gamepads and libinput for keyboard/mouse.
349353
- [x] Document required Linux permissions and sample udev rules.
350354

351355
### Phase 2B: Linux inputtino Parity
@@ -371,9 +375,9 @@ third-party/googletest/ GoogleTest submodule
371375
- [x] Keep gamepad feedback on UHID output reports. There is no uinput-backed
372376
gamepad path in this library; if one is added later, it must implement Linux
373377
force-feedback upload, erase, playback, and gain handling.
374-
- [x] Expand Linux consumer tests so SDL2 validates controller-specific behavior
375-
and libinput validates keyboard, mouse, touchscreen, trackpad, and pen tablet
376-
events.
378+
- [x] Expand Linux consumer tests so SDL2 validates generic joystick input and
379+
DualSense USB/Bluetooth controller-specific behavior, and libinput validates
380+
keyboard, mouse, touchscreen, trackpad, and pen tablet events.
377381

378382
### Phase 2C: Linux uinput Hardening
379383

tests/fixtures/linux_backend_test_hooks.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@
55

66
// standard includes
77
#include <algorithm>
8+
#include <array>
89
#include <atomic>
910
#include <cerrno>
1011
#include <chrono>
12+
#include <cmath>
1113
#include <cstddef>
14+
#include <cstdint>
1215
#include <cstring>
16+
#include <filesystem>
17+
#include <fstream>
18+
#include <iomanip>
19+
#include <map>
20+
#include <memory>
21+
#include <mutex>
22+
#include <numbers>
23+
#include <optional>
24+
#include <set>
25+
#include <sstream>
1326
#include <string>
27+
#include <system_error>
1428
#include <thread>
1529
#include <utility>
1630
#include <vector>

tests/unit/test_linux_consumers.cpp

Lines changed: 174 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,20 @@ namespace {
5151
#if defined(__linux__)
5252
using LibinputContext = std::unique_ptr<libinput, void (*)(libinput *)>;
5353
using LibinputEvent = std::unique_ptr<libinput_event, void (*)(libinput_event *)>;
54+
using SdlGameController = std::unique_ptr<SDL_GameController, void (*)(SDL_GameController *)>;
5455
using SdlJoystick = std::unique_ptr<SDL_Joystick, void (*)(SDL_Joystick *)>;
5556

57+
/**
58+
* @brief SDL-visible gamepad case.
59+
*/
60+
struct SdlGamepadConsumerCase {
61+
lvh::DeviceProfile profile;
62+
std::string_view name_suffix;
63+
std::string_view stable_id;
64+
int minimum_buttons = 1;
65+
int minimum_axes = 2;
66+
};
67+
5668
/**
5769
* @brief Execute cleanup code when a scope exits.
5870
*/
@@ -229,6 +241,139 @@ namespace {
229241
return false;
230242
}
231243

244+
bool wait_for_sdl_controller_input(SDL_GameController *controller) {
245+
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {3};
246+
247+
while (std::chrono::steady_clock::now() < deadline) {
248+
SDL_GameControllerUpdate();
249+
pump_sdl_events();
250+
251+
const auto button_pressed = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A) != 0;
252+
const auto left_x = std::abs(static_cast<int>(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX)));
253+
const auto left_y = std::abs(static_cast<int>(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY)));
254+
255+
if (button_pressed && (left_x > 8000 || left_y > 8000)) {
256+
return true;
257+
}
258+
259+
std::this_thread::sleep_for(std::chrono::milliseconds {50});
260+
}
261+
262+
return false;
263+
}
264+
265+
void configure_sdl_hidapi_hints() {
266+
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
267+
SDL_SetHint("SDL_JOYSTICK_HIDAPI", "1");
268+
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5", "1");
269+
}
270+
271+
lvh::GamepadCreationResult create_sdl_gamepad(lvh::Runtime &runtime, SdlGamepadConsumerCase test_case) {
272+
lvh::CreateGamepadOptions options;
273+
options.profile = std::move(test_case.profile);
274+
options.profile.name = unique_device_name(test_case.name_suffix);
275+
options.metadata.stable_id = std::string {test_case.stable_id};
276+
277+
return runtime.create_gamepad(options);
278+
}
279+
280+
void expect_sdl_joystick_profile(SDL_Joystick *joystick, const lvh::DeviceProfile &profile, int minimum_buttons, int minimum_axes) {
281+
EXPECT_EQ(SDL_JoystickGetVendor(joystick), profile.vendor_id);
282+
EXPECT_EQ(SDL_JoystickGetProduct(joystick), profile.product_id);
283+
EXPECT_GE(SDL_JoystickNumButtons(joystick), minimum_buttons);
284+
EXPECT_GE(SDL_JoystickNumAxes(joystick), minimum_axes);
285+
}
286+
287+
void run_sdl_uhid_joystick_test(SdlGamepadConsumerCase test_case) {
288+
configure_sdl_hidapi_hints();
289+
ASSERT_EQ(SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_EVENTS), 0) << SDL_GetError();
290+
ScopeExit sdl_quit {[]() {
291+
SDL_Quit();
292+
}};
293+
294+
lvh::RuntimeOptions runtime_options;
295+
runtime_options.backend = lvh::BackendKind::platform_default;
296+
auto runtime = lvh::Runtime::create(runtime_options);
297+
ASSERT_TRUE(runtime->capabilities().supports_gamepad);
298+
299+
const auto expected_profile = [&test_case]() {
300+
auto profile = test_case.profile;
301+
profile.name = unique_device_name(test_case.name_suffix);
302+
return profile;
303+
}();
304+
305+
auto created = create_sdl_gamepad(*runtime, test_case);
306+
ASSERT_TRUE(created) << created.status.message();
307+
308+
const auto joystick_index = wait_for_sdl_joystick(expected_profile);
309+
ASSERT_GE(joystick_index, 0);
310+
311+
SdlJoystick joystick {SDL_JoystickOpen(joystick_index), SDL_JoystickClose};
312+
ASSERT_NE(joystick.get(), nullptr) << SDL_GetError();
313+
expect_sdl_joystick_profile(
314+
joystick.get(),
315+
expected_profile,
316+
test_case.minimum_buttons,
317+
test_case.minimum_axes
318+
);
319+
320+
lvh::GamepadState state;
321+
state.buttons.set(lvh::GamepadButton::a);
322+
state.left_stick = {0.75F, -0.5F};
323+
ASSERT_TRUE(created.gamepad->submit(state).ok());
324+
325+
EXPECT_TRUE(wait_for_sdl_gamepad_input(joystick.get())) << describe_sdl_state(joystick.get());
326+
}
327+
328+
void run_sdl_dualsense_controller_test(SdlGamepadConsumerCase test_case) {
329+
configure_sdl_hidapi_hints();
330+
ASSERT_EQ(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS), 0) << SDL_GetError();
331+
ScopeExit sdl_quit {[]() {
332+
SDL_Quit();
333+
}};
334+
335+
lvh::RuntimeOptions runtime_options;
336+
runtime_options.backend = lvh::BackendKind::platform_default;
337+
auto runtime = lvh::Runtime::create(runtime_options);
338+
ASSERT_TRUE(runtime->capabilities().supports_gamepad);
339+
340+
const auto expected_profile = [&test_case]() {
341+
auto profile = test_case.profile;
342+
profile.name = unique_device_name(test_case.name_suffix);
343+
return profile;
344+
}();
345+
346+
auto created = create_sdl_gamepad(*runtime, test_case);
347+
ASSERT_TRUE(created) << created.status.message();
348+
349+
const auto joystick_index = wait_for_sdl_joystick(expected_profile);
350+
ASSERT_GE(joystick_index, 0);
351+
ASSERT_EQ(SDL_IsGameController(joystick_index), SDL_TRUE) << SDL_GetError();
352+
353+
SdlGameController controller {SDL_GameControllerOpen(joystick_index), SDL_GameControllerClose};
354+
ASSERT_NE(controller.get(), nullptr) << SDL_GetError();
355+
356+
auto *joystick = SDL_GameControllerGetJoystick(controller.get());
357+
ASSERT_NE(joystick, nullptr) << SDL_GetError();
358+
expect_sdl_joystick_profile(
359+
joystick,
360+
expected_profile,
361+
test_case.minimum_buttons,
362+
test_case.minimum_axes
363+
);
364+
365+
lvh::GamepadState state;
366+
state.buttons.set(lvh::GamepadButton::a);
367+
state.left_stick = {0.75F, -0.5F};
368+
state.acceleration = lvh::Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F};
369+
state.gyroscope = lvh::Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F};
370+
state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 80};
371+
state.touchpad_contacts[0] = {.id = 1, .active = true, .x = 0.5F, .y = 0.25F};
372+
ASSERT_TRUE(created.gamepad->submit(state).ok());
373+
374+
EXPECT_TRUE(wait_for_sdl_controller_input(controller.get())) << describe_sdl_state(joystick);
375+
}
376+
232377
void destroy_libinput_event(libinput_event *event) {
233378
if (event != nullptr) {
234379
libinput_event_destroy(event);
@@ -306,39 +451,35 @@ namespace {
306451
TEST_F(LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {
307452
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid"));
308453

309-
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
310-
ASSERT_EQ(SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_EVENTS), 0) << SDL_GetError();
311-
ScopeExit sdl_quit {[]() {
312-
SDL_Quit();
313-
}};
314-
315-
lvh::RuntimeOptions runtime_options;
316-
runtime_options.backend = lvh::BackendKind::platform_default;
317-
auto runtime = lvh::Runtime::create(runtime_options);
318-
ASSERT_TRUE(runtime->capabilities().supports_gamepad);
319-
320-
lvh::CreateGamepadOptions options;
321-
options.profile = lvh::profiles::generic_gamepad();
322-
options.profile.name = unique_device_name("SDL Gamepad");
323-
options.metadata.stable_id = "libvirtualhid-sdl-gamepad-test";
324-
325-
auto created = runtime->create_gamepad(options);
326-
ASSERT_TRUE(created) << created.status.message();
454+
run_sdl_uhid_joystick_test({
455+
.profile = lvh::profiles::generic_gamepad(),
456+
.name_suffix = "SDL Gamepad",
457+
.stable_id = "libvirtualhid-sdl-gamepad-test",
458+
});
459+
}
327460

328-
const auto joystick_index = wait_for_sdl_joystick(options.profile);
329-
ASSERT_GE(joystick_index, 0);
461+
TEST_F(LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {
462+
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid"));
330463

331-
SdlJoystick joystick {SDL_JoystickOpen(joystick_index), SDL_JoystickClose};
332-
ASSERT_NE(joystick.get(), nullptr) << SDL_GetError();
333-
EXPECT_GE(SDL_JoystickNumButtons(joystick.get()), 1);
334-
EXPECT_GE(SDL_JoystickNumAxes(joystick.get()), 2);
464+
run_sdl_dualsense_controller_test({
465+
.profile = lvh::profiles::dualsense_usb(),
466+
.name_suffix = "SDL DualSense USB",
467+
.stable_id = "02:00:00:00:00:01",
468+
.minimum_buttons = 10,
469+
.minimum_axes = 4,
470+
});
471+
}
335472

336-
lvh::GamepadState state;
337-
state.buttons.set(lvh::GamepadButton::a);
338-
state.left_stick = {0.75F, -0.5F};
339-
ASSERT_TRUE(created.gamepad->submit(state).ok());
473+
TEST_F(LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerBehavior) {
474+
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid"));
340475

341-
EXPECT_TRUE(wait_for_sdl_gamepad_input(joystick.get())) << describe_sdl_state(joystick.get());
476+
run_sdl_dualsense_controller_test({
477+
.profile = lvh::profiles::dualsense_bluetooth(),
478+
.name_suffix = "SDL DualSense Bluetooth",
479+
.stable_id = "02:00:00:00:00:02",
480+
.minimum_buttons = 10,
481+
.minimum_axes = 4,
482+
});
342483
}
343484

344485
TEST_F(LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {
@@ -569,6 +710,10 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputPenTabletTool) {
569710
#else
570711
TEST_F(LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {}
571712

713+
TEST_F(LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {}
714+
715+
TEST_F(LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerBehavior) {}
716+
572717
TEST_F(LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {}
573718

574719
TEST_F(LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) {}

0 commit comments

Comments
 (0)