/** * @file tests/unit/test_linux_backend.cpp * @brief Unit tests for Linux backend internals. */ // standard includes #include #include #include #include #include #include #include #include #include // platform includes #include #include #if defined(LIBVIRTUALHID_HAVE_XTEST) #include #endif // lib includes #include #include // local includes #include "fixtures/fixtures.hpp" #include "fixtures/linux_backend_test_hooks.hpp" /** * @brief Test fixture for Linux backend internals. */ class LinuxBackendTest: public LinuxTest {}; namespace { std::vector pressed_key_codes( const std::vector &events ) { std::vector pressed_codes; for (const auto &event : events) { if (event.type == EV_KEY && event.value == 1) { pressed_codes.push_back(event.code); } } return pressed_codes; } std::vector relative_axis_values( const std::vector &events, std::uint16_t code ) { std::vector values; for (const auto &event : events) { if (event.type == EV_REL && event.code == code) { values.push_back(event.value); } } return values; } } // namespace TEST_F(LinuxBackendTest, TranslatesKeyboardKeys) { EXPECT_EQ(lvh::detail::test::linux_key_code(0x08), KEY_BACKSPACE); EXPECT_EQ(lvh::detail::test::linux_key_code(0x09), KEY_TAB); EXPECT_EQ(lvh::detail::test::linux_key_code(0x0D), KEY_ENTER); EXPECT_EQ(lvh::detail::test::linux_key_code(0x10), KEY_LEFTSHIFT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x11), KEY_LEFTCTRL); EXPECT_EQ(lvh::detail::test::linux_key_code(0x12), KEY_LEFTALT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x14), KEY_CAPSLOCK); EXPECT_EQ(lvh::detail::test::linux_key_code(0x1B), KEY_ESC); EXPECT_EQ(lvh::detail::test::linux_key_code(0x20), KEY_SPACE); EXPECT_EQ(lvh::detail::test::linux_key_code(0x21), KEY_PAGEUP); EXPECT_EQ(lvh::detail::test::linux_key_code(0x22), KEY_PAGEDOWN); EXPECT_EQ(lvh::detail::test::linux_key_code(0x23), KEY_END); EXPECT_EQ(lvh::detail::test::linux_key_code(0x24), KEY_HOME); EXPECT_EQ(lvh::detail::test::linux_key_code(0x25), KEY_LEFT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x26), KEY_UP); EXPECT_EQ(lvh::detail::test::linux_key_code(0x27), KEY_RIGHT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x28), KEY_DOWN); EXPECT_EQ(lvh::detail::test::linux_key_code(0x2C), KEY_SYSRQ); EXPECT_EQ(lvh::detail::test::linux_key_code(0x2D), KEY_INSERT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x2E), KEY_DELETE); EXPECT_EQ(lvh::detail::test::linux_key_code(0x5B), KEY_LEFTMETA); EXPECT_EQ(lvh::detail::test::linux_key_code(0x5C), KEY_RIGHTMETA); EXPECT_EQ(lvh::detail::test::linux_key_code(0x5D), KEY_COMPOSE); EXPECT_EQ(lvh::detail::test::linux_key_code(0x90), KEY_NUMLOCK); EXPECT_EQ(lvh::detail::test::linux_key_code(0x91), KEY_SCROLLLOCK); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA0), KEY_LEFTSHIFT); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA1), KEY_RIGHTSHIFT); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA2), KEY_LEFTCTRL); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA3), KEY_RIGHTCTRL); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA4), KEY_LEFTALT); EXPECT_EQ(lvh::detail::test::linux_key_code(0xA5), KEY_RIGHTALT); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBA), KEY_SEMICOLON); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBB), KEY_EQUAL); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBC), KEY_COMMA); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBD), KEY_MINUS); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBE), KEY_DOT); EXPECT_EQ(lvh::detail::test::linux_key_code(0xBF), KEY_SLASH); EXPECT_EQ(lvh::detail::test::linux_key_code(0xC0), KEY_GRAVE); EXPECT_EQ(lvh::detail::test::linux_key_code(0xDB), KEY_LEFTBRACE); EXPECT_EQ(lvh::detail::test::linux_key_code(0xDC), KEY_BACKSLASH); EXPECT_EQ(lvh::detail::test::linux_key_code(0xDD), KEY_RIGHTBRACE); EXPECT_EQ(lvh::detail::test::linux_key_code(0xDE), KEY_APOSTROPHE); EXPECT_EQ(lvh::detail::test::linux_key_code(0xE2), KEY_102ND); EXPECT_EQ(lvh::detail::test::linux_key_code(0x30), KEY_0); EXPECT_EQ(lvh::detail::test::linux_key_code(0x31), KEY_1); EXPECT_EQ(lvh::detail::test::linux_key_code(0x39), KEY_9); EXPECT_EQ(lvh::detail::test::linux_key_code(0x41), KEY_A); EXPECT_EQ(lvh::detail::test::linux_key_code(0x42), KEY_B); EXPECT_EQ(lvh::detail::test::linux_key_code(0x5A), KEY_Z); EXPECT_EQ(lvh::detail::test::linux_key_code(0x60), KEY_KP0); EXPECT_EQ(lvh::detail::test::linux_key_code(0x61), KEY_KP1); EXPECT_EQ(lvh::detail::test::linux_key_code(0x69), KEY_KP9); EXPECT_EQ(lvh::detail::test::linux_key_code(0x6A), KEY_KPASTERISK); EXPECT_EQ(lvh::detail::test::linux_key_code(0x6B), KEY_KPPLUS); EXPECT_EQ(lvh::detail::test::linux_key_code(0x6D), KEY_KPMINUS); EXPECT_EQ(lvh::detail::test::linux_key_code(0x6E), KEY_KPDOT); EXPECT_EQ(lvh::detail::test::linux_key_code(0x6F), KEY_KPSLASH); EXPECT_EQ(lvh::detail::test::linux_key_code(0x70), KEY_F1); EXPECT_EQ(lvh::detail::test::linux_key_code(0x7B), KEY_F12); EXPECT_EQ(lvh::detail::test::linux_key_code(0x87), KEY_F24); EXPECT_EQ(lvh::detail::test::linux_key_code(0), -1); EXPECT_EQ(lvh::detail::test::linux_key_code(0x88), -1); } TEST_F(LinuxBackendTest, TranslatesMouseButtonsAndBusTypes) { EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::left), BTN_LEFT); EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::middle), BTN_MIDDLE); EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::right), BTN_RIGHT); EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::side), BTN_SIDE); EXPECT_EQ(lvh::detail::test::linux_mouse_button(lvh::MouseButton::extra), BTN_EXTRA); EXPECT_EQ(lvh::detail::test::linux_mouse_button(static_cast(255)), BTN_LEFT); EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::unknown), BUS_USB); EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::usb), BUS_USB); EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH); EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::xbox_one), BUS_BLUETOOTH); EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::xbox_series), BUS_BLUETOOTH); EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::switch_pro), BUS_BLUETOOTH); EXPECT_EQ(lvh::detail::test::linux_uinput_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::pen), BTN_TOOL_PEN); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::eraser), BTN_TOOL_RUBBER); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::brush), BTN_TOOL_BRUSH); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::pencil), BTN_TOOL_PENCIL); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::airbrush), BTN_TOOL_AIRBRUSH); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::touch), BTN_TOUCH); EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::unchanged), -1); EXPECT_EQ(lvh::detail::test::linux_pen_tool(static_cast(255)), -1); EXPECT_EQ(lvh::detail::test::linux_pen_button(lvh::PenButton::primary), BTN_STYLUS); EXPECT_EQ(lvh::detail::test::linux_pen_button(lvh::PenButton::secondary), BTN_STYLUS2); EXPECT_EQ(lvh::detail::test::linux_pen_button(static_cast(255)), BTN_STYLUS); } TEST_F(LinuxBackendTest, ScalesAbsoluteAxesAndScrollSteps) { EXPECT_EQ(lvh::detail::test::linux_absolute_axis(-1, 100), 0); EXPECT_EQ(lvh::detail::test::linux_absolute_axis(0, 100), 0); EXPECT_EQ(lvh::detail::test::linux_absolute_axis(50, 100), 32767); EXPECT_EQ(lvh::detail::test::linux_absolute_axis(100, 100), 65535); EXPECT_EQ(lvh::detail::test::linux_absolute_axis(101, 100), 65535); EXPECT_EQ(lvh::detail::test::linux_absolute_axis(1, 0), 0); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(0.0F, 1920, 0, 1920, -1920, 3840), 32768); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(960.0F, 1920, 0, 1920, -1920, 3840), 49143); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1920.0F, 1920, 0, 1920, -1920, 3840), 65518); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 0, 0, 1920, -1920, 3840), 0); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 1, 0, 0, 0, 1), 0); EXPECT_EQ(lvh::detail::test::linux_absolute_axis_to_viewport(1.0F, 1, 0, 1, 0, 0), 0); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(0), 0); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(1), 1); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(-1), -1); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(119), 1); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(120), 1); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(240), 2); EXPECT_EQ(lvh::detail::test::linux_legacy_scroll_steps(-240), -2); } TEST_F(LinuxBackendTest, DecodesTextHelpers) { EXPECT_EQ( lvh::detail::test::linux_decode_utf8("A\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"), (std::vector {0x41, 0xE9, 0x20AC, 0x1F600}) ); EXPECT_EQ( lvh::detail::test::linux_decode_utf8(std::string {"A\xFF" "B"}), (std::vector {0x41, 0x42}) ); EXPECT_EQ( lvh::detail::test::linux_decode_utf8(std::string {"A\xC3" "B"}), (std::vector {0x41, 0x42}) ); EXPECT_TRUE(lvh::detail::test::linux_decode_utf8("\xF0\x9F").empty()); EXPECT_EQ(lvh::detail::test::linux_uppercase_hex(0x1F600), "1F600"); EXPECT_EQ(lvh::detail::test::linux_hex_digit_key_code('0'), 0x30); EXPECT_EQ(lvh::detail::test::linux_hex_digit_key_code('9'), 0x39); EXPECT_EQ(lvh::detail::test::linux_hex_digit_key_code('A'), 0x41); EXPECT_EQ(lvh::detail::test::linux_hex_digit_key_code('F'), 0x46); } TEST_F(LinuxBackendTest, CoversLinuxDiscoveryAndIdentityHelpers) { EXPECT_EQ(lvh::detail::test::linux_dualsense_mac_address("02:03:04:05:06:07", 0x1020304), "02:03:04:05:06:07"); EXPECT_EQ(lvh::detail::test::linux_dualsense_mac_address("02-03-04-05-06-07", 0x1020304), "02:00:01:02:03:04"); EXPECT_EQ(lvh::detail::test::linux_dualsense_mac_address("ff:00:100:00:00:00", 0x1020304), "02:00:01:02:03:04"); const auto temp_dir = std::filesystem::current_path() / "cmake-build-linux-backend-test-scratch"; std::filesystem::remove_all(temp_dir); std::filesystem::create_directories(temp_dir); const auto first_line_path = temp_dir / "libvirtualhid-linux-first-line-test.txt"; const auto uevent_path = temp_dir / "libvirtualhid-linux-hidraw-uevent-test.txt"; { std::ofstream file {first_line_path}; file << "libvirtualhid first line\nsecond line\n"; } EXPECT_TRUE(lvh::detail::test::linux_first_line_matches(first_line_path.string(), "libvirtualhid first line")); EXPECT_FALSE(lvh::detail::test::linux_first_line_matches(first_line_path.string(), "other")); EXPECT_TRUE(lvh::detail::test::linux_first_line_missing((temp_dir / "libvirtualhid-missing-first-line-test.txt").string())); { std::ofstream file {uevent_path}; file << "HID_ID=0003:0000:0000\nHID_NAME=libvirtualhid hidraw\n"; } EXPECT_TRUE(lvh::detail::test::linux_hidraw_name_matches(uevent_path.string(), "libvirtualhid hidraw")); EXPECT_FALSE(lvh::detail::test::linux_hidraw_name_matches(uevent_path.string(), "other")); EXPECT_FALSE(lvh::detail::test::linux_hidraw_name_matches((temp_dir / "libvirtualhid-missing-uevent-test.txt").string(), "other")); EXPECT_TRUE(lvh::detail::test::linux_discover_nodes_by_name("").empty()); static_cast(lvh::detail::test::linux_discover_nodes_by_name("libvirtualhid missing device")); EXPECT_EQ(lvh::detail::test::linux_empty_device_nodes_count(), 0U); const auto sysfs_root = temp_dir / "libvirtualhid-linux-sysfs-test"; const auto input_root = sysfs_root / "input"; const auto hidraw_root = sysfs_root / "hidraw"; std::filesystem::remove_all(sysfs_root); std::filesystem::create_directories(input_root / "event0" / "device"); std::filesystem::create_directories(input_root / "js0" / "device"); std::filesystem::create_directories(input_root / "mouse0" / "device"); std::filesystem::create_directories(input_root / "event1" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw0" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw1" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw2" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw3" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw4" / "device"); std::filesystem::create_directories(hidraw_root / "hidraw5" / "device"); { std::ofstream file {input_root / "event0" / "device" / "name"}; file << "libvirtualhid device\n"; } { std::ofstream file {input_root / "js0" / "device" / "name"}; file << "libvirtualhid device\n"; } { std::ofstream file {input_root / "mouse0" / "device" / "name"}; file << "libvirtualhid device\n"; } { std::ofstream file {input_root / "event1" / "device" / "name"}; file << "other device\n"; } { std::ofstream file {hidraw_root / "hidraw0" / "device" / "uevent"}; file << "HID_NAME=libvirtualhid device\n"; } { std::ofstream file {hidraw_root / "hidraw1" / "device" / "uevent"}; file << "HID_NAME=other device\n"; } { std::ofstream file {hidraw_root / "hidraw2" / "device" / "uevent"}; file << "HID_ID=0003:0000:0000\n"; } { std::ofstream file {hidraw_root / "hidraw3" / "device" / "uevent"}; file << "HID_NAME=kernel renamed controller\n" << "HID_PHYS=libvirtualhid/uhid/42\n" << "HID_UNIQ=02:00:00:00:00:01\n"; } { std::ofstream file {hidraw_root / "hidraw4" / "device" / "uevent"}; file << "HID_NAME=other device\n" << "HID_PHYS=other/physical/path\n" << "HID_UNIQ=02:00:00:00:00:02\n"; } { std::ofstream file {hidraw_root / "hidraw5" / "device" / "uevent"}; file << "HID_NAME=missing hidraw name\n" << "HID_PHYS=other/physical/path\n" << "HID_UNIQ=02:00:00:00:00:02\n"; } const auto nodes = lvh::detail::test::linux_discover_nodes_by_name( "libvirtualhid device", input_root.string(), hidraw_root.string() ); EXPECT_TRUE(std::ranges::any_of(nodes, [](const auto &node) { return node.kind == lvh::DeviceNodeKind::input_event && node.path == "/dev/input/event0"; })); EXPECT_TRUE(std::ranges::any_of(nodes, [](const auto &node) { return node.kind == lvh::DeviceNodeKind::joystick && node.path == "/dev/input/js0"; })); EXPECT_TRUE(std::ranges::any_of(nodes, [](const auto &node) { return node.kind == lvh::DeviceNodeKind::hidraw && node.path == "/dev/hidraw0"; })); EXPECT_TRUE(std::ranges::any_of(nodes, [sysfs_root](const auto &node) { return node.kind == lvh::DeviceNodeKind::sysfs && node.path.starts_with(sysfs_root.string()); })); const auto metadata_nodes = lvh::detail::test::linux_discover_hidraw_nodes_by_metadata( "missing hidraw name", "libvirtualhid/uhid/42", "02:00:00:00:00:01", hidraw_root.string() ); EXPECT_TRUE(std::ranges::any_of(metadata_nodes, [](const auto &node) { return node.kind == lvh::DeviceNodeKind::hidraw && node.path == "/dev/hidraw3"; })); EXPECT_TRUE(std::ranges::any_of(metadata_nodes, [hidraw_root](const auto &node) { return node.kind == lvh::DeviceNodeKind::sysfs && node.path == (hidraw_root / "hidraw3").string(); })); EXPECT_FALSE(std::ranges::any_of(metadata_nodes, [](const auto &node) { return node.path.ends_with("hidraw4") || node.path.ends_with("hidraw5"); })); std::filesystem::remove_all(temp_dir); } TEST_F(LinuxBackendTest, HandlesUhidInvalidFileDescriptorPaths) { EXPECT_EQ( lvh::detail::test::linux_uhid_create_with_descriptor_size( lvh::detail::test::linux_uhid_descriptor_limit() + 1 ) .code(), lvh::ErrorCode::unsupported_profile ); EXPECT_EQ(lvh::detail::test::linux_uhid_create_with_descriptor_size(1).code(), lvh::ErrorCode::device_closed); EXPECT_EQ(lvh::detail::test::linux_uhid_submit_report_size(1).code(), lvh::ErrorCode::device_closed); EXPECT_EQ( lvh::detail::test::linux_uhid_submit_report_size(lvh::detail::test::linux_uhid_input_limit() + 1).code(), lvh::ErrorCode::invalid_argument ); EXPECT_EQ(lvh::detail::test::linux_uhid_submit_after_close().code(), lvh::ErrorCode::device_closed); } TEST_F(LinuxBackendTest, HandlesUinputKeyboardInvalidFileDescriptorPaths) { EXPECT_EQ(lvh::detail::test::linux_uinput_keyboard_create_invalid_fd().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_submit_invalid_fd({.key_code = 0, .pressed = true}).code(), lvh::ErrorCode::invalid_argument ); EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_submit_invalid_fd({.key_code = 0x41, .pressed = true}).code(), lvh::ErrorCode::device_closed ); EXPECT_TRUE(lvh::detail::test::linux_uinput_keyboard_type_text_invalid_fd("").ok()); EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_type_text_invalid_fd("A").code(), lvh::ErrorCode::device_closed ); EXPECT_EQ(lvh::detail::test::linux_uinput_keyboard_submit_after_close().code(), lvh::ErrorCode::device_closed); } TEST_F(LinuxBackendTest, PipeBackedUinputKeyboardEmitsEvents) { EXPECT_EQ(lvh::detail::test::linux_copy_string_char_buffer("abcdef"), "abcd"); const auto result = lvh::detail::test::linux_uinput_keyboard_submit_pipe({.key_code = 0x41, .pressed = true}); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 2U); EXPECT_EQ(result.events[0].type, EV_KEY); EXPECT_EQ(result.events[0].code, KEY_A); EXPECT_EQ(result.events[0].value, 1); EXPECT_EQ(result.events[1].type, EV_SYN); EXPECT_EQ(result.events[1].code, SYN_REPORT); EXPECT_EQ(result.events[1].value, 0); EXPECT_EQ(lvh::detail::test::linux_uinput_user_device_invalid_fd().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_uinput_user_device_pipe().code(), lvh::ErrorCode::backend_failure); } TEST_F(LinuxBackendTest, PipeBackedUinputKeyboardEmitsComposeKey) { for (const auto pressed : {true, false}) { const auto result = lvh::detail::test::linux_uinput_keyboard_submit_pipe({.key_code = 0x5D, .pressed = pressed}); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 2U); EXPECT_EQ(result.events[0].type, EV_KEY); EXPECT_EQ(result.events[0].code, KEY_COMPOSE); EXPECT_EQ(result.events[0].value, pressed ? 1 : 0); EXPECT_EQ(result.events[1].type, EV_SYN); EXPECT_EQ(result.events[1].code, SYN_REPORT); } } TEST_F(LinuxBackendTest, UinputKeyboardEmitsOnlySubmittedKeyTransitions) { const auto default_interval = lvh::CreateKeyboardOptions {}.auto_repeat_interval_ms; for (const auto interval_ms : {0U, 1U, default_interval}) { SCOPED_TRACE(interval_ms); const auto result = lvh::detail::test::linux_uinput_keyboard_held_key_events(interval_ms); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 4U); for (std::size_t index = 0; index < 2; ++index) { const auto &key = result.events[index * 2]; EXPECT_EQ(key.type, EV_KEY); EXPECT_EQ(key.code, KEY_F); EXPECT_EQ(key.value, index == 0 ? 1 : 0); const auto &sync = result.events[index * 2 + 1]; EXPECT_EQ(sync.type, EV_SYN); EXPECT_EQ(sync.code, SYN_REPORT); EXPECT_EQ(sync.value, 0); } } } TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) { using enum lvh::GamepadButton; using enum lvh::GamepadProfileKind; struct ButtonCase { lvh::GamepadButton button; std::uint16_t linux_code; }; constexpr std::array button_cases { ButtonCase {a, BTN_SOUTH}, ButtonCase {b, BTN_EAST}, ButtonCase {x, BTN_NORTH}, ButtonCase {y, BTN_WEST}, ButtonCase {left_shoulder, BTN_TL}, ButtonCase {right_shoulder, BTN_TR}, ButtonCase {back, BTN_SELECT}, ButtonCase {start, BTN_START}, ButtonCase {guide, BTN_MODE}, ButtonCase {left_stick, BTN_THUMBL}, ButtonCase {right_stick, BTN_THUMBR}, }; constexpr std::array profile_kinds {generic, xbox_360, xbox_one, xbox_series}; for (const auto kind : profile_kinds) { for (const auto &[button, linux_code] : button_cases) { lvh::GamepadState state; state.buttons.set(button); const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(kind, state); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto pressed_codes = pressed_key_codes(result.events); ASSERT_EQ(pressed_codes.size(), 1U) << "profile " << static_cast(std::to_underlying(kind)) << " logical button " << static_cast(std::to_underlying(button)); EXPECT_EQ(pressed_codes.front(), linux_code) << "profile " << static_cast(std::to_underlying(kind)) << " logical button " << static_cast(std::to_underlying(button)); } } for (const auto kind : {generic, xbox_series}) { lvh::GamepadState state; state.buttons.set(misc1); const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(kind, state); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto pressed = std::ranges::find_if(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == KEY_RECORD && event.value == 1; }); EXPECT_NE(pressed, result.events.end()); } for (const auto kind : profile_kinds) { lvh::GamepadState state; state.buttons.set(dpad_up); state.buttons.set(dpad_right); state.left_stick = {.x = -0.5F, .y = 0.25F}; state.right_stick = {.x = 0.75F, .y = -1.0F}; state.left_trigger = 0.25F; state.right_trigger = 0.75F; const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(kind, state); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto event_value = [&result](std::uint16_t type, std::uint16_t code) -> std::optional { const auto event = std::ranges::find_if(result.events, [type, code](const auto &candidate) { return candidate.type == type && candidate.code == code; }); if (event == result.events.end()) { return std::nullopt; } return event->value; }; EXPECT_EQ(event_value(EV_ABS, ABS_HAT0X), 1); EXPECT_EQ(event_value(EV_ABS, ABS_HAT0Y), -1); for (const auto dpad_button : {BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT}) { EXPECT_EQ(event_value(EV_KEY, dpad_button), std::nullopt); } EXPECT_EQ(event_value(EV_ABS, ABS_X), lvh::reports::normalize_axis(-0.5F)); EXPECT_EQ(event_value(EV_ABS, ABS_Y), lvh::reports::normalize_axis(-0.25F)); EXPECT_EQ(event_value(EV_ABS, ABS_RX), lvh::reports::normalize_axis(0.75F)); EXPECT_EQ(event_value(EV_ABS, ABS_RY), lvh::reports::normalize_axis(1.0F)); EXPECT_EQ(event_value(EV_ABS, ABS_Z), lvh::reports::normalize_trigger(0.25F)); EXPECT_EQ(event_value(EV_ABS, ABS_RZ), lvh::reports::normalize_trigger(0.75F)); } } TEST_F(LinuxBackendTest, UinputGamepadsNormalizeForceFeedback) { using enum lvh::GamepadProfileKind; for (const auto kind : { generic, xbox_360, xbox_one, xbox_series, }) { for (const auto effect_type : {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_RAMP}) { const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(kind, effect_type); ASSERT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); ASSERT_GE(result.callback_count, 1U); EXPECT_EQ(result.last_output.kind, lvh::GamepadOutputKind::rumble); if (effect_type == FF_RUMBLE) { EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5678); EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1234); } else { EXPECT_GT(result.last_output.low_frequency_rumble, 0); EXPECT_GT(result.last_output.high_frequency_rumble, 0); } } } } TEST_F(LinuxBackendTest, UinputGamepadKeepsInfiniteRumbleActiveUntilExplicitStop) { const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble( lvh::GamepadProfileKind::xbox_series, FF_RUMBLE, 0, true ); ASSERT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); ASSERT_GE(result.nonzero_callback_count, 1U); EXPECT_GE(result.zero_callback_count_before_close, 1U); EXPECT_EQ(result.last_output.kind, lvh::GamepadOutputKind::rumble); EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5678); EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1234); } TEST_F(LinuxBackendTest, UinputGamepadRecalculatesActiveRumbleEndAfterReupload) { const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble( lvh::GamepadProfileKind::xbox_series, FF_RUMBLE, 0, false, 1 ); ASSERT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_GE(result.nonzero_callback_count, 1U); EXPECT_GE(result.zero_callback_count_before_close, 1U); } TEST_F(LinuxBackendTest, UinputGamepadRestartsTimingForEachRumblePlaybackCycle) { const auto first_cycle = lvh::detail::test::linux_uinput_rumble_cycle_timing(25, 100); EXPECT_EQ(first_cycle.elapsed, 25U); EXPECT_EQ(first_cycle.remaining, 75U); const auto second_cycle = lvh::detail::test::linux_uinput_rumble_cycle_timing(125, 100); EXPECT_EQ(second_cycle.elapsed, first_cycle.elapsed); EXPECT_EQ(second_cycle.remaining, first_cycle.remaining); const auto cycle_boundary = lvh::detail::test::linux_uinput_rumble_cycle_timing(200, 100); EXPECT_EQ(cycle_boundary.elapsed, 0U); EXPECT_EQ(cycle_boundary.remaining, 100U); } TEST_F(LinuxBackendTest, HandlesUinputMouseInvalidFileDescriptorPaths) { EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_create_invalid_fd().code(), lvh::ErrorCode::backend_failure); lvh::MouseEvent event; event.kind = static_cast(255); EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::invalid_argument); event.kind = lvh::MouseEventKind::relative_motion; event.x = 5; event.y = 0; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.x = 0; event.y = 5; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.y = 0; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.kind = lvh::MouseEventKind::absolute_motion; event.x = 50; event.y = 75; event.width = 100; event.height = 100; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.kind = lvh::MouseEventKind::button; event.button = lvh::MouseButton::side; event.pressed = true; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.kind = lvh::MouseEventKind::vertical_scroll; event.high_resolution_scroll = 120; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); event.kind = lvh::MouseEventKind::horizontal_scroll; event.high_resolution_scroll = -120; EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_invalid_fd(event).code(), lvh::ErrorCode::device_closed); EXPECT_EQ(lvh::detail::test::linux_uinput_mouse_submit_after_close().code(), lvh::ErrorCode::device_closed); } TEST_F(LinuxBackendTest, PipeBackedUinputMouseEmitsEvents) { lvh::MouseEvent event; event.kind = lvh::MouseEventKind::relative_motion; event.x = 5; event.y = -2; auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 3U); EXPECT_EQ(result.events[0].type, EV_REL); EXPECT_EQ(result.events[0].code, REL_X); EXPECT_EQ(result.events[0].value, 5); EXPECT_EQ(result.events[1].type, EV_REL); EXPECT_EQ(result.events[1].code, REL_Y); EXPECT_EQ(result.events[1].value, -2); EXPECT_EQ(result.events[2].type, EV_SYN); event = {}; event.kind = lvh::MouseEventKind::absolute_motion; event.x = 50; event.y = 100; event.width = 100; event.height = 100; result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 3U); EXPECT_EQ(result.events[0].type, EV_ABS); EXPECT_EQ(result.events[0].code, ABS_X); EXPECT_EQ(result.events[0].value, 32767); EXPECT_EQ(result.events[1].type, EV_ABS); EXPECT_EQ(result.events[1].code, ABS_Y); EXPECT_EQ(result.events[1].value, 65535); EXPECT_EQ(result.events[2].type, EV_SYN); event.x = 2580; event.y = 1080; event.width = 3440; event.height = 1440; result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 3U); EXPECT_EQ(result.events[0].type, EV_ABS); EXPECT_EQ(result.events[0].code, ABS_X); EXPECT_EQ(result.events[0].value, 49151); EXPECT_EQ(result.events[1].type, EV_ABS); EXPECT_EQ(result.events[1].code, ABS_Y); EXPECT_EQ(result.events[1].value, 49151); EXPECT_EQ(result.events[2].type, EV_SYN); event = {}; event.kind = lvh::MouseEventKind::button; event.button = lvh::MouseButton::extra; event.pressed = true; result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 2U); EXPECT_EQ(result.events[0].type, EV_KEY); EXPECT_EQ(result.events[0].code, BTN_EXTRA); EXPECT_EQ(result.events[0].value, 1); EXPECT_EQ(result.events[1].type, EV_SYN); event = {}; event.kind = lvh::MouseEventKind::vertical_scroll; event.high_resolution_scroll = 120; result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); #if defined(REL_WHEEL_HI_RES) constexpr auto expected_vertical_scroll_event_count = 3U; #else constexpr auto expected_vertical_scroll_event_count = 2U; #endif ASSERT_EQ(result.events.size(), expected_vertical_scroll_event_count); EXPECT_EQ(result.events[0].type, EV_REL); EXPECT_EQ(result.events[0].code, REL_WHEEL); EXPECT_EQ(result.events[0].value, 1); #if defined(REL_WHEEL_HI_RES) EXPECT_EQ(result.events[1].type, EV_REL); EXPECT_EQ(result.events[1].code, REL_WHEEL_HI_RES); EXPECT_EQ(result.events[1].value, 120); #endif EXPECT_EQ(result.events.back().type, EV_SYN); event = {}; event.kind = lvh::MouseEventKind::horizontal_scroll; event.high_resolution_scroll = -120; result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event); ASSERT_TRUE(result.status.ok()) << result.status.message(); #if defined(REL_HWHEEL_HI_RES) constexpr auto expected_horizontal_scroll_event_count = 3U; #else constexpr auto expected_horizontal_scroll_event_count = 2U; #endif ASSERT_EQ(result.events.size(), expected_horizontal_scroll_event_count); EXPECT_EQ(result.events[0].type, EV_REL); EXPECT_EQ(result.events[0].code, REL_HWHEEL); EXPECT_EQ(result.events[0].value, -1); #if defined(REL_HWHEEL_HI_RES) EXPECT_EQ(result.events[1].type, EV_REL); EXPECT_EQ(result.events[1].code, REL_HWHEEL_HI_RES); EXPECT_EQ(result.events[1].value, -120); #endif EXPECT_EQ(result.events.back().type, EV_SYN); } TEST_F(LinuxBackendTest, PipeBackedUinputMouseMapsConfiguredViewport) { lvh::MouseEvent event { .kind = lvh::MouseEventKind::absolute_motion, .x = 960, .y = 540, .width = 1920, .height = 1080, }; lvh::CreateMouseOptions options; options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080}; options.viewport = {.offset_x = 0, .offset_y = 0, .width = 1920, .height = 1080}; const auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event, options); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.events.size(), 3U); EXPECT_EQ(result.events[0].code, ABS_X); EXPECT_EQ(result.events[0].value, 49143); EXPECT_EQ(result.events[1].code, ABS_Y); EXPECT_EQ(result.events[1].value, 32737); } TEST_F(LinuxBackendTest, PipeBackedUinputMouseRoutesMotionAndButtonsAcrossSplitDevices) { const std::vector events { {.kind = lvh::MouseEventKind::absolute_motion, .x = 50, .y = 25, .width = 100, .height = 100}, {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::side, .pressed = true}, {.kind = lvh::MouseEventKind::relative_motion, .x = 5, .y = -2}, {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::side, .pressed = false}, {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::extra, .pressed = true}, {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::extra, .pressed = false}, }; const auto result = lvh::detail::test::linux_uinput_mouse_submit_split_pipe_sequence(events); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_EQ(result.absolute_events.size(), 7U); EXPECT_EQ(result.absolute_events[0].type, EV_ABS); EXPECT_EQ(result.absolute_events[0].code, ABS_X); EXPECT_EQ(result.absolute_events[1].type, EV_ABS); EXPECT_EQ(result.absolute_events[1].code, ABS_Y); EXPECT_EQ(result.absolute_events[3].type, EV_KEY); EXPECT_EQ(result.absolute_events[3].code, BTN_SIDE); EXPECT_EQ(result.absolute_events[3].value, 1); EXPECT_EQ(result.absolute_events[5].type, EV_KEY); EXPECT_EQ(result.absolute_events[5].code, BTN_SIDE); EXPECT_EQ(result.absolute_events[5].value, 0); EXPECT_TRUE(std::ranges::none_of(result.absolute_events, [](const auto &event) { return event.type == EV_REL; })); ASSERT_EQ(result.relative_events.size(), 7U); EXPECT_EQ(result.relative_events[0].type, EV_REL); EXPECT_EQ(result.relative_events[0].code, REL_X); EXPECT_EQ(result.relative_events[1].type, EV_REL); EXPECT_EQ(result.relative_events[1].code, REL_Y); EXPECT_EQ(result.relative_events[3].type, EV_KEY); EXPECT_EQ(result.relative_events[3].code, BTN_EXTRA); EXPECT_EQ(result.relative_events[3].value, 1); EXPECT_EQ(result.relative_events[5].type, EV_KEY); EXPECT_EQ(result.relative_events[5].code, BTN_EXTRA); EXPECT_EQ(result.relative_events[5].value, 0); EXPECT_TRUE(std::ranges::none_of(result.relative_events, [](const auto &event) { return event.type == EV_ABS; })); } TEST_F(LinuxBackendTest, PipeBackedUinputMouseScrollsOnLastMotionDevice) { using enum lvh::MouseEventKind; const std::vector events { {.kind = absolute_motion, .x = 50, .y = 25, .width = 100, .height = 100}, {.kind = vertical_scroll, .high_resolution_scroll = 120}, {.kind = horizontal_scroll, .high_resolution_scroll = -120}, {.kind = relative_motion, .x = 5, .y = -2}, {.kind = vertical_scroll, .high_resolution_scroll = -120}, }; const auto result = lvh::detail::test::linux_uinput_mouse_submit_split_pipe_sequence(events); ASSERT_TRUE(result.status.ok()) << result.status.message(); EXPECT_EQ(relative_axis_values(result.absolute_events, REL_WHEEL), (std::vector {1})); EXPECT_EQ(relative_axis_values(result.absolute_events, REL_HWHEEL), (std::vector {-1})); EXPECT_EQ(relative_axis_values(result.relative_events, REL_WHEEL), (std::vector {-1})); #if defined(REL_WHEEL_HI_RES) EXPECT_EQ(relative_axis_values(result.absolute_events, REL_WHEEL_HI_RES), (std::vector {120})); EXPECT_EQ(relative_axis_values(result.relative_events, REL_WHEEL_HI_RES), (std::vector {-120})); #endif } TEST_F(LinuxBackendTest, PipeBackedUinputMouseAccumulatesLegacyScrollDetentsPerAxis) { const std::vector events { {.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60}, {.kind = lvh::MouseEventKind::horizontal_scroll, .high_resolution_scroll = -60}, {.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60}, {.kind = lvh::MouseEventKind::horizontal_scroll, .high_resolution_scroll = -60}, }; const auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe_sequence(events); ASSERT_TRUE(result.status.ok()) << result.status.message(); EXPECT_EQ(relative_axis_values(result.events, REL_WHEEL), (std::vector {1})); EXPECT_EQ(relative_axis_values(result.events, REL_HWHEEL), (std::vector {-1})); #if defined(REL_WHEEL_HI_RES) EXPECT_EQ(relative_axis_values(result.events, REL_WHEEL_HI_RES), (std::vector {60, 60})); #endif #if defined(REL_HWHEEL_HI_RES) EXPECT_EQ(relative_axis_values(result.events, REL_HWHEEL_HI_RES), (std::vector {-60, -60})); #endif EXPECT_EQ( std::ranges::count_if(result.events, [](const auto &event) { return event.type == EV_SYN && event.code == SYN_REPORT; }), 4 ); } TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesEmitEvents) { const lvh::TouchContact contact { .id = 7, .x = 0.5F, .y = 0.25F, .pressure = 0.75F, .orientation = 45, }; auto result = lvh::detail::test::linux_uinput_touchscreen_contact_pipe(contact); ASSERT_TRUE(result.status.ok()) << result.status.message(); ASSERT_GE(result.events.size(), 10U); EXPECT_EQ(result.events[0].type, EV_ABS); EXPECT_EQ(result.events[0].code, ABS_MT_SLOT); EXPECT_EQ(result.events[1].type, EV_ABS); EXPECT_EQ(result.events[1].code, ABS_MT_TRACKING_ID); EXPECT_EQ(result.events[2].type, EV_KEY); EXPECT_EQ(result.events[2].code, BTN_TOUCH); EXPECT_EQ(result.events[2].value, 1); EXPECT_EQ(result.events[3].type, EV_ABS); EXPECT_EQ(result.events[3].code, ABS_X); EXPECT_EQ(result.events[4].type, EV_ABS); EXPECT_EQ(result.events[4].code, ABS_MT_POSITION_X); result = lvh::detail::test::linux_uinput_trackpad_contact_pipe(contact); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto saw_left_button = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_LEFT && event.value == 1; }); const auto saw_finger_tool = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_FINGER && event.value == 1; }); EXPECT_TRUE(saw_left_button); EXPECT_TRUE(saw_finger_tool); const lvh::PenToolState tool { .tool = lvh::PenToolType::pen, .x = 0.25F, .y = 0.5F, .pressure = 0.5F, .distance = -1.0F, .tilt_x = 45.0F, .tilt_y = -45.0F, }; result = lvh::detail::test::linux_uinput_pen_tablet_tool_pipe(tool); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto saw_pen_tool = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_PEN && event.value == 1; }); const auto saw_pressure = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_ABS && event.code == ABS_PRESSURE && event.value > 0; }); const auto saw_stylus = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_STYLUS && event.value == 1; }); EXPECT_TRUE(saw_pen_tool); EXPECT_TRUE(saw_pressure); EXPECT_TRUE(saw_stylus); } TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesCoverStateTransitions) { auto result = lvh::detail::test::linux_uinput_trackpad_multi_contact_pipe(); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto saw_doubletap = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_DOUBLETAP && event.value == 1; }); const auto saw_tripletap = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_TRIPLETAP && event.value == 1; }); const auto saw_quadtap = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_QUADTAP && event.value == 1; }); const auto saw_quinttap = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_QUINTTAP && event.value == 1; }); EXPECT_TRUE(saw_doubletap); EXPECT_TRUE(saw_tripletap); EXPECT_TRUE(saw_quadtap); EXPECT_TRUE(saw_quinttap); EXPECT_EQ(lvh::detail::test::linux_uinput_touchscreen_invalid_contacts().code(), lvh::ErrorCode::invalid_argument); result = lvh::detail::test::linux_uinput_pen_tablet_transition_pipe(); ASSERT_TRUE(result.status.ok()) << result.status.message(); const auto saw_rubber = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_RUBBER && event.value == 1; }); const auto saw_pen_release = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_TOOL_PEN && event.value == 0; }); const auto saw_distance = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_ABS && event.code == ABS_DISTANCE && event.value > 0; }); const auto saw_secondary_button = std::ranges::any_of(result.events, [](const auto &event) { return event.type == EV_KEY && event.code == BTN_STYLUS2 && event.value == 1; }); EXPECT_TRUE(saw_rubber); EXPECT_TRUE(saw_pen_release); EXPECT_TRUE(saw_distance); EXPECT_TRUE(saw_secondary_button); EXPECT_EQ(lvh::detail::test::linux_uinput_pen_tablet_closed_status().code(), lvh::ErrorCode::device_closed); } TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesReuseSlotsWithNewTrackingIds) { for (const auto device_type : {lvh::DeviceType::touchscreen, lvh::DeviceType::trackpad}) { const auto result = lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(device_type); ASSERT_TRUE(result.status.ok()) << result.status.message(); std::vector selected_slots; std::vector tracking_ids; for (const auto &event : result.events) { if (event.type == EV_ABS && event.code == ABS_MT_SLOT) { selected_slots.push_back(event.value); } else if (event.type == EV_ABS && event.code == ABS_MT_TRACKING_ID) { tracking_ids.push_back(event.value); } } EXPECT_EQ(selected_slots, (std::vector {0, 1, 0, 1})); EXPECT_EQ(tracking_ids, (std::vector {0, 1, -1, 2})); } EXPECT_EQ( lvh::detail::test::linux_uinput_touch_contact_reuse_pipe(lvh::DeviceType::mouse).status.code(), lvh::ErrorCode::invalid_argument ); } TEST_F(LinuxBackendTest, SocketpairBackedUhidGamepadRoundTripsEvents) { const auto result = lvh::detail::test::linux_uhid_socketpair_roundtrip(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, lvh::profiles::xbox_360().name); EXPECT_TRUE(result.saw_input); EXPECT_TRUE(result.saw_get_report_reply); EXPECT_TRUE(result.saw_set_report_reply); EXPECT_TRUE(result.saw_destroy); EXPECT_GE(result.output.callback_count, 2U); EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5678); EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1234); } TEST_F(LinuxBackendTest, XboxOneAndSeriesPreferBluetoothUhidWithUinputFallback) { using enum lvh::GamepadProfileKind; EXPECT_FALSE(lvh::detail::test::linux_gamepad_prefers_uhid(generic)); EXPECT_FALSE(lvh::detail::test::linux_gamepad_prefers_uhid(xbox_360)); EXPECT_TRUE(lvh::detail::test::linux_gamepad_prefers_uhid(switch_pro)); EXPECT_TRUE(lvh::detail::test::linux_gamepad_prefers_uhid(dualshock4)); EXPECT_TRUE(lvh::detail::test::linux_gamepad_prefers_uhid(dualsense)); for (const auto kind : {xbox_one, xbox_series}) { EXPECT_TRUE(lvh::detail::test::linux_gamepad_prefers_uhid(kind)); EXPECT_TRUE(lvh::detail::test::linux_gamepad_uses_uinput(kind)); const auto effective_profile = lvh::detail::test::linux_uinput_effective_gamepad_profile(kind); EXPECT_TRUE(effective_profile.capabilities.supports_rumble); EXPECT_FALSE(effective_profile.capabilities.supports_trigger_rumble); EXPECT_FALSE(effective_profile.capabilities.supports_battery); const auto result = lvh::detail::test::linux_xbox_bluetooth_uhid_socketpair_reports(kind, true, true); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_TRUE(result.xbox.saw_transport_descriptor); // The kernel must not match Microsoft's physical-device driver IDs, while // the 16-bit input and hidraw identities must still identify an Xbox. EXPECT_EQ(result.creation.vendor_id, 0x0001045EU); EXPECT_NE(result.creation.vendor_id, 0x045EU); EXPECT_EQ(static_cast(result.creation.vendor_id), 0x045EU); EXPECT_EQ(result.creation.product_id, kind == xbox_series ? 0x0B13U : 0x0B20U); EXPECT_TRUE(result.xbox.saw_battery_descriptor); EXPECT_TRUE(result.xbox.saw_canonical_evdev_axes); EXPECT_TRUE(result.xbox.saw_gamepad_application_usage); EXPECT_TRUE(result.xbox.saw_bluetooth_identity); EXPECT_TRUE(result.xbox.saw_input); EXPECT_TRUE(result.xbox.saw_battery_input); EXPECT_TRUE(result.xbox.saw_guide); EXPECT_TRUE(result.xbox.saw_profile_consumer_button); EXPECT_TRUE(result.saw_destroy); ASSERT_TRUE(result.output.rumble.has_value()); EXPECT_EQ(result.output.rumble->low_frequency_rumble, 49151U); EXPECT_EQ(result.output.rumble->high_frequency_rumble, 65535U); ASSERT_TRUE(result.output.trigger_rumble.has_value()); EXPECT_EQ(result.output.trigger_rumble->left_trigger_rumble, 16384U); EXPECT_EQ(result.output.trigger_rumble->right_trigger_rumble, 32768U); const auto no_battery_result = lvh::detail::test::linux_xbox_bluetooth_uhid_socketpair_reports(kind, false, true); EXPECT_TRUE(no_battery_result.create_status.ok()) << no_battery_result.create_status.message(); EXPECT_TRUE(no_battery_result.submit_status.ok()) << no_battery_result.submit_status.message(); EXPECT_TRUE(no_battery_result.close_status.ok()) << no_battery_result.close_status.message(); EXPECT_TRUE(no_battery_result.creation.saw_create); EXPECT_TRUE(no_battery_result.xbox.saw_transport_descriptor); EXPECT_FALSE(no_battery_result.xbox.saw_battery_descriptor); EXPECT_TRUE(no_battery_result.xbox.saw_input); EXPECT_FALSE(no_battery_result.xbox.saw_battery_input); EXPECT_TRUE(no_battery_result.saw_destroy); const auto missing_battery_result = lvh::detail::test::linux_xbox_bluetooth_uhid_socketpair_reports(kind, true, false); EXPECT_TRUE(missing_battery_result.create_status.ok()) << missing_battery_result.create_status.message(); EXPECT_TRUE(missing_battery_result.submit_status.ok()) << missing_battery_result.submit_status.message(); EXPECT_TRUE(missing_battery_result.close_status.ok()) << missing_battery_result.close_status.message(); EXPECT_TRUE(missing_battery_result.creation.saw_create); EXPECT_TRUE(missing_battery_result.xbox.saw_transport_descriptor); EXPECT_TRUE(missing_battery_result.xbox.saw_battery_descriptor); EXPECT_TRUE(missing_battery_result.xbox.saw_input); EXPECT_FALSE(missing_battery_result.xbox.saw_battery_input); EXPECT_TRUE(missing_battery_result.saw_destroy); } } TEST_F(LinuxBackendTest, SocketpairBackedSwitchProUsesNativeUhidProtocol) { const auto result = lvh::detail::test::linux_switch_pro_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, lvh::profiles::switch_pro().name); EXPECT_EQ(result.creation.vendor_id, 0x057EU); EXPECT_EQ(result.creation.product_id, 0x2009U); EXPECT_EQ(result.creation.physical_id, "libvirtualhid/uhid/057e:2009/12"); EXPECT_EQ(result.creation.unique_id, "057e:2009/streaming-host-gamepad-0"); EXPECT_TRUE(result.switch_pro.saw_subcommand_reply); EXPECT_TRUE(result.switch_pro.saw_motion_input); ASSERT_TRUE(result.switch_pro.subcommand_reply_packet_timer.has_value()); ASSERT_TRUE(result.switch_pro.motion_input_packet_timer.has_value()); EXPECT_NE( *result.switch_pro.motion_input_packet_timer, *result.switch_pro.subcommand_reply_packet_timer ); EXPECT_TRUE(result.switch_pro.saw_player_leds); ASSERT_EQ(result.output.callback_count, 2U); EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::player_leds); } TEST_F(LinuxBackendTest, SwitchProStreamsIdleAndLatestStateUntilClosed) { const auto result = lvh::detail::test::linux_switch_pro_uhid_socketpair_reports(); ASSERT_TRUE(result.create_status.ok()) << result.create_status.message(); ASSERT_TRUE(result.submit_status.ok()) << result.submit_status.message(); ASSERT_TRUE(result.switch_pro.clear_status.ok()) << result.switch_pro.clear_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); for (const auto &reports : { result.switch_pro.idle_input_reports, result.switch_pro.repeated_input_reports, result.switch_pro.cleared_input_reports, }) { ASSERT_EQ(reports.size(), 2U); EXPECT_EQ(reports[0].size(), lvh::profiles::switch_pro().input_report_size); EXPECT_EQ(reports[0][0], 0x30U); EXPECT_NE(reports[0][1], reports[1][1]); EXPECT_TRUE(std::equal(reports[0].begin() + 2, reports[0].end(), reports[1].begin() + 2)); } EXPECT_TRUE(result.saw_destroy); EXPECT_TRUE(result.switch_pro.stopped_after_destroy); } TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) { const auto result = lvh::detail::test::linux_dualsense_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.dualsense.saw_calibration); EXPECT_TRUE(result.dualsense.saw_pairing); EXPECT_TRUE(result.dualsense.saw_firmware); EXPECT_TRUE(result.saw_set_report_reply); ASSERT_GE(result.output.callback_count, 1U); EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5656); EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1212); } TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) { const auto result = lvh::detail::test::linux_dualsense_bluetooth_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.dualsense.saw_bluetooth_input_with_live_sensor_metadata); EXPECT_TRUE(result.dualsense.saw_pairing); EXPECT_TRUE(result.dualsense.saw_feature_crc); } TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) { const auto result = lvh::detail::test::linux_dualshock4_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.dualshock4.saw_usb_input); EXPECT_TRUE(result.dualshock4.saw_calibration); EXPECT_TRUE(result.dualshock4.saw_pairing); EXPECT_TRUE(result.dualshock4.saw_firmware); EXPECT_TRUE(result.saw_set_report_reply); ASSERT_GE(result.output.callback_count, 1U); EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble); EXPECT_EQ(result.output.last.low_frequency_rumble, 0x5656); EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1212); } TEST_F(LinuxBackendTest, SocketpairBackedDualShock4BluetoothFramesReports) { const auto result = lvh::detail::test::linux_dualshock4_bluetooth_uhid_socketpair_reports(); EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); EXPECT_TRUE(result.creation.saw_create); EXPECT_TRUE(result.creation.waited_for_start); EXPECT_EQ(result.creation.name, "Wireless Controller"); EXPECT_TRUE(result.dualshock4.saw_bluetooth_input); EXPECT_TRUE(result.dualshock4.saw_calibration); EXPECT_TRUE(result.dualshock4.saw_pairing); EXPECT_TRUE(result.dualshock4.saw_feature_crc); } TEST_F(LinuxBackendTest, FakeLinuxBackendCreatesAllDeviceTypes) { const auto unavailable = lvh::detail::test::linux_backend_fake_unavailable_capabilities(); EXPECT_FALSE(unavailable.supports_virtual_hid); EXPECT_FALSE(unavailable.supports_gamepad); EXPECT_FALSE(unavailable.supports_output_reports); #if defined(LIBVIRTUALHID_HAVE_XTEST) EXPECT_TRUE(unavailable.supports_keyboard); EXPECT_TRUE(unavailable.supports_mouse); EXPECT_TRUE(unavailable.supports_xtest_fallback); #else EXPECT_FALSE(unavailable.supports_keyboard); EXPECT_FALSE(unavailable.supports_mouse); EXPECT_FALSE(unavailable.supports_xtest_fallback); #endif EXPECT_EQ(lvh::detail::test::linux_backend_gamepad_fake_open_failure().code(), lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_backend_gamepad_fake_create_failure().code(), lvh::ErrorCode::backend_failure); const auto keyboard_open_status = lvh::detail::test::linux_backend_keyboard_fake_open_failure(); EXPECT_TRUE(keyboard_open_status.ok() || keyboard_open_status.code() == lvh::ErrorCode::backend_unavailable); const auto keyboard_create_status = lvh::detail::test::linux_backend_keyboard_fake_create_failure(); EXPECT_TRUE(keyboard_create_status.ok() || keyboard_create_status.code() == lvh::ErrorCode::backend_failure); EXPECT_EQ( lvh::detail::test::linux_backend_keyboard_fake_create_failure_without_fallback().code(), lvh::ErrorCode::backend_failure ); const auto keyboard_fallback_status = lvh::detail::test::linux_backend_keyboard_fake_fallback_success(); EXPECT_TRUE(keyboard_fallback_status.ok() || keyboard_fallback_status.code() == lvh::ErrorCode::backend_unavailable); const auto mouse_open_status = lvh::detail::test::linux_backend_mouse_fake_open_failure(); EXPECT_TRUE(mouse_open_status.ok() || mouse_open_status.code() == lvh::ErrorCode::backend_unavailable); const auto mouse_create_status = lvh::detail::test::linux_backend_mouse_fake_create_failure(); EXPECT_TRUE(mouse_create_status.ok() || mouse_create_status.code() == lvh::ErrorCode::backend_failure); EXPECT_EQ( lvh::detail::test::linux_backend_mouse_fake_create_failure_without_fallback().code(), lvh::ErrorCode::backend_failure ); const auto mouse_fallback_status = lvh::detail::test::linux_backend_mouse_fake_fallback_success(); EXPECT_TRUE(mouse_fallback_status.ok() || mouse_fallback_status.code() == lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_backend_touchscreen_fake_open_failure().code(), lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_backend_touchscreen_fake_create_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_backend_trackpad_fake_open_failure().code(), lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_backend_trackpad_fake_create_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_backend_pen_tablet_fake_open_failure().code(), lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_backend_pen_tablet_fake_create_failure().code(), lvh::ErrorCode::backend_failure); const auto result = lvh::detail::test::linux_backend_create_all_fake_success(); EXPECT_TRUE(result.capabilities.supports_virtual_hid); EXPECT_TRUE(result.capabilities.supports_gamepad); EXPECT_TRUE(result.capabilities.supports_keyboard); EXPECT_TRUE(result.capabilities.supports_mouse); EXPECT_TRUE(result.capabilities.supports_touchscreen); EXPECT_TRUE(result.capabilities.supports_trackpad); EXPECT_TRUE(result.capabilities.supports_pen_tablet); EXPECT_TRUE(result.capabilities.supports_output_reports); EXPECT_TRUE(result.gamepad_status.ok()) << result.gamepad_status.message(); EXPECT_TRUE(result.gamepad_close_status.ok()) << result.gamepad_close_status.message(); EXPECT_TRUE(result.keyboard_status.ok()) << result.keyboard_status.message(); EXPECT_TRUE(result.keyboard_close_status.ok()) << result.keyboard_close_status.message(); EXPECT_TRUE(result.mouse_status.ok()) << result.mouse_status.message(); EXPECT_TRUE(result.mouse_close_status.ok()) << result.mouse_close_status.message(); EXPECT_TRUE(result.touchscreen_status.ok()) << result.touchscreen_status.message(); EXPECT_TRUE(result.touchscreen_close_status.ok()) << result.touchscreen_close_status.message(); EXPECT_TRUE(result.trackpad_status.ok()) << result.trackpad_status.message(); EXPECT_TRUE(result.trackpad_close_status.ok()) << result.trackpad_close_status.message(); EXPECT_TRUE(result.pen_tablet_status.ok()) << result.pen_tablet_status.message(); EXPECT_TRUE(result.pen_tablet_close_status.ok()) << result.pen_tablet_close_status.message(); } TEST_F(LinuxBackendTest, OpensUhidGamepadsNonblocking) { const auto flags = lvh::detail::test::linux_backend_gamepad_open_flags(); EXPECT_EQ(flags & O_ACCMODE, O_RDWR); EXPECT_NE(flags & O_CLOEXEC, 0); EXPECT_NE(flags & O_NONBLOCK, 0); } TEST_F(LinuxBackendTest, FakeUhidSyscallsCoverFailureBranches) { EXPECT_EQ(lvh::detail::test::linux_uhid_submit_fake_write_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_uhid_submit_fake_short_write().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_uhid_close_fake_write_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_EQ(lvh::detail::test::linux_uhid_close_fake_close_failure().code(), lvh::ErrorCode::backend_failure); EXPECT_TRUE(lvh::detail::test::linux_uhid_read_loop_fake_retry_branches().ok()); EXPECT_TRUE(lvh::detail::test::linux_uhid_read_loop_fake_poll_errors().ok()); EXPECT_TRUE(lvh::detail::test::linux_uhid_read_loop_fake_read_error().ok()); EXPECT_TRUE(lvh::detail::test::linux_uhid_read_loop_fake_output_without_callback().ok()); } TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranches) { using enum lvh::GamepadProfileKind; const auto has_type = [](const lvh::detail::test::LinuxLibevdevCreationResult &result, std::uint32_t type) { return std::ranges::find(result.event_types, type) != result.event_types.end(); }; const auto has_property = [](const lvh::detail::test::LinuxLibevdevCreationResult &result, std::uint32_t property) { return std::ranges::find(result.properties, property) != result.properties.end(); }; const auto find_code = [](const lvh::detail::test::LinuxLibevdevCreationResult &result, std::uint32_t type, std::uint32_t code) { const auto it = std::ranges::find_if(result.event_codes, [type, code](const auto &event_code) { return event_code.type == type && event_code.code == code; }); return it == result.event_codes.end() ? nullptr : std::to_address(it); }; const auto keyboard = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::keyboard); ASSERT_TRUE(keyboard.status.ok()) << keyboard.status.message(); EXPECT_EQ(keyboard.name, lvh::profiles::keyboard().name); EXPECT_EQ(keyboard.bustype, BUS_USB); EXPECT_TRUE(has_type(keyboard, EV_KEY)); for (const auto code : {KEY_A, KEY_ENTER, KEY_LEFTSHIFT, KEY_F1, KEY_VOLUMEUP, KEY_OK}) { EXPECT_NE(find_code(keyboard, EV_KEY, code), nullptr) << code; } for (const auto code : {BTN_LEFT, BTN_SOUTH, BTN_TOOL_PEN, BTN_TOUCH}) { EXPECT_EQ(find_code(keyboard, EV_KEY, code), nullptr) << code; } constexpr auto unnamed_code = 0x54; ASSERT_EQ(libevdev_event_code_get_name(EV_KEY, unnamed_code), nullptr); EXPECT_EQ(find_code(keyboard, EV_KEY, unnamed_code), nullptr); EXPECT_EQ(keyboard.destroy_count, 1U); struct GamepadCase { lvh::GamepadProfileKind kind; std::uint16_t bustype; std::uint16_t vendor_id; std::uint16_t product_id; bool key_record; bool sparse_button_slots; }; constexpr std::array gamepad_cases { GamepadCase {generic, BUS_USB, 0x1209, 0x0001, true, false}, GamepadCase {xbox_360, BUS_BLUETOOTH, 0x045E, 0x028E, false, true}, GamepadCase {xbox_one, BUS_BLUETOOTH, 0x045E, 0x0B20, false, true}, GamepadCase {xbox_series, BUS_BLUETOOTH, 0x045E, 0x0B13, true, true}, }; constexpr std::array active_buttons { BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR, }; constexpr std::array reserved_buttons {BTN_C, BTN_Z, BTN_TL2, BTN_TR2}; constexpr std::array dpad_buttons {BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT}; constexpr std::array feedback_codes {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_SINE, FF_RAMP, FF_GAIN}; for (const auto &[kind, bustype, vendor_id, product_id, key_record, sparse_button_slots] : gamepad_cases) { const auto expected_profile = lvh::profiles::gamepad_profile(kind); ASSERT_TRUE(expected_profile.has_value()); const auto gamepad = lvh::detail::test::linux_uinput_create_fake_gamepad(kind); ASSERT_TRUE(gamepad.status.ok()) << gamepad.status.message(); EXPECT_EQ(gamepad.name, expected_profile->name); EXPECT_EQ(gamepad.vendor, vendor_id); EXPECT_EQ(gamepad.bustype, bustype); EXPECT_EQ(gamepad.product, product_id); EXPECT_EQ(gamepad.version, expected_profile->version); EXPECT_TRUE(has_type(gamepad, EV_KEY)); EXPECT_TRUE(has_type(gamepad, EV_ABS)); EXPECT_EQ(has_type(gamepad, EV_FF), expected_profile->capabilities.supports_rumble); for (const auto button : active_buttons) { EXPECT_NE(find_code(gamepad, EV_KEY, button), nullptr) << "missing canonical gamepad button " << button; } for (const auto button : reserved_buttons) { const auto expected = sparse_button_slots; EXPECT_EQ(find_code(gamepad, EV_KEY, button) != nullptr, expected) << "unexpected reserved gamepad button slot state for " << button; } for (const auto button : dpad_buttons) { EXPECT_EQ(find_code(gamepad, EV_KEY, button), nullptr) << "unexpected directional gamepad button capability for " << button; } EXPECT_NE(find_code(gamepad, EV_ABS, ABS_HAT0X), nullptr); EXPECT_NE(find_code(gamepad, EV_ABS, ABS_HAT0Y), nullptr); EXPECT_EQ(find_code(gamepad, EV_KEY, KEY_RECORD) != nullptr, key_record); const auto *left_trigger = find_code(gamepad, EV_ABS, ABS_Z); ASSERT_NE(left_trigger, nullptr); EXPECT_EQ(left_trigger->minimum, 0); EXPECT_EQ(left_trigger->maximum, 255); for (const auto code : feedback_codes) { EXPECT_EQ(find_code(gamepad, EV_FF, code) != nullptr, expected_profile->capabilities.supports_rumble); } } const auto xbox_360_result = lvh::detail::test::linux_uinput_create_fake_gamepad(lvh::GamepadProfileKind::xbox_360); ASSERT_TRUE(xbox_360_result.status.ok()) << xbox_360_result.status.message(); std::vector xbox_360_button_slots; for (const auto &event_code : xbox_360_result.event_codes) { if (event_code.type == EV_KEY && event_code.code >= BTN_SOUTH && event_code.code <= BTN_THUMBR) { xbox_360_button_slots.push_back(event_code.code); } } std::ranges::sort(xbox_360_button_slots); constexpr std::array expected_xbox_360_button_slots { BTN_SOUTH, BTN_EAST, BTN_C, BTN_NORTH, BTN_WEST, BTN_Z, BTN_TL, BTN_TR, BTN_TL2, BTN_TR2, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR, }; ASSERT_EQ(xbox_360_button_slots.size(), expected_xbox_360_button_slots.size()); for (std::size_t index = 0; index < expected_xbox_360_button_slots.size(); ++index) { EXPECT_EQ(xbox_360_button_slots[index], expected_xbox_360_button_slots[index]) << "button slot " << index; } const auto relative_mouse = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::mouse); ASSERT_TRUE(relative_mouse.status.ok()) << relative_mouse.status.message(); EXPECT_TRUE(has_type(relative_mouse, EV_KEY)); EXPECT_TRUE(has_type(relative_mouse, EV_REL)); EXPECT_FALSE(has_type(relative_mouse, EV_ABS)); EXPECT_FALSE(has_property(relative_mouse, INPUT_PROP_DIRECT)); EXPECT_NE(find_code(relative_mouse, EV_KEY, BTN_LEFT), nullptr); EXPECT_NE(find_code(relative_mouse, EV_REL, REL_X), nullptr); EXPECT_NE(find_code(relative_mouse, EV_REL, REL_WHEEL), nullptr); EXPECT_NE(find_code(relative_mouse, EV_REL, REL_HWHEEL), nullptr); #if defined(REL_WHEEL_HI_RES) EXPECT_NE(find_code(relative_mouse, EV_REL, REL_WHEEL_HI_RES), nullptr); #endif #if defined(REL_HWHEEL_HI_RES) EXPECT_NE(find_code(relative_mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr); #endif const auto absolute_mouse = lvh::detail::test::linux_uinput_create_fake_absolute_mouse_device(); ASSERT_TRUE(absolute_mouse.status.ok()) << absolute_mouse.status.message(); EXPECT_TRUE(has_type(absolute_mouse, EV_KEY)); EXPECT_TRUE(has_type(absolute_mouse, EV_REL)); EXPECT_TRUE(has_type(absolute_mouse, EV_ABS)); EXPECT_TRUE(has_property(absolute_mouse, INPUT_PROP_DIRECT)); EXPECT_NE(find_code(absolute_mouse, EV_KEY, BTN_LEFT), nullptr); EXPECT_EQ(find_code(absolute_mouse, EV_REL, REL_X), nullptr); EXPECT_EQ(find_code(absolute_mouse, EV_REL, REL_Y), nullptr); EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_WHEEL), nullptr); EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_HWHEEL), nullptr); #if defined(REL_WHEEL_HI_RES) EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_WHEEL_HI_RES), nullptr); #endif #if defined(REL_HWHEEL_HI_RES) EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr); #endif const auto *mouse_x = find_code(absolute_mouse, EV_ABS, ABS_X); ASSERT_NE(mouse_x, nullptr); EXPECT_TRUE(mouse_x->has_absinfo); EXPECT_EQ(mouse_x->maximum, 65535); const auto touchscreen = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::touchscreen); ASSERT_TRUE(touchscreen.status.ok()) << touchscreen.status.message(); EXPECT_TRUE(has_property(touchscreen, INPUT_PROP_DIRECT)); EXPECT_NE(find_code(touchscreen, EV_KEY, BTN_TOUCH), nullptr); const auto *touch_slot = find_code(touchscreen, EV_ABS, ABS_MT_SLOT); ASSERT_NE(touch_slot, nullptr); EXPECT_TRUE(touch_slot->has_absinfo); EXPECT_EQ(touch_slot->maximum, 15); const auto trackpad = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::trackpad); ASSERT_TRUE(trackpad.status.ok()) << trackpad.status.message(); EXPECT_TRUE(has_property(trackpad, INPUT_PROP_POINTER)); EXPECT_TRUE(has_property(trackpad, INPUT_PROP_BUTTONPAD)); EXPECT_NE(find_code(trackpad, EV_KEY, BTN_LEFT), nullptr); EXPECT_NE(find_code(trackpad, EV_KEY, BTN_TOOL_DOUBLETAP), nullptr); const auto pen_tablet = lvh::detail::test::linux_uinput_create_fake_libevdev_device(lvh::DeviceType::pen_tablet); ASSERT_TRUE(pen_tablet.status.ok()) << pen_tablet.status.message(); EXPECT_TRUE(has_property(pen_tablet, INPUT_PROP_POINTER)); EXPECT_TRUE(has_property(pen_tablet, INPUT_PROP_DIRECT)); EXPECT_NE(find_code(pen_tablet, EV_KEY, BTN_TOOL_PEN), nullptr); const auto *pen_x = find_code(pen_tablet, EV_ABS, ABS_X); ASSERT_NE(pen_x, nullptr); EXPECT_TRUE(pen_x->has_absinfo); EXPECT_EQ(pen_x->maximum, 19200); EXPECT_EQ(pen_x->fuzz, 1); EXPECT_EQ(pen_x->resolution, 28); const auto *pen_pressure = find_code(pen_tablet, EV_ABS, ABS_PRESSURE); ASSERT_NE(pen_pressure, nullptr); EXPECT_EQ(pen_pressure->maximum, 4096); const auto *pen_tilt = find_code(pen_tablet, EV_ABS, ABS_TILT_X); ASSERT_NE(pen_tilt, nullptr); EXPECT_EQ(pen_tilt->minimum, -90); EXPECT_EQ(pen_tilt->maximum, 90); EXPECT_EQ(pen_tilt->resolution, 28); EXPECT_EQ( lvh::detail::test::linux_uinput_create_fake_libevdev_allocation_failure(lvh::DeviceType::mouse).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_create_fake_libevdev_event_type_failure(lvh::DeviceType::keyboard).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_create_fake_libevdev_event_code_failure(lvh::DeviceType::mouse).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_create_fake_libevdev_property_failure(lvh::DeviceType::trackpad).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_create_fake_libevdev_create_failure(lvh::DeviceType::pen_tablet).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_submit_fake_write_failure().code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ(lvh::detail::test::linux_uinput_keyboard_submit_fake_short_write().code(), lvh::ErrorCode::backend_failure); EXPECT_TRUE(lvh::detail::test::linux_uinput_keyboard_type_text_fake_success().ok()); for (const auto fail_call : {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23}) { EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_type_text_fake_write_failure(fail_call).code(), lvh::ErrorCode::backend_failure ); } EXPECT_EQ( lvh::detail::test::linux_uinput_keyboard_close_fake_close_failure().code(), lvh::ErrorCode::backend_failure ); lvh::MouseEvent event; event.kind = lvh::MouseEventKind::relative_motion; event.x = 1; event.y = 1; EXPECT_EQ( lvh::detail::test::linux_uinput_mouse_submit_fake_write_failure(event).code(), lvh::ErrorCode::backend_failure ); EXPECT_EQ( lvh::detail::test::linux_uinput_mouse_submit_fake_short_write(event).code(), lvh::ErrorCode::backend_failure ); } TEST_F(LinuxBackendTest, XTestFallbackCoversKeyboardAndMousePaths) { const auto keyboard_status = lvh::detail::test::linux_xtest_keyboard_submit_success(); EXPECT_TRUE(keyboard_status.ok() || keyboard_status.code() == lvh::ErrorCode::backend_unavailable); const auto compose_status = lvh::detail::test::linux_xtest_keyboard_submit_compose_fallback(); #if defined(LIBVIRTUALHID_HAVE_XTEST) EXPECT_TRUE(compose_status.ok()) << compose_status.message(); #else EXPECT_EQ(compose_status.code(), lvh::ErrorCode::backend_unavailable); #endif const auto keyboard_invalid_status = lvh::detail::test::linux_xtest_keyboard_submit_invalid(); EXPECT_TRUE(keyboard_invalid_status.code() == lvh::ErrorCode::invalid_argument || keyboard_invalid_status.code() == lvh::ErrorCode::backend_unavailable); const auto keyboard_closed_status = lvh::detail::test::linux_xtest_keyboard_submit_closed(); EXPECT_TRUE(keyboard_closed_status.code() == lvh::ErrorCode::device_closed || keyboard_closed_status.code() == lvh::ErrorCode::backend_unavailable); const auto keyboard_text_status = lvh::detail::test::linux_xtest_keyboard_type_text_success(); EXPECT_TRUE(keyboard_text_status.ok() || keyboard_text_status.code() == lvh::ErrorCode::backend_unavailable); EXPECT_EQ( lvh::detail::test::linux_xtest_keyboard_create_query_failure().code(), lvh::ErrorCode::backend_unavailable ); for (const auto fail_call : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) { const auto status = lvh::detail::test::linux_xtest_keyboard_type_text_fake_keycode_failure(fail_call); EXPECT_TRUE(status.code() == lvh::ErrorCode::invalid_argument || status.code() == lvh::ErrorCode::backend_unavailable); } const auto mouse_status = lvh::detail::test::linux_xtest_mouse_submit_success(); EXPECT_TRUE(mouse_status.ok() || mouse_status.code() == lvh::ErrorCode::backend_unavailable); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_create_query_failure().code(), lvh::ErrorCode::backend_unavailable); const auto mouse_closed_status = lvh::detail::test::linux_xtest_mouse_submit_closed(); EXPECT_TRUE(mouse_closed_status.code() == lvh::ErrorCode::device_closed || mouse_closed_status.code() == lvh::ErrorCode::backend_unavailable); #if defined(LIBVIRTUALHID_HAVE_XTEST) EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x08), XK_BackSpace); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x09), XK_Tab); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x0D), XK_Return); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x10), XK_Shift_L); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x11), XK_Control_L); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x12), XK_Alt_L); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x14), XK_Caps_Lock); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x1B), XK_Escape); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x20), XK_space); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x21), XK_Page_Up); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x22), XK_Page_Down); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x23), XK_End); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x24), XK_Home); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x25), XK_Left); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x26), XK_Up); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x27), XK_Right); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x28), XK_Down); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2D), XK_Insert); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x2E), XK_Delete); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5B), XK_Super_L); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5C), XK_Super_R); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5D), XK_Menu); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x90), XK_Num_Lock); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x91), XK_Scroll_Lock); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA1), XK_Shift_R); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA3), XK_Control_R); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xA5), XK_Alt_R); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBA), XK_semicolon); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBB), XK_equal); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBC), XK_comma); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBD), XK_minus); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBE), XK_period); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xBF), XK_slash); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xC0), XK_grave); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDB), XK_bracketleft); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDC), XK_backslash); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDD), XK_bracketright); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0xDE), XK_apostrophe); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x30), XK_0); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x39), XK_9); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41), XK_a); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x5A), XK_z); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x60), XK_KP_0); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x69), XK_KP_9); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6A), XK_KP_Multiply); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6B), XK_KP_Add); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6D), XK_KP_Subtract); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6E), XK_KP_Decimal); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x6F), XK_KP_Divide); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x70), XK_F1); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x87), XK_F24); EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x88), 0UL); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::left), 1); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::middle), 2); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::right), 3); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::side), 8); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::extra), 9); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(static_cast(255)), 1); #else EXPECT_EQ(lvh::detail::test::linux_xtest_keysym(0x41), 0UL); EXPECT_EQ(lvh::detail::test::linux_xtest_mouse_button(lvh::MouseButton::left), 1); #endif } TEST_F(LinuxBackendTest, PlatformRuntimeReportsUnavailableDeviceCreationWhenNodesAreMissing) { lvh::RuntimeOptions options; options.backend = lvh::BackendKind::platform_default; auto runtime = lvh::Runtime::create(options); if (!runtime->capabilities().supports_gamepad) { auto gamepad = runtime->create_gamepad(lvh::profiles::xbox_360()); EXPECT_FALSE(gamepad); EXPECT_EQ(gamepad.status.code(), lvh::ErrorCode::backend_unavailable); } if (!runtime->capabilities().supports_keyboard) { auto keyboard = runtime->create_keyboard(); EXPECT_FALSE(keyboard); EXPECT_EQ(keyboard.status.code(), lvh::ErrorCode::backend_unavailable); } if (!runtime->capabilities().supports_mouse) { auto mouse = runtime->create_mouse(); EXPECT_FALSE(mouse); EXPECT_EQ(mouse.status.code(), lvh::ErrorCode::backend_unavailable); } }