spiegel-keyman/linux/ibus-keyman/tests/testfixture.cpp
Eberhard Beilharz 37e20c5cbb
fix(linux): Fix ibus-keyman integration tests
Previously we overwrote our test data, so we basically ran the same
test repeatedly under different names.

Properly running them showed some failures which this change also
fixes or skips.
2021-11-24 15:59:44 +01:00

424 lines
13 KiB
C++

#include <glib-object.h>
#include <glib.h>
#include <ibus.h>
#include <iostream>
#include <keyman/keyboardprocessor.h>
#include <kmx/kmx_processevent.h>
#include <linux/input-event-codes.h>
#include <list>
#include <memory>
#include <stack>
#include <stdexcept>
#include <string>
#include "ibusimcontext.h"
#include "keycodes.h"
#include "keymanutil.h"
#include "kmx_test_source.hpp"
#include "testmodule.h"
#ifdef GDK_WINDOWING_X11
#include <X11/XKBlib.h>
#include <gdk/gdkx.h>
#endif
typedef struct {
IBusBus *bus;
GtkIMContext *context;
IBusIMContext *ibuscontext;
} IBusKeymanTestsFixture;
typedef struct {
char *test_name;
char *test_path;
char *skip_reason;
} TestData;
static gboolean loaded = FALSE;
static GdkWindow *window = NULL;
static GMainLoop *thread_loop = NULL;
static GTypeModule *module = NULL;
static Display *display = NULL;
static void
module_register(GTypeModule *module) {
ibus_im_context_register_type(module);
}
void
destroy(GtkWidget *widget, gpointer data) {
gtk_main_quit();
}
static void
ibus_keyman_tests_fixture_set_up(IBusKeymanTestsFixture *fixture, gconstpointer user_data) {
IBusIMContextClass *classClass;
if (!window) {
GtkWidget *widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
g_signal_connect(widget, "destroy", G_CALLBACK(destroy), NULL);
window = gtk_widget_get_window(widget);
}
if (!display) {
#ifdef GDK_WINDOWING_X11
if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) {
display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
}
#endif
}
if (!loaded) {
ibus_init();
module = test_module_new(module_register);
g_assert_nonnull(module);
/* Not loaded until we call ref for the first time */
classClass = static_cast<IBusIMContextClass *>(g_type_class_peek(IBUS_TYPE_IM_CONTEXT));
g_assert_null(classClass);
loaded = TRUE;
}
fixture->bus = ibus_bus_new();
}
static void
ibus_keyman_tests_fixture_tear_down(IBusKeymanTestsFixture *fixture, gconstpointer user_data) {
if (thread_loop) {
g_main_loop_unref(thread_loop);
thread_loop = NULL;
}
g_clear_object(&fixture->bus);
g_clear_object(&fixture->context);
}
static void
set_caps_lock_state(IBusKeymanTestsFixture *fixture, bool caps_lock_on) {
#ifdef GDK_WINDOWING_X11
if (display) {
XkbLockModifiers(display, XkbUseCoreKbd, LockMask, caps_lock_on ? LockMask : 0);
XSync(display, False);
}
#endif
}
static bool
get_caps_lock_state(IBusKeymanTestsFixture *fixture) {
#ifdef GDK_WINDOWING_X11
if (display) {
XKeyboardState state;
XGetKeyboardControl(display, &state);
return state.led_mask & 1;
}
#endif
return false;
}
static void
switch_keyboard(IBusKeymanTestsFixture *fixture, const gchar *keyboard) {
ibus_bus_set_global_engine(fixture->bus, keyboard);
IBusEngineDesc *desc = ibus_bus_get_global_engine(fixture->bus);
g_object_ref_sink(desc);
// g_debug("Switched to engine: '%s'", ibus_engine_desc_get_name(desc));
g_assert_cmpstr(keyboard, ==, ibus_engine_desc_get_name(desc));
g_clear_object(&desc);
if (thread_loop) {
g_main_loop_unref(thread_loop);
thread_loop = NULL;
}
if (fixture->context) {
g_clear_object(&fixture->context);
}
fixture->ibuscontext = ibus_im_context_new();
fixture->context = (GtkIMContext *)fixture->ibuscontext;
thread_loop = g_main_loop_new(NULL, TRUE);
ibus_im_test_set_thread_loop(fixture->ibuscontext, thread_loop);
}
template <typename... Args>
std::string
string_format(const std::string &format, Args... args) {
// from https://stackoverflow.com/a/26221725/487503
int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; // Extra space for '\0'
if (size_s <= 0) {
throw std::runtime_error("Error during formatting.");
}
auto size = static_cast<size_t>(size_s);
auto buf = std::make_unique<char[]>(size);
std::snprintf(buf.get(), size, format.c_str(), args...);
return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside
}
static unsigned short vk_to_keycode(unsigned short vk) {
for (int i = 0; i < sizeof(keycode_to_vk); i++) {
if (keycode_to_vk[i] == vk)
return i;
}
return -1;
}
#define KEYMAN_LCTRL 29 // 0x1D
#define KEYMAN_LALT 56 // 0x38
#define KEYMAN_RCTRL 97 // 0x61
#define KEYMAN_RALT 100 // 0x64
typedef struct {
guint modifiers_keydown;
guint modifiers_keyup;
guint keyval;
guint16 keycode;
guint is_modifier;
} gdk_key_event;
static gdk_key_event
get_key_event_for_modifier(guint modifier, guint & prev_modifiers, guint keyval, guint keycode) {
gdk_key_event event;
event.modifiers_keydown = prev_modifiers;
event.modifiers_keyup = prev_modifiers | modifier | IBUS_RELEASE_MASK;
prev_modifiers |= modifier;
event.keyval = keyval;
event.keycode = keycode;
event.is_modifier = 1;
return event;
}
static std::list<gdk_key_event>
get_involved_keys(IBusKeymanTestsFixture *fixture, km::tests::key_event test_event) {
guint modifiers = 0;
if (get_caps_lock_state(fixture)) {
modifiers = IBUS_LOCK_MASK;
}
// process all modifiers
std::list<gdk_key_event> result;
if (test_event.modifier_state & KM_KBP_MODIFIER_SHIFT) {
// we don't distinguish between R and L Shift
result.push_back(get_key_event_for_modifier(IBUS_SHIFT_MASK, modifiers, GDK_KEY_Shift_L, KEY_LEFTSHIFT));
}
if (test_event.modifier_state & KM_KBP_MODIFIER_ALT || test_event.modifier_state & KM_KBP_MODIFIER_LALT) {
result.push_back(get_key_event_for_modifier(IBUS_MOD1_MASK, modifiers, GDK_KEY_Alt_L, KEY_LEFTALT));
}
if (test_event.modifier_state & KM_KBP_MODIFIER_RALT) {
if (test_event.vk == KEYMAN_RALT)
result.push_back(get_key_event_for_modifier(IBUS_MOD1_MASK, modifiers, GDK_KEY_Alt_R, KEY_RIGHTALT));
else
result.push_back(get_key_event_for_modifier(IBUS_MOD5_MASK, modifiers, GDK_KEY_Alt_R, KEY_RIGHTALT));
}
if (test_event.modifier_state & KM_KBP_MODIFIER_CTRL || test_event.modifier_state & KM_KBP_MODIFIER_LCTRL) {
result.push_back(get_key_event_for_modifier(IBUS_CONTROL_MASK, modifiers, GDK_KEY_Control_L, KEY_LEFTCTRL));
}
if (test_event.modifier_state & KM_KBP_MODIFIER_RCTRL) {
result.push_back(get_key_event_for_modifier(IBUS_CONTROL_MASK, modifiers, GDK_KEY_Control_R, KEY_RIGHTCTRL));
}
// process key
guint keyval = (test_event.modifier_state & KM_KBP_MODIFIER_SHIFT) ? gdk_keyval_to_upper(test_event.vk)
: gdk_keyval_to_lower(test_event.vk);
gdk_key_event event;
event.modifiers_keydown = modifiers;
event.modifiers_keyup = modifiers | IBUS_RELEASE_MASK;
event.keyval = keyval;
event.keycode = vk_to_keycode(test_event.vk);
event.is_modifier = 0;
result.push_back(event);
return result;
}
static std::list<km::tests::key_event>
get_context_keys(std::u16string context) {
std::list<km::tests::key_event> result;
for (auto c = context.begin(); c != context.end(); c++) {
gchar utf8[6];
if (g_unichar_to_utf8((gunichar)*c, utf8) != 1 || utf8[0] < 32 || utf8[0] > 127) {
g_assertion_message_cmpstr(
G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, "We can only deal with ASCII characters in the context", utf8, "", "");
}
result.push_back(
{km::kbp::kmx::s_char_to_vkey[(int)utf8[0] - 32].vk,
(uint16_t)(km::kbp::kmx::s_char_to_vkey[(int)utf8[0] - 32].shifted ? KM_KBP_MODIFIER_SHIFT : 0)});
}
return result;
}
static void
press_keys(IBusKeymanTestsFixture *fixture, km::tests::KmxTestSource & test_source,km::tests::key_event key_event) {
auto involved_keys = get_involved_keys(fixture, key_event);
// press and hold the individual modifier keys, finally the actual key
std::stack<GdkEventKey> keys_to_release;
for (auto key = involved_keys.begin(); key != involved_keys.end(); key++) {
bool old_caps_lock = get_caps_lock_state(fixture);
if (key->keycode == KEY_CAPSLOCK && !old_caps_lock) {
set_caps_lock_state(fixture, true);
}
GdkEventKey keyEvent = {
.type = GDK_KEY_PRESS,
.window = window,
.send_event = 0,
.time = 0,
.state = key->modifiers_keydown,
.keyval = key->keyval,
.length = 0,
.string = NULL,
.hardware_keycode = key->keycode,
.group = 0,
.is_modifier = key->is_modifier};
gtk_im_context_filter_keypress(fixture->context, &keyEvent);
keyEvent.type = GDK_KEY_RELEASE;
keyEvent.state = key->modifiers_keyup;
keys_to_release.push(keyEvent);
if (key->keycode == KEY_CAPSLOCK && old_caps_lock) {
set_caps_lock_state(fixture, false);
}
}
// then release the keys in reverse order
while (!keys_to_release.empty()) {
auto keyEvent = keys_to_release.top();
gtk_im_context_filter_keypress(fixture->context, &keyEvent);
keys_to_release.pop();
}
}
static void test_source(IBusKeymanTestsFixture *fixture, gconstpointer user_data) {
auto data = (TestData*)user_data;
auto sourcefile = string_format("%s.kmn", data->test_path);
auto kmxfile = string_format("und:%s.kmx", data->test_path);
km::tests::KmxTestSource test_source;
std::string keys = "";
std::u16string expected = u"", context = u"";
km::tests::kmx_options options;
bool expected_beep = false;
g_assert_cmpint(test_source.load_source(sourcefile.c_str(), keys, expected, context, options, expected_beep), ==, 0);
for (auto & option : options) {
if (option.type == km::tests::KOT_INPUT) {
auto key = g_utf16_to_utf8((gunichar2 *)option.key.c_str(), option.key.length(), NULL, NULL, NULL);
auto value = g_utf16_to_utf8((gunichar2 *)option.value.c_str(), option.value.length(), NULL, NULL, NULL);
keyman_put_options_todconf(data->test_name, data->test_name, key, value);
}
}
switch_keyboard(fixture, kmxfile.c_str());
if (test_source.caps_lock_state() != get_caps_lock_state(fixture)) {
set_caps_lock_state(fixture, test_source.caps_lock_state());
}
auto contextKeys = get_context_keys(context);
for (auto k = contextKeys.begin(); k != contextKeys.end(); k++) {
press_keys(fixture, test_source, *k);
}
gtk_im_context_focus_in(fixture->context);
for (auto p = test_source.next_key(keys); p.vk != 0; p = test_source.next_key(keys)) {
press_keys(fixture, test_source, p);
}
if (expected.length() == 0) {
g_assert_null(ibus_im_test_get_text(fixture->ibuscontext));
} else {
auto expectedText = g_utf16_to_utf8((gunichar2*)expected.c_str(), expected.length(), NULL, NULL, NULL);
g_assert_cmpstr(ibus_im_test_get_text(fixture->ibuscontext), ==, expectedText);
}
// Cleanup
g_free(data->test_name);
g_free(data->test_path);
delete data;
}
static void
test_skip(IBusKeymanTestsFixture *fixture, gconstpointer user_data) {
auto data = (TestData *)user_data;
g_test_skip(data->skip_reason);
}
void
print_usage() {
printf("Usage: %s --directory <keyboarddir> test1 [test2 ...]", g_get_prgname());
}
int
main(int argc, char *argv[]) {
gtk_init(&argc, &argv);
g_test_init(&argc, &argv, NULL);
g_test_set_nonfatal_assertions();
if (argc < 4) {
print_usage();
return 1;
}
if (strcmp(argv[1], "--directory") != 0) {
print_usage();
return 2;
}
char *directory = argv[2];
int nTests = argc - 3;
char **tests = &argv[3];
// Add tests
for (int i = 0; i < nTests; i++)
{
auto filename = g_string_new(tests[i]);
if (strstr(filename->str, ".kmx") || strstr(filename->str, ".kmn")) {
g_string_truncate(filename, filename->len - 4);
}
auto testdata = new TestData();
auto file = g_file_new_for_commandline_arg(filename->str);
auto testfilebase = g_file_get_basename(file);
auto testname = g_string_new(NULL);
g_string_append_printf(testname, "/%s", testfilebase);
auto testfile = g_file_new_build_filename(directory, testfilebase, NULL);
testdata->test_name = strdup(filename->str);
testdata->test_path = g_file_get_parse_name(testfile);
// Check for tests to skip - #3345
gboolean skip = FALSE;
char *skipReason = NULL;
if (strcmp(filename->str, "k_026___system_stores") == 0 || strcmp(filename->str, "k_027___system_stores_2") == 0) {
skip = TRUE;
testdata->skip_reason = "mnemonic keyboards are not yet supported on Linux (#3345)";
} else if (
strcmp(filename->str, "k_041___long_context_and_deadkeys") == 0 ||
strcmp(filename->str, "k_042___long_context_and_split_deadkeys") == 0) {
skip = TRUE;
testdata->skip_reason = "can't test context with deadkey";
}
g_test_add(
testname->str, IBusKeymanTestsFixture, testdata, ibus_keyman_tests_fixture_set_up,
skip ? test_skip : test_source, ibus_keyman_tests_fixture_tear_down);
g_object_unref(file);
g_object_unref(testfile);
g_string_free(testname, TRUE);
g_string_free(filename, TRUE);
}
// Run tests
int retVal = g_test_run();
// Cleanup
#ifdef GDK_WINDOWING_X11
if (display) {
XCloseDisplay(display);
display = NULL;
}
#endif
test_module_unuse(module);
return retVal;
}