Files

488 lines
20 KiB
C++

/**
* @file tests/unit/test_runtime.cpp
* @brief Unit tests for runtime and virtual gamepad handles.
*/
// lib includes
#include <libvirtualhid/libvirtualhid.hpp>
// standard includes
#include <algorithm>
#include <string>
#include <string_view>
#include <vector>
// local includes
#include "fixtures/fixtures.hpp"
#if defined(_WIN32)
#include "platform/windows/shared/lvh_windows_protocol.h"
#endif
/**
* @brief Test fixture for Linux runtime integration tests.
*/
class LinuxRuntimeTest: public LinuxTest {};
TEST(RuntimeTest, FakeBackendReportsCapabilities) {
auto runtime = lvh::Runtime::create();
EXPECT_EQ(runtime->backend_kind(), lvh::BackendKind::fake);
EXPECT_EQ(runtime->capabilities().backend_name, "fake");
EXPECT_TRUE(runtime->capabilities().supports_gamepad);
EXPECT_TRUE(runtime->capabilities().supports_keyboard);
EXPECT_TRUE(runtime->capabilities().supports_mouse);
EXPECT_TRUE(runtime->capabilities().supports_touchscreen);
EXPECT_TRUE(runtime->capabilities().supports_trackpad);
EXPECT_TRUE(runtime->capabilities().supports_pen_tablet);
EXPECT_TRUE(runtime->capabilities().supports_output_reports);
EXPECT_FALSE(runtime->capabilities().requires_installed_driver);
}
TEST(RuntimeTest, RoutesDiagnosticsToConsumerCallback) {
struct Diagnostic {
lvh::LogLevel level;
std::string message;
};
std::vector<Diagnostic> diagnostics;
lvh::RuntimeOptions runtime_options;
runtime_options.log_callback = [&diagnostics](lvh::LogLevel level, const std::string &message) {
diagnostics.emplace_back(level, message);
};
auto runtime = lvh::Runtime::create(runtime_options);
lvh::CreateMouseOptions mouse_options;
mouse_options.profile = lvh::profiles::mouse();
mouse_options.desktop = {.offset_x = -1920, .offset_y = 0, .width = 3840, .height = 1080};
mouse_options.viewport = {.offset_x = -1920, .offset_y = 0, .width = 1920, .height = 1080};
auto created = runtime->create_mouse(mouse_options);
ASSERT_TRUE(created) << created.status.message();
EXPECT_TRUE(created.mouse->move_relative(1, -1).ok());
EXPECT_TRUE(created.mouse->move_absolute(960, 540, 1920, 1080).ok());
EXPECT_TRUE(created.mouse->move_absolute(0.5F, 0.25F, 1, 1).ok());
EXPECT_TRUE(created.mouse->button(lvh::MouseButton::left, true).ok());
EXPECT_TRUE(created.mouse->vertical_scroll(120).ok());
EXPECT_TRUE(created.mouse->horizontal_scroll(-120).ok());
EXPECT_TRUE(created.mouse->close().ok());
EXPECT_EQ(created.mouse->move_relative(1, 1).code(), lvh::ErrorCode::device_closed);
const auto has_message = [&diagnostics](lvh::LogLevel level, std::string_view text) {
return std::ranges::any_of(diagnostics, [level, text](const auto &diagnostic) {
return diagnostic.level == level && diagnostic.message.contains(text);
});
};
EXPECT_TRUE(has_message(lvh::LogLevel::info, "initialized fake backend"));
EXPECT_TRUE(has_message(lvh::LogLevel::info, "created mouse 1"));
EXPECT_TRUE(has_message(lvh::LogLevel::debug, "viewport=-1920,0 1920x1080"));
EXPECT_TRUE(has_message(lvh::LogLevel::error, "mouse input failed: mouse is closed"));
}
TEST(RuntimeTest, DisablesConsumerDiagnosticsAfterException) {
struct ConsumerLoggerFailure {};
std::size_t callback_count = 0;
lvh::RuntimeOptions options;
options.log_callback = [&callback_count](lvh::LogLevel, const std::string &) {
++callback_count;
throw ConsumerLoggerFailure {};
};
EXPECT_NO_THROW({
auto runtime = lvh::Runtime::create(options);
auto created = runtime->create_mouse();
ASSERT_TRUE(created);
EXPECT_TRUE(created.mouse->move_relative(1, 1).ok());
});
EXPECT_EQ(callback_count, 1U);
}
TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) {
lvh::RuntimeOptions options;
options.backend = lvh::BackendKind::platform_default;
auto runtime = lvh::Runtime::create(options);
EXPECT_EQ(runtime->backend_kind(), lvh::BackendKind::platform_default);
#if defined(__linux__)
EXPECT_EQ(runtime->capabilities().backend_name, "linux-uhid-uinput");
EXPECT_FALSE(runtime->capabilities().requires_installed_driver);
#elif defined(__FreeBSD__)
EXPECT_EQ(runtime->capabilities().backend_name, "freebsd-uinput");
EXPECT_FALSE(runtime->capabilities().requires_installed_driver);
EXPECT_TRUE(runtime->capabilities().supports_gamepad);
auto created = runtime->create_gamepad(lvh::profiles::xbox_360());
ASSERT_TRUE(created) << created.status.message();
EXPECT_TRUE(created.gamepad->close().ok());
#elif defined(_WIN32)
EXPECT_EQ(runtime->capabilities().backend_name, "windows-umdf");
EXPECT_TRUE(runtime->capabilities().requires_installed_driver);
EXPECT_TRUE(runtime->capabilities().supports_keyboard);
EXPECT_TRUE(runtime->capabilities().supports_mouse);
EXPECT_FALSE(runtime->capabilities().supports_trackpad);
auto keyboard = runtime->create_keyboard();
ASSERT_TRUE(keyboard) << keyboard.status.message();
EXPECT_TRUE(keyboard.keyboard->close().ok());
auto mouse = runtime->create_mouse();
ASSERT_TRUE(mouse) << mouse.status.message();
EXPECT_TRUE(mouse.mouse->close().ok());
auto touchscreen = runtime->create_touchscreen();
if (runtime->capabilities().supports_touchscreen) {
ASSERT_TRUE(touchscreen) << touchscreen.status.message();
EXPECT_TRUE(touchscreen.touchscreen->close().ok());
} else {
EXPECT_FALSE(touchscreen);
}
EXPECT_EQ(runtime->create_trackpad().status.code(), lvh::ErrorCode::unsupported_profile);
auto pen_tablet = runtime->create_pen_tablet();
if (runtime->capabilities().supports_pen_tablet) {
ASSERT_TRUE(pen_tablet) << pen_tablet.status.message();
EXPECT_TRUE(pen_tablet.pen_tablet->close().ok());
} else {
EXPECT_FALSE(pen_tablet);
}
if (!runtime->capabilities().supports_gamepad) {
auto created = runtime->create_gamepad(lvh::profiles::xbox_360());
EXPECT_FALSE(created);
EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable);
} else {
auto xbox_360 = runtime->create_gamepad(lvh::profiles::xbox_360());
ASSERT_TRUE(xbox_360) << xbox_360.status.message();
ASSERT_NE(xbox_360.gamepad, nullptr);
EXPECT_FALSE(xbox_360.gamepad->device_nodes().empty());
EXPECT_TRUE(xbox_360.gamepad->close().ok());
auto created = runtime->create_gamepad(lvh::profiles::xbox_series());
ASSERT_TRUE(created) << created.status.message();
ASSERT_NE(created.gamepad, nullptr);
EXPECT_FALSE(created.gamepad->device_nodes().empty());
lvh::GamepadState state;
state.buttons.set(lvh::GamepadButton::a);
state.left_stick.x = 0.25F;
state.right_trigger = 1.0F;
EXPECT_TRUE(created.gamepad->submit(state).ok());
EXPECT_TRUE(created.gamepad->close().ok());
EXPECT_TRUE(created.gamepad->close().ok());
EXPECT_EQ(created.gamepad->submit(state).code(), lvh::ErrorCode::device_closed);
}
auto invalid_profile = lvh::profiles::xbox_series();
invalid_profile.report_descriptor.resize(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 1U);
EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument);
invalid_profile = lvh::profiles::xbox_series();
invalid_profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U;
EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument);
invalid_profile = lvh::profiles::xbox_series();
invalid_profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U;
EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument);
#elif defined(__APPLE__) && defined(__MACH__)
EXPECT_EQ(runtime->capabilities().backend_name, "macos-virtual-hid-coregraphics");
EXPECT_FALSE(runtime->capabilities().requires_installed_driver);
EXPECT_TRUE(runtime->capabilities().supports_virtual_hid);
EXPECT_TRUE(runtime->capabilities().supports_gamepad);
EXPECT_TRUE(runtime->capabilities().supports_keyboard);
EXPECT_TRUE(runtime->capabilities().supports_mouse);
EXPECT_FALSE(runtime->capabilities().supports_touchscreen);
EXPECT_FALSE(runtime->capabilities().supports_trackpad);
EXPECT_FALSE(runtime->capabilities().supports_pen_tablet);
EXPECT_TRUE(runtime->capabilities().supports_output_reports);
auto keyboard = runtime->create_keyboard();
ASSERT_TRUE(keyboard) << keyboard.status.message();
EXPECT_TRUE(keyboard.keyboard->type_text({.text = "A"}).ok());
EXPECT_TRUE(keyboard.keyboard->close().ok());
auto mouse = runtime->create_mouse();
ASSERT_TRUE(mouse) << mouse.status.message();
EXPECT_TRUE(mouse.mouse->close().ok());
EXPECT_EQ(runtime->create_touchscreen().status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(runtime->create_trackpad().status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(runtime->create_pen_tablet().status.code(), lvh::ErrorCode::unsupported_profile);
#else
EXPECT_EQ(runtime->capabilities().backend_name, "platform-default-unimplemented");
EXPECT_FALSE(runtime->capabilities().supports_gamepad);
auto created = runtime->create_gamepad(lvh::profiles::xbox_360());
EXPECT_FALSE(created);
EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable);
#endif
}
TEST(RuntimeTest, CreatesSubmitsAndClosesGamepad) {
auto runtime = lvh::Runtime::create();
auto created = runtime->create_gamepad(lvh::profiles::xbox_360());
ASSERT_TRUE(created);
ASSERT_NE(created.gamepad, nullptr);
EXPECT_TRUE(created.gamepad->is_open());
EXPECT_TRUE(created.gamepad->device_nodes().empty());
EXPECT_EQ(runtime->active_device_count(), 1U);
lvh::GamepadState state;
state.buttons.set(lvh::GamepadButton::b);
state.left_stick.x = 2.0F;
state.left_trigger = 2.0F;
EXPECT_TRUE(created.gamepad->submit(state).ok());
EXPECT_EQ(created.gamepad->submit_count(), 1U);
EXPECT_EQ(created.gamepad->last_submitted_state().left_stick.x, 1.0F);
EXPECT_EQ(created.gamepad->last_submitted_state().left_trigger, 1.0F);
EXPECT_FALSE(created.gamepad->last_input_report().empty());
EXPECT_TRUE(created.gamepad->close().ok());
EXPECT_FALSE(created.gamepad->is_open());
EXPECT_EQ(runtime->active_device_count(), 0U);
EXPECT_EQ(created.gamepad->submit(state).code(), lvh::ErrorCode::device_closed);
}
TEST(RuntimeTest, DispatchesOutputCallback) {
auto runtime = lvh::Runtime::create();
auto created = runtime->create_gamepad(lvh::profiles::dualsense());
ASSERT_TRUE(created);
lvh::GamepadOutput received;
bool was_called = false;
created.gamepad->set_output_callback([&received, &was_called](const lvh::GamepadOutput &output) {
received = output;
was_called = true;
});
lvh::GamepadOutput output;
output.kind = lvh::GamepadOutputKind::rumble;
output.low_frequency_rumble = 123;
output.high_frequency_rumble = 456;
EXPECT_TRUE(created.gamepad->dispatch_output(output).ok());
EXPECT_TRUE(was_called);
EXPECT_EQ(received.kind, lvh::GamepadOutputKind::rumble);
EXPECT_EQ(received.low_frequency_rumble, 123);
EXPECT_EQ(received.high_frequency_rumble, 456);
}
TEST(RuntimeTest, CreatesSubmitsAndClosesKeyboard) {
auto runtime = lvh::Runtime::create();
auto created = runtime->create_keyboard();
ASSERT_TRUE(created);
ASSERT_NE(created.keyboard, nullptr);
EXPECT_TRUE(created.keyboard->is_open());
EXPECT_TRUE(created.keyboard->device_nodes().empty());
EXPECT_EQ(created.keyboard->profile().device_type, lvh::DeviceType::keyboard);
EXPECT_EQ(runtime->active_device_count(), 1U);
EXPECT_TRUE(created.keyboard->press(0x41).ok());
EXPECT_EQ(created.keyboard->submit_count(), 1U);
EXPECT_EQ(created.keyboard->last_submitted_event().key_code, 0x41);
EXPECT_TRUE(created.keyboard->last_submitted_event().pressed);
EXPECT_TRUE(created.keyboard->release(0x41).ok());
EXPECT_TRUE(created.keyboard->type_text({.text = "A"}).ok());
EXPECT_EQ(created.keyboard->submit_count(), 3U);
EXPECT_EQ(created.keyboard->press(0).code(), lvh::ErrorCode::invalid_argument);
EXPECT_TRUE(created.keyboard->close().ok());
EXPECT_FALSE(created.keyboard->is_open());
EXPECT_EQ(runtime->active_device_count(), 0U);
EXPECT_EQ(created.keyboard->press(0x41).code(), lvh::ErrorCode::device_closed);
}
TEST(RuntimeTest, CreatesSubmitsAndClosesMouse) {
auto runtime = lvh::Runtime::create();
auto created = runtime->create_mouse();
ASSERT_TRUE(created);
ASSERT_NE(created.mouse, nullptr);
EXPECT_TRUE(created.mouse->is_open());
EXPECT_TRUE(created.mouse->device_nodes().empty());
EXPECT_EQ(created.mouse->profile().device_type, lvh::DeviceType::mouse);
EXPECT_EQ(runtime->active_device_count(), 1U);
EXPECT_TRUE(created.mouse->move_relative(10, -5).ok());
EXPECT_EQ(created.mouse->submit_count(), 1U);
EXPECT_EQ(created.mouse->last_submitted_event().kind, lvh::MouseEventKind::relative_motion);
EXPECT_EQ(created.mouse->last_submitted_event().x, 10);
EXPECT_EQ(created.mouse->last_submitted_event().y, -5);
EXPECT_TRUE(created.mouse->move_absolute(100, 200, 1920, 1080).ok());
EXPECT_EQ(created.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion);
EXPECT_EQ(created.mouse->last_submitted_event().x, 100);
EXPECT_EQ(created.mouse->last_submitted_event().y, 200);
EXPECT_EQ(created.mouse->last_submitted_event().width, 1920);
EXPECT_EQ(created.mouse->last_submitted_event().height, 1080);
EXPECT_TRUE(created.mouse->move_absolute(0.25F, 0.75F, 1, 1).ok());
EXPECT_EQ(created.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion);
EXPECT_TRUE(created.mouse->last_submitted_event().has_fractional_absolute_coordinates);
EXPECT_FLOAT_EQ(created.mouse->last_submitted_event().absolute_x, 0.25F);
EXPECT_FLOAT_EQ(created.mouse->last_submitted_event().absolute_y, 0.75F);
EXPECT_TRUE(created.mouse->button(lvh::MouseButton::right, true).ok());
EXPECT_TRUE(created.mouse->vertical_scroll(120).ok());
EXPECT_TRUE(created.mouse->horizontal_scroll(-120).ok());
EXPECT_EQ(created.mouse->submit_count(), 6U);
EXPECT_EQ(created.mouse->move_absolute(1, 1, 0, 0).code(), lvh::ErrorCode::invalid_argument);
EXPECT_TRUE(created.mouse->close().ok());
EXPECT_FALSE(created.mouse->is_open());
EXPECT_EQ(runtime->active_device_count(), 0U);
EXPECT_EQ(created.mouse->move_relative(1, 1).code(), lvh::ErrorCode::device_closed);
}
TEST(RuntimeTest, RejectsIncompleteMouseViewport) {
auto runtime = lvh::Runtime::create();
const auto expect_invalid = [&runtime](lvh::PointerViewport desktop, lvh::PointerViewport viewport) {
lvh::CreateMouseOptions options;
options.profile = lvh::profiles::mouse();
options.desktop = desktop;
options.viewport = viewport;
const auto created = runtime->create_mouse(options);
EXPECT_FALSE(created);
EXPECT_EQ(created.status.code(), lvh::ErrorCode::invalid_argument);
};
expect_invalid({}, {.width = 1920});
expect_invalid({}, {.width = 1920, .height = 1080});
expect_invalid({.width = 3840, .height = 1080}, {});
expect_invalid({.width = -1, .height = 1080}, {});
expect_invalid(
{.offset_x = -1920, .width = 3840, .height = 1080},
{.offset_x = 0, .offset_y = 1080, .width = 1920, .height = 1080}
);
}
TEST(RuntimeTest, CreatesSubmitsAndClosesTouchDevices) {
auto runtime = lvh::Runtime::create();
auto touchscreen = runtime->create_touchscreen();
ASSERT_TRUE(touchscreen);
ASSERT_NE(touchscreen.touchscreen, nullptr);
EXPECT_EQ(touchscreen.touchscreen->profile().device_type, lvh::DeviceType::touchscreen);
lvh::TouchContact contact {
.id = 1,
.x = 0.5F,
.y = 0.25F,
.pressure = 1.0F,
.orientation = 10,
};
EXPECT_TRUE(touchscreen.touchscreen->place_contact(contact).ok());
EXPECT_TRUE(touchscreen.touchscreen->release_contact(contact.id).ok());
EXPECT_EQ(touchscreen.touchscreen->submit_count(), 2U);
EXPECT_TRUE(touchscreen.touchscreen->close().ok());
EXPECT_EQ(touchscreen.touchscreen->place_contact(contact).code(), lvh::ErrorCode::device_closed);
auto trackpad = runtime->create_trackpad();
ASSERT_TRUE(trackpad);
ASSERT_NE(trackpad.trackpad, nullptr);
EXPECT_EQ(trackpad.trackpad->profile().device_type, lvh::DeviceType::trackpad);
EXPECT_TRUE(trackpad.trackpad->place_contact(contact).ok());
EXPECT_TRUE(trackpad.trackpad->button(true).ok());
EXPECT_TRUE(trackpad.trackpad->release_contact(contact.id).ok());
EXPECT_EQ(trackpad.trackpad->submit_count(), 3U);
EXPECT_TRUE(trackpad.trackpad->close().ok());
EXPECT_EQ(trackpad.trackpad->button(false).code(), lvh::ErrorCode::device_closed);
}
TEST(RuntimeTest, CreatesSubmitsAndClosesPenTablet) {
auto runtime = lvh::Runtime::create();
auto created = runtime->create_pen_tablet();
ASSERT_TRUE(created);
ASSERT_NE(created.pen_tablet, nullptr);
EXPECT_EQ(created.pen_tablet->profile().device_type, lvh::DeviceType::pen_tablet);
lvh::PenToolState tool {
.tool = lvh::PenToolType::pen,
.x = 0.25F,
.y = 0.5F,
.pressure = 0.75F,
.distance = -1.0F,
.tilt_x = 45.0F,
.tilt_y = -45.0F,
};
EXPECT_TRUE(created.pen_tablet->place_tool(tool).ok());
EXPECT_TRUE(created.pen_tablet->button(lvh::PenButton::primary, true).ok());
EXPECT_EQ(created.pen_tablet->submit_count(), 2U);
EXPECT_EQ(created.pen_tablet->last_submitted_tool().tool, lvh::PenToolType::pen);
EXPECT_TRUE(created.pen_tablet->close().ok());
EXPECT_EQ(created.pen_tablet->button(lvh::PenButton::primary, false).code(), lvh::ErrorCode::device_closed);
}
TEST_F(LinuxRuntimeTest, LinuxUhidSmokeTestRequiresPrerequisites) {
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid"));
lvh::RuntimeOptions options;
options.backend = lvh::BackendKind::platform_default;
auto runtime = lvh::Runtime::create(options);
ASSERT_TRUE(runtime->capabilities().supports_gamepad);
auto created = runtime->create_gamepad(lvh::profiles::xbox_360());
ASSERT_TRUE(created) << created.status.message();
lvh::GamepadState state;
state.buttons.set(lvh::GamepadButton::a);
state.left_stick.x = 0.25F;
state.right_trigger = 1.0F;
EXPECT_TRUE(created.gamepad->submit(state).ok());
EXPECT_TRUE(created.gamepad->close().ok());
}
TEST_F(LinuxRuntimeTest, LinuxUinputSmokeTestRequiresPrerequisites) {
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uinput"));
lvh::RuntimeOptions options;
options.backend = lvh::BackendKind::platform_default;
auto runtime = lvh::Runtime::create(options);
const auto &capabilities = runtime->capabilities();
ASSERT_TRUE(capabilities.supports_keyboard);
auto keyboard = runtime->create_keyboard();
ASSERT_TRUE(keyboard) << keyboard.status.message();
EXPECT_TRUE(keyboard.keyboard->press(0x41).ok());
EXPECT_TRUE(keyboard.keyboard->release(0x41).ok());
ASSERT_TRUE(capabilities.supports_mouse);
auto mouse = runtime->create_mouse();
ASSERT_TRUE(mouse) << mouse.status.message();
EXPECT_TRUE(mouse.mouse->move_relative(1, 1).ok());
EXPECT_TRUE(mouse.mouse->vertical_scroll(120).ok());
ASSERT_TRUE(capabilities.supports_touchscreen);
auto touchscreen = runtime->create_touchscreen();
ASSERT_TRUE(touchscreen) << touchscreen.status.message();
EXPECT_TRUE(touchscreen.touchscreen->place_contact({.id = 1, .x = 0.5F, .y = 0.25F, .pressure = 1.0F}).ok());
EXPECT_TRUE(touchscreen.touchscreen->release_contact(1).ok());
ASSERT_TRUE(capabilities.supports_trackpad);
auto trackpad = runtime->create_trackpad();
ASSERT_TRUE(trackpad) << trackpad.status.message();
EXPECT_TRUE(trackpad.trackpad->place_contact({.id = 1, .x = 0.5F, .y = 0.25F, .pressure = 1.0F}).ok());
EXPECT_TRUE(trackpad.trackpad->button(true).ok());
EXPECT_TRUE(trackpad.trackpad->button(false).ok());
EXPECT_TRUE(trackpad.trackpad->release_contact(1).ok());
ASSERT_TRUE(capabilities.supports_pen_tablet);
auto pen_tablet = runtime->create_pen_tablet();
ASSERT_TRUE(pen_tablet) << pen_tablet.status.message();
EXPECT_TRUE(
pen_tablet.pen_tablet
->place_tool({.tool = lvh::PenToolType::pen, .x = 0.5F, .y = 0.25F, .pressure = 0.75F, .tilt_x = 10.0F})
.ok()
);
EXPECT_TRUE(pen_tablet.pen_tablet->button(lvh::PenButton::primary, true).ok());
EXPECT_TRUE(pen_tablet.pen_tablet->button(lvh::PenButton::primary, false).ok());
}