diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index f2c5c3ef..f52ae0c0 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -32,8 +32,13 @@ tasks: meson --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs + bld/debug/foot --version + bld/debug/footclient --version + - release: | mkdir -p bld/release meson --buildtype=minsize -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs + bld/release/foot --version + bld/release/footclient --version diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 296f6f78..01baedfb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,6 +36,8 @@ debug-x64-no-grapheme-clustering: - meson --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../../ - ninja -v -k0 - ninja -v test + - ./foot --version + - ./footclient --version artifacts: reports: junit: bld/debug/meson-logs/testlog.junit.xml @@ -49,6 +51,8 @@ release-x64: - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../../ - ninja -v -k0 - ninja -v test + - ./foot --version + - ./footclient --version artifacts: reports: junit: bld/release/meson-logs/testlog.junit.xml @@ -62,6 +66,8 @@ debug-x86: - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../../ - ninja -v -k0 - ninja -v test + - ./foot --version + - ./footclient --version artifacts: reports: junit: bld/debug/meson-logs/testlog.junit.xml @@ -75,6 +81,8 @@ release-x86: - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../../ - ninja -v -k0 - ninja -v test + - ./foot --version + - ./footclient --version artifacts: reports: junit: bld/release/meson-logs/testlog.junit.xml diff --git a/meson.build b/meson.build index 9faaf44b..39237a33 100644 --- a/meson.build +++ b/meson.build @@ -272,6 +272,7 @@ endif subdir('completions') subdir('icons') +subdir('tests') summary( { diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 00000000..abc7f34a --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,8 @@ +config_test = executable( + 'test-config', + 'test-config.c', '../tokenize.c', + wl_proto_headers, + link_with: [common], + dependencies: [pixman, xkb, fcft, tllist]) + +test('config', config_test) diff --git a/tests/test-config.c b/tests/test-config.c new file mode 100644 index 00000000..befe4864 --- /dev/null +++ b/tests/test-config.c @@ -0,0 +1,657 @@ +#if !defined(_DEBUG) + #define _DEBUG +#endif +#undef NDEBUG + +#include "../log.h" + +#include "../config.c" + +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) + +/* + * Stubs + */ + +void +user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, + const char *fmt, ...) +{ +} + +static void +test_invalid_key(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key) +{ + ctx->key = key; + ctx->value = "value for invalid key"; + + if (parse_fun(ctx)) { + BUG("[%s].%s: did not fail to parse as expected" + "(key should be invalid)", ctx->section, ctx->key); + } +} + +static void +test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char *const *conf_ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const char *value; + bool invalid; + } input[] = { + {"a string", "a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (strcmp(*conf_ptr, input[i].value) != 0) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *conf_ptr, input[i].value); + } + } + } +} + +static void +test_wstring(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, wchar_t *const *conf_ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const wchar_t *value; + bool invalid; + } input[] = { + {"a string", L"a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (wcscmp(*conf_ptr, input[i].value) != 0) { + BUG("[%s].%s=%s: set value (%ls) not the expected one (%ls)", + ctx->section, ctx->key, ctx->value, + *conf_ptr, input[i].value); + } + } + } +} + +static void +test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const bool *conf_ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + bool value; + bool invalid; + } input[] = { + {"1", true}, {"0", false}, + {"on", true}, {"off", false}, + {"true", true}, {"false", false}, + {"unittest-invalid-boolean-value", false, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*conf_ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *conf_ptr ? "true" : "false", + input[i].value ? "true" : "false"); + } + } + } +} + +static void +test_uint16(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const uint16_t *conf_ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + uint16_t value; + bool invalid; + } input[] = { + {"0", 0}, {"65535", 65535}, {"65536", 0, true}, + {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*conf_ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%hu) not the expected one (%hu)", + ctx->section, ctx->key, ctx->value, + *conf_ptr, input[i].value); + } + } + } +} + +static void +test_pt_or_px(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const struct pt_or_px *conf_ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + struct pt_or_px value; + bool invalid; + } input[] = { + {"12", {.pt = 12}}, {"12px", {.px = 12}}, + {"unittest-invalid-pt-or-px-value", {0}, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (memcmp(conf_ptr, &input[i].value, sizeof(*conf_ptr)) != 0) { + BUG("[%s].%s=%s: " + "set value (pt=%f, px=%d) not the expected one (pt=%f, px=%d)", + ctx->section, ctx->key, ctx->value, + conf_ptr->pt, conf_ptr->px, + input[i].value.pt, input[i].value.px); + } + } + } +} + +static void +test_section_main(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "main", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_main, "invalid-key"); + + test_string(&ctx, &parse_section_main, "shell", &conf.shell); + test_string(&ctx, &parse_section_main, "term", &conf.term); + test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + + test_wstring(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); + + test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); + test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); + test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); + test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit); + + test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); + test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); + test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); + + test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); + test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); + + /* TODO: font (custom) */ + /* TODO: include (custom) */ + /* TODO: dpi-aware (enum/boolean) */ + /* TODO: bold-text-in-bright (enum/boolean) */ + /* TODO: pad (geometry + optional string)*/ + /* TODO: initial-window-size-pixels (geometry) */ + /* TODO: initial-window-size-chars (geometry) */ + /* TODO: notify (spawn template)*/ + /* TODO: selection-target (enum) */ + /* TODO: initial-window-mode (enum) */ + + config_free(conf); +} + +static void +test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), + int action, int max_action, const char *const *map, + struct config_key_binding_list *bindings) +{ + xassert(map[action] != NULL); + xassert(bindings->count == 0); + + const char *key = map[action]; + + /* “Randomize” which modifiers to enable */ + const bool ctrl = action % 2; + const bool alt = action % 3; + const bool shift = action % 4; + const bool super = action % 5; + + /* Generate the modifier part of the ‘value’ */ + char modifier_string[32]; + int chars = sprintf(modifier_string, "%s%s%s%s", + ctrl ? XKB_MOD_NAME_CTRL "+" : "", + alt ? XKB_MOD_NAME_ALT "+" : "", + shift ? XKB_MOD_NAME_SHIFT "+" : "", + super ? XKB_MOD_NAME_LOGO "+" : ""); + + /* Use a unique symbol for this action */ + xkb_keysym_t sym = XKB_KEY_a + action; + char sym_name[8]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + + /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */ + char value[chars + strlen(sym_name) + 1]; + sprintf(value, "%s%s", modifier_string, sym_name); + + ctx->key = key; + ctx->value = value; + + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s failed to parse", + ctx->section, ctx->key, ctx->value); + } + + const struct config_key_binding *binding = + &bindings->arr[bindings->count - 1]; + + xassert(binding->pipe.argv.args == NULL); + + if (binding->action != action) { + BUG("[%s].%s=%s: action mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->action, action); + } + + if (binding->sym != sym) { + BUG("[%s].%s=%s: key symbol mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->sym, sym); + } + + if (binding->modifiers.ctrl != ctrl || + binding->modifiers.alt != alt || + binding->modifiers.shift != shift || + binding->modifiers.meta != super) + { + BUG("[%s].%s=%s: modifier mismatch:\n" + " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" + " expected: ctrl=%d, alt=%d, shift=%d, super=%d", + ctx->section, ctx->key, ctx->value, + binding->modifiers.ctrl, binding->modifiers.alt, + binding->modifiers.shift, binding->modifiers.meta, + ctrl, alt, shift, super); + } + + key_binding_list_free(bindings); + bindings->arr = NULL; + bindings->count = 0; + + if (action >= max_action) + return; + + /* + * Test collisions + */ + + /* First, verify we get a collision when trying to assign the same + * key combo to multiple actions */ + bindings->count = 1; + bindings->arr = xmalloc(sizeof(bindings->arr[0])); + bindings->arr[0] = (struct config_key_binding){ + .action = action + 1, + .sym = XKB_KEY_a, + .modifiers = { + .ctrl = true, + }, + }; + + xkb_keysym_get_name(XKB_KEY_a, sym_name, sizeof(sym_name)); + + char collision[128]; + snprintf(collision, sizeof(collision), "%s+%s", XKB_MOD_NAME_CTRL, sym_name); + + ctx->value = collision; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: key combo collision not detected", + ctx->section, ctx->key, ctx->value); + } + + /* Next, verify we get a collision when trying to assign the same + * key combo to the same action, but with different pipe argvs */ + bindings->arr[0].action = action; + bindings->arr[0].pipe.master_copy = true; + bindings->arr[0].pipe.argv.args = xmalloc(4 * sizeof(bindings->arr[0].pipe.argv.args[0])); + bindings->arr[0].pipe.argv.args[0] = xstrdup("/usr/bin/foobar"); + bindings->arr[0].pipe.argv.args[1] = xstrdup("hello"); + bindings->arr[0].pipe.argv.args[2] = xstrdup("world"); + bindings->arr[0].pipe.argv.args[3] = NULL; + + snprintf(collision, sizeof(collision), + "[/usr/bin/foobar hello] %s+%s", + XKB_MOD_NAME_CTRL, sym_name); + + ctx->value = collision; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: key combo collision not detected", + ctx->section, ctx->key, ctx->value); + } + + /* Finally, verify we do *not* get a collision when assigning the + * same key combo to the same action, with matching argvs */ + snprintf(collision, sizeof(collision), + "[/usr/bin/foobar hello world] %s+%s", + XKB_MOD_NAME_CTRL, sym_name); + + ctx->value = collision; + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: invalid key combo collision", + ctx->section, ctx->key, ctx->value); + } + + key_binding_list_free(bindings); + bindings->arr = NULL; + bindings->count = 0; +} + +static void +test_mouse_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), + int action, int max_action, const char *const *map, + struct config_mouse_binding_list *bindings) +{ + xassert(map[action] != NULL); + xassert(bindings->count == 0); + + const char *key = map[action]; + + /* “Randomize” which modifiers to enable */ + const bool ctrl = action % 2; + const bool alt = action % 3; + const bool shift = action % 4; + const bool super = action % 5; + + /* Generate the modifier part of the ‘value’ */ + char modifier_string[32]; + int chars = sprintf(modifier_string, "%s%s%s%s", + ctrl ? XKB_MOD_NAME_CTRL "+" : "", + alt ? XKB_MOD_NAME_ALT "+" : "", + shift ? XKB_MOD_NAME_SHIFT "+" : "", + super ? XKB_MOD_NAME_LOGO "+" : ""); + + const int button_idx = action % ALEN(button_map); + const int button = button_map[button_idx].code; + const char *const button_name = button_map[button_idx].name; + const int click_count = action % 3 + 1; + + xassert(click_count > 0); + + /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */ + char value[chars + strlen(button_name) + 2 + 1]; + chars = sprintf(value, "%s%s", modifier_string, button_name); + if (click_count > 1) + sprintf(&value[chars], "-%d", click_count); + + ctx->key = key; + ctx->value = value; + + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s failed to parse", + ctx->section, ctx->key, ctx->value); + } + + const struct config_mouse_binding *binding = + &bindings->arr[bindings->count - 1]; + + xassert(binding->pipe.argv.args == NULL); + + if (binding->action != action) { + BUG("[%s].%s=%s: action mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->action, action); + } + + if (binding->button != button) { + BUG("[%s].%s=%s: button mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->button, button); + } + + if (binding->count != click_count) { + BUG("[%s].%s=%s: button click count mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->count, click_count); + } + + if (binding->modifiers.ctrl != ctrl || + binding->modifiers.alt != alt || + binding->modifiers.shift != shift || + binding->modifiers.meta != super) + { + BUG("[%s].%s=%s: modifier mismatch:\n" + " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" + " expected: ctrl=%d, alt=%d, shift=%d, super=%d", + ctx->section, ctx->key, ctx->value, + binding->modifiers.ctrl, binding->modifiers.alt, + binding->modifiers.shift, binding->modifiers.meta, + ctrl, alt, shift, super); + } + + mouse_binding_list_free(bindings); + bindings->arr = NULL; + bindings->count = 0; + + if (action >= max_action) + return; + + /* + * Test collisions + */ + + /* First, verify we get a collision when trying to assign the same + * key combo to multiple actions */ + bindings->count = 1; + bindings->arr = xmalloc(sizeof(bindings->arr[0])); + bindings->arr[0] = (struct config_mouse_binding){ + .action = action + 1, + .button = BTN_LEFT, + .count = 1, + .modifiers = { + .ctrl = true, + }, + }; + + char collision[128]; + snprintf(collision, sizeof(collision), "%s+BTN_LEFT", XKB_MOD_NAME_CTRL); + + ctx->value = collision; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: mouse combo collision not detected", + ctx->section, ctx->key, ctx->value); + } + + /* Next, verify we get a collision when trying to assign the same + * key combo to the same action, but with different pipe argvs */ + bindings->arr[0].action = action; + bindings->arr[0].pipe.master_copy = true; + bindings->arr[0].pipe.argv.args = xmalloc(4 * sizeof(bindings->arr[0].pipe.argv.args[0])); + bindings->arr[0].pipe.argv.args[0] = xstrdup("/usr/bin/foobar"); + bindings->arr[0].pipe.argv.args[1] = xstrdup("hello"); + bindings->arr[0].pipe.argv.args[2] = xstrdup("world"); + bindings->arr[0].pipe.argv.args[3] = NULL; + + snprintf(collision, sizeof(collision), + "[/usr/bin/foobar hello] %s+BTN_LEFT", XKB_MOD_NAME_CTRL); + + ctx->value = collision; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: key combo collision not detected", + ctx->section, ctx->key, ctx->value); + } + +#if 0 /* BUG! (should be fixed by https://codeberg.org/dnkl/foot/pulls/832) */ + /* Finally, verify we do *not* get a collision when assigning the + * same key combo to the same action, with matching argvs */ + snprintf(collision, sizeof(collision), + "[/usr/bin/foobar hello world] %s+BTN_LEFT", XKB_MOD_NAME_CTRL); + + ctx->value = collision; + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: invalid mouse combo collision", + ctx->section, ctx->key, ctx->value); + } +#endif + mouse_binding_list_free(bindings); + bindings->arr = NULL; + bindings->count = 0; +} + +static void +test_section_key_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "key-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_key_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_KEY_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_key_bindings, + action, BIND_ACTION_KEY_COUNT - 1, + binding_action_map, &conf.bindings.key); + } + + config_free(conf); +} + +#if 0 +static void +test_section_search_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "search-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_search_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) { + if (search_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_search_bindings, + action, BIND_ACTION_SEARCH_COUNT - 1, + search_binding_action_map, &conf.bindings.search); + } + + config_free(conf); +} + +static void +test_section_url_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "rul-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_url_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_URL_COUNT; action++) { + if (url_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_url_bindings, + action, BIND_ACTION_URL_COUNT - 1, + url_binding_action_map, &conf.bindings.url); + } + + config_free(conf); +} +#endif + +static void +test_section_mouse_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_mouse_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_mouse_binding( + &ctx, &parse_section_mouse_bindings, + action, BIND_ACTION_COUNT - 1, + binding_action_map, &conf.bindings.mouse); + } + + config_free(conf); +} + +int +main(int argc, const char *const *argv) +{ + log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); + test_section_main(); + test_section_key_bindings(); +#if 0 + test_section_search_bindings(); + test_section_url_bindings(); +#endif + test_section_mouse_bindings(); + log_deinit(); + return 0; +}