From 94c5e361fbb229b0cd6853e2102a7d95f1f358b3 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 22:12:02 +0300 Subject: better public interface and doxygen --- tests/cocoa/test_cursor.m | 12 ++-- tests/cocoa/test_keyboard.m | 134 +++++++++++++++++++++++++++++++++++ tests/common/test_backends.c | 90 +++++++++++++++++++++-- tests/common/test_contracts.c | 92 ++++++++++++++++++++++++ tests/common/test_keyboard.c | 32 +++++++++ tests/common/test_utf8.c | 27 +++++++ tests/helpers/check_exports.py | 11 ++- tests/helpers/package_cocoa_tests.py | 70 ++++++++++++++++++ tests/helpers/run_backends.py | 8 ++- tests/helpers/run_cocoa_bundle.py | 68 ++++++++++++++++++ tests/meson.build | 47 ++++++++++-- tests/test_api.cpp | 13 ++++ tests/wayland/test_clipboard.c | 47 ++++++++++++ tests/wayland/test_keyboard.c | 128 +++++++++++++++++++++++++++++++++ tests/wayland/test_tablet.c | 37 ++++++++++ tests/x11/test_input.c | 123 ++++++++++++++++++++++++++++++++ tests/x11/test_tablet.c | 13 ++++ 17 files changed, 934 insertions(+), 18 deletions(-) create mode 100644 tests/cocoa/test_keyboard.m create mode 100644 tests/common/test_keyboard.c create mode 100644 tests/common/test_utf8.c create mode 100644 tests/helpers/package_cocoa_tests.py create mode 100644 tests/helpers/run_cocoa_bundle.py create mode 100644 tests/wayland/test_clipboard.c create mode 100644 tests/wayland/test_keyboard.c create mode 100644 tests/x11/test_input.c (limited to 'tests') diff --git a/tests/cocoa/test_cursor.m b/tests/cocoa/test_cursor.m index 0f3ba06..7a328f2 100644 --- a/tests/cocoa/test_cursor.m +++ b/tests/cocoa/test_cursor.m @@ -22,8 +22,12 @@ int main(void) assert(surface); NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface); NSView *view = window.contentView; + NSEvent *cursor = [NSEvent enterExitEventWithType:NSEventTypeCursorUpdate + location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0 + windowNumber:window.windowNumber context:nil eventNumber:0 trackingNumber:0 userData:NULL]; + assert(cursor); yasl_surface_set_cursor(surface, YASL_CURSOR_HIDDEN); - [view cursorUpdate:nil]; + [view cursorUpdate:cursor]; NSCursor *hidden = [NSCursor currentCursor]; assert_transparent(hidden); @@ -31,7 +35,7 @@ int main(void) location:NSMakePoint(100, 120) modifierFlags:0 timestamp:0 windowNumber:window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0]; [view mouseMoved:motion]; - [view cursorUpdate:nil]; + [view cursorUpdate:cursor]; assert([NSCursor currentCursor] == hidden); struct yasl_input_event event; bool moved = false; @@ -41,10 +45,10 @@ int main(void) assert(moved); [[NSCursor arrowCursor] set]; /* Simulate leaving and re-entering. */ - [view cursorUpdate:nil]; + [view cursorUpdate:cursor]; assert([NSCursor currentCursor] == hidden); yasl_surface_set_cursor(surface, YASL_CURSOR_ARROW); - [view cursorUpdate:nil]; + [view cursorUpdate:cursor]; assert([NSCursor currentCursor] == [NSCursor arrowCursor]); yasl_surface_destroy(surface); yasl_display_close(display); diff --git a/tests/cocoa/test_keyboard.m b/tests/cocoa/test_keyboard.m new file mode 100644 index 0000000..4854065 --- /dev/null +++ b/tests/cocoa/test_keyboard.m @@ -0,0 +1,134 @@ +#import +#include +#include +#include +#include + +/* Reject character queries on modifier-only events, as AppKit does. */ +@interface InputSample : NSObject +@property NSEventType type; +@property unsigned short keyCode; +@property NSEventModifierFlags modifierFlags; +@property(copy) NSString *label; +@property(copy) NSString *shiftedLabel; +@property NSPoint locationInWindow; +@property CGFloat scrollingDeltaX; +@property CGFloat scrollingDeltaY; +@property BOOL hasPreciseScrollingDeltas; +@end + +@implementation InputSample +- (NSString *)charactersByApplyingModifiers:(NSEventModifierFlags)modifiers +{ + assert(self.type == NSEventTypeKeyDown || self.type == NSEventTypeKeyUp); + return modifiers & NSEventModifierFlagShift ? self.shiftedLabel : self.label; +} +- (void)dealloc +{ + [_label release]; + [_shiftedLabel release]; + [super dealloc]; +} +@end + +static struct yasl_input_event next(struct yasl_surface *surface, enum yasl_input_kind kind) +{ + struct yasl_input_event event; + assert(yasl_surface_input_poll(surface, &event)); + assert(event.kind == kind && event.surface == surface); + return event; +} + +static void empty(struct yasl_surface *surface) +{ + struct yasl_input_event event; + assert(!yasl_surface_input_poll(surface, &event)); +} + +int main(void) +{ + @autoreleasepool { + struct yasl_display *display = yasl_display_open(NULL, NULL); + assert(display); + struct yasl_surface_spec spec = {.title = "Keyboard test", .size = {320, 240}}; + struct yasl_surface *surface = yasl_surface_create_toplevel(display, &spec); + assert(surface); + NSWindow *window = (NSWindow *)yasl_surface_get_native_handle(surface); + NSView *view = (id)window.contentView; + struct yasl_input_event event; + while (yasl_surface_input_poll(surface, &event)) {} + + InputSample *sample = [InputSample new]; + NSEvent *native = (NSEvent *)sample; + sample.type = NSEventTypeKeyUp; + sample.keyCode = 0; /* US A, even with a different active layout. */ + sample.label = @"q"; + sample.shiftedLabel = @"Q"; + [view keyUp:native]; + event = next(surface, YASL_INPUT_KEY); + assert(event.key.key == YASL_KEY_A && event.key.native_code == 0); + assert(event.key.action == YASL_KEY_RELEASE); + assert(event.key.codepoint == 'q' && event.key.shifted == 'Q'); + empty(surface); + sample.label = @"😀"; /* One scalar, two UTF-16 units. */ + sample.shiftedLabel = @"ab"; + [view keyUp:native]; + event = next(surface, YASL_INPUT_KEY); + assert(event.key.codepoint == 0x1f600 && event.key.shifted == 0); + sample.type = NSEventTypeFlagsChanged; + sample.keyCode = 56; /* Left Shift. */ + sample.modifierFlags = NSEventModifierFlagShift | 0x0002; + [view flagsChanged:native]; + event = next(surface, YASL_INPUT_KEY); + assert(event.key.key == YASL_KEY_LEFT_SHIFT && event.key.action == YASL_KEY_PRESS); + assert(event.key.mods == YASL_MOD_SHIFT && !event.key.codepoint && !event.key.shifted); + sample.modifierFlags = 0; + [view flagsChanged:native]; + assert(next(surface, YASL_INPUT_KEY).key.action == YASL_KEY_RELEASE); + + /* Native text services own preedit and commits. Marked text is not an + * insertion, and commits retain all UTF-8 bytes, including non-BMP. */ + [view setMarkedText:@"é" selectedRange:NSMakeRange(1, 0) + replacementRange:NSMakeRange(NSNotFound, 0)]; + assert(strcmp(yasl_surface_ime_preedit(surface), "é") == 0); + empty(surface); + [view insertText:@"😀é" replacementRange:NSMakeRange(NSNotFound, 0)]; + event = next(surface, YASL_INPUT_TEXT); + assert(event.text.len == strlen("😀é") && strcmp(event.text.text, "😀é") == 0); + assert(!yasl_surface_ime_preedit(surface)); + empty(surface); + + /* Exercise the keyDown -> interpretKeyEvents -> insertText route with + * an actual AppKit key event rather than the strict event double. */ + NSEvent *press = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint + modifierFlags:0 timestamp:0 windowNumber:window.windowNumber context:nil + characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:0]; + [view keyDown:press]; + assert(next(surface, YASL_INPUT_KEY).key.key == YASL_KEY_A); + event = next(surface, YASL_INPUT_TEXT); + assert(event.text.len == 1 && event.text.text[0] == 'a'); + empty(surface); + + sample.type = NSEventTypeScrollWheel; + sample.hasPreciseScrollingDeltas = YES; + sample.scrollingDeltaX = -2.5; + sample.scrollingDeltaY = 1.25; + [view scrollWheel:native]; + event = next(surface, YASL_INPUT_MOUSE); + assert(event.mouse.kind == YASL_PTR_SCROLL); + assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.125); + sample.hasPreciseScrollingDeltas = NO; + sample.scrollingDeltaX = 1; + sample.scrollingDeltaY = -1; + [view scrollWheel:native]; + event = next(surface, YASL_INPUT_MOUSE); + assert(event.mouse.scroll.x == -1 && event.mouse.scroll.y == 1); + sample.scrollingDeltaX = NAN; + [view scrollWheel:native]; + empty(surface); + [sample release]; + yasl_surface_destroy(surface); + yasl_display_close(display); + } + return 0; +} diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c index 4c99646..cf5b1f9 100644 --- a/tests/common/test_backends.c +++ b/tests/common/test_backends.c @@ -194,23 +194,105 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct yasl_display_copy(d, YASL_CLIPBOARD, "clipboard text"); yasl_display_paste(d, YASL_CLIPBOARD, w->surface); pump(loop, d, NULL); - bool saw_key = false, saw_paste = false; + bool saw_key = false, saw_paste = false, saw_text = false; struct yasl_input_event event; while (yasl_surface_input_poll(w->surface, &event)) { assert(event.surface == w->surface); if (event.kind == YASL_INPUT_KEY) { assert(event.key.codepoint == 'a'); assert(event.key.shifted == 'A'); - assert(event.key.base == 30); - saw_key = event.key.key == XK_a; + assert(event.key.native_code == key.xkey.keycode); + saw_key = event.key.key == YASL_KEY_A; } if (event.kind == YASL_INPUT_PASTE) { assert(event.paste.len == strlen("clipboard text")); assert(memcmp(event.paste.text, "clipboard text", event.paste.len) == 0); saw_paste = true; } + if (event.kind == YASL_INPUT_TEXT) { + assert(event.text.len == 1 && event.text.text[0] == 'a'); + saw_text = true; + } + } + assert(saw_key && saw_paste && saw_text); + /* Physical identity survives modifier and layout changes, including a + * release whose modifiers differ from its press. */ + key.xkey.state = ShiftMask; + XSendEvent(native, window, False, KeyPressMask, &key); + key.xkey.type = KeyRelease; + key.xkey.state = 0; + XSendEvent(native, window, False, KeyReleaseMask, &key); + XFlush(native); + pump(loop, d, NULL); + int key_count = 0, text_count = 0; + while (yasl_surface_input_poll(w->surface, &event)) { + if (event.kind == YASL_INPUT_KEY) { + assert(event.key.key == YASL_KEY_A); + assert(event.key.action == (key_count ? YASL_KEY_RELEASE : YASL_KEY_PRESS)); + key_count++; + } else if (event.kind == YASL_INPUT_TEXT) { + assert(event.text.len == 1 && event.text.text[0] == 'A'); + text_count++; + } + } + assert(key_count == 2 && text_count == 1); + key.xkey.type = KeyPress; + key.xkey.state = ControlMask; + XSendEvent(native, window, False, KeyPressMask, &key); + XFlush(native); + pump(loop, d, NULL); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A); + assert(event.key.mods & YASL_MOD_CTRL); + assert(!yasl_surface_input_poll(w->surface, &event)); + + int symbols_per_key = 0; + KeySym *saved_symbols = XGetKeyboardMapping(native, key.xkey.keycode, 1, &symbols_per_key); + assert(saved_symbols && symbols_per_key >= 2); + KeySym *changed_symbols = calloc((size_t)symbols_per_key, sizeof(KeySym)); + assert(changed_symbols); + changed_symbols[0] = XK_q; + changed_symbols[1] = XK_Q; + XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, changed_symbols, 1); + XSync(native, False); + pump(loop, d, NULL); /* Consume MappingNotify before translating the key. */ + key.xkey.state = 0; + XSendEvent(native, window, False, KeyPressMask, &key); + XFlush(native); + pump(loop, d, NULL); + saw_key = saw_text = false; + while (yasl_surface_input_poll(w->surface, &event)) { + if (event.kind == YASL_INPUT_KEY) { + assert(event.key.key == YASL_KEY_A && event.key.codepoint == 'q'); + saw_key = true; + } else if (event.kind == YASL_INPUT_TEXT) { + assert(event.text.len == 1 && event.text.text[0] == 'q'); + saw_text = true; + } + } + assert(saw_key && saw_text); + XChangeKeyboardMapping(native, key.xkey.keycode, symbols_per_key, saved_symbols, 1); + free(changed_symbols); + XFree(saved_symbols); + XSync(native, False); + pump(loop, d, NULL); + + for (unsigned button = 4; button <= 7; button++) { + XEvent wheel = {.xbutton = {.type = ButtonPress, .display = native, + .window = window, .root = DefaultRootWindow(native), + .same_screen = True, .button = button}}; + /* Direct delivery exercises the core fallback even with XI2 enabled. */ + XSendEvent(native, window, False, NoEventMask, &wheel); + wheel.xbutton.type = ButtonRelease; + XSendEvent(native, window, False, NoEventMask, &wheel); + XFlush(native); + pump(loop, d, NULL); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_MOUSE && event.mouse.kind == YASL_PTR_SCROLL); + assert(event.mouse.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0)); + assert(event.mouse.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0)); + assert(!yasl_surface_input_poll(w->surface, &event)); } - assert(saw_key && saw_paste); assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION); assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING)); diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c index 4e22de1..8580dd6 100644 --- a/tests/common/test_contracts.c +++ b/tests/common/test_contracts.c @@ -232,6 +232,97 @@ static void test_scale(void) assert(yasl_scale_to_logical(1, 0) == 0); } +static int popup_calls; +static struct yasl_surface *popup_create(struct yasl_surface *parent, + const struct yasl_popup_spec *spec) +{ + (void)parent; (void)spec; + popup_calls++; + return NULL; /* Backend fails without setting errno. */ +} + +static bool popup_reposition(struct yasl_surface *popup, + const struct yasl_popup_spec *spec) +{ + (void)popup; (void)spec; + popup_calls++; + errno = EBUSY; + return false; +} + +static uint32_t popup_caps(const struct yasl_display *d) +{ (void)d; return YASL_CAP_POPUPS; } + +static int dispatch_failure(struct yasl_display *d, int timeout) +{ (void)d; (void)timeout; return -1; } + +static struct yasl_surface *create_failure(struct yasl_display *d, + const struct yasl_surface_spec *spec, void *device) +{ (void)d; (void)spec; (void)device; return NULL; } + +static int drag_starts; +static bool drag_without_listener(struct yasl_display *d, struct yasl_surface *s, + const struct yasl_drag_image *image, const struct yasl_drag_listener *listener, + void *ud) +{ + (void)d; (void)s; (void)image; (void)ud; + /* Backends may require a table even when no completion callback is needed. */ + assert(listener && !listener->end); + drag_starts++; + return true; +} + +static void test_errors(void) +{ + const struct yasl_backend_ops ops = {.display_caps = popup_caps, + .surface_create_toplevel = create_failure, + .surface_create_popup = popup_create, + .surface_popup_reposition = popup_reposition, + .display_drag_start = drag_without_listener, + .display_dispatch_timeout = dispatch_failure}; + struct yasl_display d = {.ops = &ops}; + struct yasl_surface s = {.display = &d}; + struct yasl_popup_spec spec = {.size = {120, 80}, .anchor = {0, 0, 1, 1}}; + assert(!yasl_surface_create_popup(NULL, &spec) && errno == EINVAL); + assert(!yasl_surface_create_popup(&s, NULL) && errno == EINVAL); + spec.anchor.width = -1; /* Must not reach Wayland's protocol marshaler. */ + assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL); + assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EINVAL); + spec.anchor.width = 1; + spec.content_offset.x = INT_MAX; + spec.content_size.x = INT_MAX; + assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL); + spec.content_offset.x = spec.content_size.x = 0; + spec.size.x = 32768; + assert(!yasl_surface_create_popup(&s, &spec) && errno == EINVAL); + assert(!popup_calls); + spec.size.x = 120; + errno = ERANGE; + assert(!yasl_surface_create_popup(&s, &spec) && errno == ENODEV); + assert(!yasl_surface_popup_reposition(&s, &spec) && errno == EBUSY); + assert(popup_calls == 2); + const struct yasl_backend_ops unsupported = {0}; + d.ops = &unsupported; + assert(!yasl_surface_create_popup(&s, &spec) && errno == ENOTSUP); + assert(!yasl_surface_popup_reposition(&s, &spec) && errno == ENOTSUP); + assert(yasl_display_dispatch(&d, 0) == -1 && errno == ENOTSUP); + assert(yasl_display_dispatch(NULL, 0) == -1 && errno == EINVAL); + d.ops = &ops; + errno = ERANGE; + assert(yasl_display_dispatch(&d, 0) == -1 && errno == EIO); + struct yasl_surface_spec toplevel = {.size = {120, 80}}; + errno = ERANGE; + assert(!yasl_surface_create_toplevel(&d, &toplevel) && errno == ENODEV); + assert(yasl_display_drag_start(&d, &s, NULL, NULL, NULL)); + struct yasl_drag_image image = {.size = {1, 1}}; + assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL)); + uint32_t pixel = 0; + image.pixels = &pixel; + image.size.x = INT_MAX; + assert(!yasl_display_drag_start(&d, &s, &image, NULL, NULL)); + assert(drag_starts == 1); +} + int main(void) { test_timing_clock(); @@ -239,5 +330,6 @@ int main(void) test_validation(); test_native(); test_scale(); + test_errors(); return 0; } diff --git a/tests/common/test_keyboard.c b/tests/common/test_keyboard.c new file mode 100644 index 0000000..5a7f18e --- /dev/null +++ b/tests/common/test_keyboard.c @@ -0,0 +1,32 @@ +#include "common/keyboard.h" +#include "cocoa/keycodes.h" +#include + +int main(void) +{ + /* Same physical positions across native encodings, including keypad and + * left/right modifiers. macOS A is code zero, which is not UNKNOWN. */ + assert(yasl_key_from_evdev(30) == YASL_KEY_A); + assert(yasl_key_from_cocoa(kVK_ANSI_A) == YASL_KEY_A); + assert(yasl_key_from_xkb_name("AC01") == YASL_KEY_A); + assert(yasl_key_from_evdev(1) == YASL_KEY_ESCAPE); + assert(yasl_key_from_cocoa(kVK_Escape) == YASL_KEY_ESCAPE); + assert(yasl_key_from_xkb_name("ESC\0") == YASL_KEY_ESCAPE); + assert(yasl_key_from_evdev(96) == YASL_KEY_KP_ENTER); + assert(yasl_key_from_cocoa(kVK_ANSI_KeypadEnter) == YASL_KEY_KP_ENTER); + assert(yasl_key_from_cocoa(kVK_Return) == YASL_KEY_ENTER); + assert(yasl_key_from_evdev(100) == YASL_KEY_RIGHT_ALT); + assert(yasl_key_from_cocoa(kVK_RightOption) == YASL_KEY_RIGHT_ALT); + assert(yasl_key_from_evdev(42) == YASL_KEY_LEFT_SHIFT); + assert(yasl_key_from_cocoa(kVK_Shift) == YASL_KEY_LEFT_SHIFT); + assert(yasl_key_from_evdev(86) == YASL_KEY_ISO_BACKSLASH); + assert(yasl_key_from_cocoa(kVK_ISO_Section) == YASL_KEY_ISO_BACKSLASH); + assert(yasl_key_from_evdev(0) == YASL_KEY_UNKNOWN); + assert(yasl_key_from_evdev(UINT32_MAX) == YASL_KEY_UNKNOWN); + assert(yasl_key_from_cocoa(UINT32_MAX) == YASL_KEY_UNKNOWN); + assert(yasl_key_from_xkb_name("ZZZZ") == YASL_KEY_UNKNOWN); + assert(yasl_key_label(0x1f600) == 0x1f600); + assert(yasl_key_label(0xd800) == 0 && yasl_key_label(0x110000) == 0); + assert(yasl_key_label(0xf700) == 0 && yasl_key_label('\t') == 0); + return 0; +} diff --git a/tests/common/test_utf8.c b/tests/common/test_utf8.c new file mode 100644 index 0000000..72f8904 --- /dev/null +++ b/tests/common/test_utf8.c @@ -0,0 +1,27 @@ +#include "common/utf8.h" +#include +#include +#include + +int main(void) +{ + char latin1[256]; + for (int i = 1; i <= 255; i++) latin1[i - 1] = (char)i; + latin1[255] = '\0'; + size_t len = 0, back_len = 0; + char *utf8 = yasl_utf8_from_latin1(latin1, 255, &len); + assert(utf8 && yasl_utf8_validate(utf8, len)); + char *back = yasl_utf8_to_latin1(utf8, &back_len); + assert(back && back_len == 255 && memcmp(back, latin1, 256) == 0); + free(back); + free(utf8); + assert(!yasl_utf8_to_latin1("😀", &len)); + assert(!yasl_utf8_to_latin1("\xc3", &len)); + assert(!yasl_utf8_validate("\xc3", 1)); + assert(!yasl_utf8_validate("\xe2\x82", 2)); + assert(!yasl_utf8_validate("\xf0\x9f\x98", 3)); + assert(!yasl_utf8_validate("\xed\xa0\x80", 3)); /* Surrogate. */ + assert(yasl_utf8_validate("😀", sizeof("😀") - 1)); + assert(yasl_utf8_validate(NULL, 0)); + return 0; +} diff --git a/tests/helpers/check_exports.py b/tests/helpers/check_exports.py index 0d10014..acb727d 100644 --- a/tests/helpers/check_exports.py +++ b/tests/helpers/check_exports.py @@ -9,6 +9,7 @@ import subprocess def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--allow-export', action='append', default=[]) + parser.add_argument('--platform', choices=['linux', 'darwin'], default='linux') parser.add_argument('nm') parser.add_argument('library') parser.add_argument('headers', nargs='+') @@ -23,10 +24,14 @@ def main(): if not expected: raise SystemExit("No public function declarations found") - output = subprocess.check_output( - [args.nm, "-D", "--defined-only", "--format=posix", args.library], text=True - ) + nm_options = ["-g", "-U", "-P"] if args.platform == 'darwin' else [ + "-D", "--defined-only", "--format=posix" + ] + output = subprocess.check_output([args.nm, *nm_options, args.library], text=True) exported = {line.split()[0] for line in output.splitlines() if line.strip()} + if args.platform == 'darwin': + # Mach-O decorates C symbols with one leading underscore. + exported = {name[1:] if name.startswith('_') else name for name in exported} missing = expected - exported leaked = exported - expected - set(args.allow_export) if missing or leaked: diff --git a/tests/helpers/package_cocoa_tests.py b/tests/helpers/package_cocoa_tests.py new file mode 100644 index 0000000..8f801f8 --- /dev/null +++ b/tests/helpers/package_cocoa_tests.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Package compiled Cocoa tests for execution on a matching macOS desktop.""" + +import argparse +import hashlib +import json +from pathlib import Path +import shutil +import tarfile +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('build', type=Path) + parser.add_argument('output', type=Path) + args = parser.parse_args() + build = args.build.resolve() + info = build / 'meson-info' + host = json.loads((info / 'intro-machines.json').read_text())['host'] + if host['system'] != 'darwin': + parser.error('Use a configured and compiled Cocoa build.') + tests = json.loads((info / 'intro-tests.json').read_text()) + required = {'native', 'native-ownership', 'keyboard-cocoa', 'tablet-cocoa', + 'cursor-cocoa', 'hello-cocoa-window', 'tablet-cocoa-window'} + missing = required - {test['name'] for test in tests} + if missing: + parser.error('Enable integration_tests; missing: ' + ', '.join(sorted(missing))) + options = json.loads((info / 'intro-buildoptions.json').read_text()) + sanitizer = next(option['value'] for option in options if option['name'] == 'b_sanitize') + if sanitizer not in ('none', [], ['none']): + parser.error('Use an unsanitized build; sanitizer runtimes are not bundled.') + + with tempfile.TemporaryDirectory(prefix='yasl-cocoa-bundle-') as directory: + root = Path(directory) / 'yasl-cocoa-tests' + root.mkdir() + files = {} + + def copy(source, relative): + destination = root / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + files[str(relative)] = hashlib.sha256(destination.read_bytes()).hexdigest() + + copy(build / 'libyasl.dylib', Path('libyasl.dylib')) + source_root = Path(__file__).resolve().parents[2] + copy(source_root / 'LICENSE', Path('LICENSE')) + copy(Path(__file__).with_name('run_cocoa_bundle.py'), Path('run.py')) + commands = [] + for test in tests: + if test['name'] == 'public-exports': + continue # Check the dylib with nm on the build machine first. + executable = Path(test['cmd'][0]).resolve() + try: + relative = executable.relative_to(build) + except ValueError: + parser.error('Unexpected external test command: ' + str(executable)) + copy(executable, relative) + commands.append({'name': test['name'], 'cmd': [str(relative), *test['cmd'][1:]], + 'timeout': test['timeout']}) + manifest = {'architecture': host['cpu_family'], 'minimum_macos': 14, + 'tests': commands, 'sha256': files} + (root / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') + with tarfile.open(args.output, 'w:gz') as archive: + archive.add(root, arcname=root.name) + print(f'Packaged {len(commands)} runtime tests for {host["cpu_family"]}: {args.output}') + + +if __name__ == '__main__': + main() diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py index 57cc9ab..70c82fb 100644 --- a/tests/helpers/run_backends.py +++ b/tests/helpers/run_backends.py @@ -21,7 +21,7 @@ def wait_for(process, ready): def main(): - weston, xwayland, test, timers, tablet = sys.argv[1:] + weston, xwayland, test, timers, tablet, hello, input_x11 = sys.argv[1:] servers = [] with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: root = Path(directory) @@ -80,10 +80,16 @@ def main(): x_env["XDG_SESSION_TYPE"] = "x11" print("Test: X11 surface timers", flush=True) subprocess.run([timers], env=x_env, check=True, timeout=8) + print("Test: X11 UTF-8 keyboard fallback and clipboard", flush=True) + subprocess.run([input_x11], env=x_env, check=True, timeout=8) for backend in ("wayland", "x11"): print("Test: tablet example on", backend, flush=True) subprocess.run([tablet, "--backend", backend, "--smoke-test"], env=env, check=True, timeout=8) + print("Test: hello example on", backend, flush=True) + subprocess.run([hello, "--smoke-test"], + env=env if backend == "wayland" else x_env, + check=True, timeout=8) except BaseException: for log in (wl_log, x_log): log.flush() diff --git a/tests/helpers/run_cocoa_bundle.py b/tests/helpers/run_cocoa_bundle.py new file mode 100644 index 0000000..338f38e --- /dev/null +++ b/tests/helpers/run_cocoa_bundle.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Run a packaged Cocoa suite, preserving output and a machine-readable report.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import re +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--list', action='store_true', help='List tests without executing them.') + args = parser.parse_args() + root = Path(__file__).resolve().parent + manifest = json.loads((root / 'manifest.json').read_text()) + if args.list: + print('Architecture:', manifest['architecture']) + for test in manifest['tests']: + print(test['name']) + return + if sys.platform != 'darwin': + parser.error('Run this suite in a macOS desktop session.') + if int(platform.mac_ver()[0].split('.')[0]) < manifest['minimum_macos']: + parser.error('This suite requires macOS 14 or newer.') + architecture = {'arm64': 'aarch64'}.get(platform.machine(), platform.machine()) + if architecture != manifest['architecture']: + parser.error('Use the bundle matching this Python process architecture.') + for relative, expected in manifest['sha256'].items(): + if hashlib.sha256((root / relative).read_bytes()).hexdigest() != expected: + parser.error('Bundle file changed: ' + relative) + + logs = root / 'logs' + logs.mkdir(exist_ok=True) + env = os.environ.copy() + env['DYLD_LIBRARY_PATH'] = str(root) + results = [] + for test in manifest['tests']: + name = test['name'] + log = logs / (re.sub(r'[^A-Za-z0-9_-]', '_', name) + '.log') + command = [str(root / test['cmd'][0]), *test['cmd'][1:]] + with log.open('w') as output: + try: + process = subprocess.run(command, cwd=root, env=env, stdout=output, + stderr=subprocess.STDOUT, timeout=test['timeout'] or None) + result = {'name': name, 'returncode': process.returncode, + 'status': 'pass' if process.returncode == 0 else 'fail'} + except subprocess.TimeoutExpired: + output.write('\nTest timed out.\n') + result = {'name': name, 'status': 'timeout'} + except OSError as error: + output.write(str(error) + '\n') + result = {'name': name, 'status': 'error'} + results.append(result) + print(f'{name}: {result["status"]}', flush=True) + report = root / 'results.json' + report.write_text(json.dumps(results, indent=2) + '\n') + failed = sum(result['status'] != 'pass' for result in results) + print(f'{len(results) - failed} passed, {failed} failed. Logs: {logs}') + sys.exit(1 if failed else 0) + + +if __name__ == '__main__': + main() diff --git a/tests/meson.build b/tests/meson.build index 910f17c..c6570d9 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -30,16 +30,20 @@ if add_languages('cpp', native: false, required: false) 'yasl-cpp-api-test', 'test_api.cpp', dependencies: shared_dep, - cpp_args: cpp.get_supported_arguments('-pedantic-errors'), + cpp_args: cpp.get_supported_arguments('-pedantic-errors') + [ + '-DYASL_HAS_WAYLAND=' + (has_wayland ? '1' : '0'), + '-DYASL_HAS_X11=' + (has_x11 ? '1' : '0'), + '-DYASL_HAS_COCOA=' + (has_cocoa ? '1' : '0'), + ], override_options: ['cpp_std=c++11'], ) test('cpp-api', cpp_api_test) endif -python = find_program('python3') -if system == 'linux' - nm = find_program('nm') - export_args = [] +python = find_program('python3', native: true) +if system == 'linux' or system == 'darwin' + nm = find_program('nm', native: false) + export_args = ['--platform=' + system] if get_option('b_sanitize').contains('address') # Clang's ASan instrumentation exports these linker-generated markers. export_args += ['--allow-export=__start_asan_globals', @@ -57,6 +61,9 @@ if system == 'linux' if has_x11 exported_headers += files('../include/yasl/native/x11.h') endif + if has_cocoa + exported_headers += files('../include/yasl/native/cocoa.h') + endif test('public-exports', python, args: [files('helpers/check_exports.py')] + export_args + [nm, yasl_lib.get_shared_lib(), exported_headers], @@ -71,6 +78,16 @@ contracts_test = executable( ) test('contracts', contracts_test) +keyboard_test = executable('yasl-keyboard-test', 'common/test_keyboard.c', + include_directories: inc, dependencies: yasl_dep, +) +test('keyboard', keyboard_test) + +utf8_test = executable('yasl-utf8-test', 'common/test_utf8.c', + include_directories: inc, dependencies: yasl_dep, +) +test('utf8', utf8_test) + timer_test = executable( 'yasl-timer-test', 'common/test_timers.c', @@ -118,6 +135,14 @@ elif get_option('x11_tablet_tests') endif if has_wayland + clipboard_wayland_test = executable('yasl-clipboard-wayland-test', 'wayland/test_clipboard.c', + include_directories: inc, dependencies: yasl_dep, + ) + test('clipboard-wayland', clipboard_wayland_test) + keyboard_wayland_test = executable('yasl-keyboard-wayland-test', 'wayland/test_keyboard.c', + include_directories: inc, dependencies: yasl_dep, + ) + test('keyboard-wayland', keyboard_wayland_test) ime_test = executable('yasl-ime-test', 'wayland/test_input.c', include_directories: inc, dependencies: yasl_dep, @@ -143,6 +168,9 @@ benchmark('performance', benchmark_exe) if get_option('integration_tests') if has_cocoa + test('hello-cocoa-window', hello_example, + args: ['--smoke-test'], timeout: 20, + ) native_test = executable('yasl-native-test', 'cocoa/test_native.c', dependencies: shared_dep, ) @@ -151,6 +179,10 @@ if get_option('integration_tests') dependencies: [shared_dep, deps], ) test('native-ownership', ownership_test, timeout: 20) + keyboard_cocoa_test = executable('yasl-keyboard-cocoa-test', 'cocoa/test_keyboard.m', + dependencies: [shared_dep, deps], + ) + test('keyboard-cocoa', keyboard_cocoa_test, timeout: 20) tablet_cocoa_test = executable('yasl-tablet-cocoa-test', 'cocoa/test_tablet.m', dependencies: [shared_dep, deps], ) @@ -171,11 +203,14 @@ if get_option('integration_tests') 'common/test_backends.c', dependencies: [shared_dep, deps], ) + input_x11_test = executable('yasl-input-x11-test', 'x11/test_input.c', + include_directories: inc, dependencies: yasl_dep, + ) weston = find_program('weston') xwayland = find_program('Xwayland') test('backends', python, args: [files('helpers/run_backends.py'), weston, xwayland, - backend_test, timer_test, tablet_example], + backend_test, timer_test, tablet_example, hello_example, input_x11_test], timeout: 60, ) endif diff --git a/tests/test_api.cpp b/tests/test_api.cpp index 9fb8525..129a41e 100644 --- a/tests/test_api.cpp +++ b/tests/test_api.cpp @@ -5,6 +5,9 @@ #if YASL_HAS_X11 #include "yasl/native/x11.h" #endif +#if YASL_HAS_COCOA +#include "yasl/native/cocoa.h" +#endif #include "yasl/loop.h" #include #include @@ -28,6 +31,10 @@ int main() yasl_loop_destroy(loop); yasl_input_event event{}; + event.key.key = YASL_KEY_A; + assert(event.key.key == YASL_KEY_A); + event.mouse.scroll = {0.25, -0.5}; + assert(event.mouse.scroll.x == 0.25 && event.mouse.scroll.y == -0.5); assert(!yasl_surface_input_poll(nullptr, &event)); assert(!yasl_display_drag_start(nullptr, nullptr, nullptr, nullptr, nullptr)); assert(!yasl_surface_drag_carry(nullptr, {0, 0})); @@ -42,5 +49,11 @@ int main() assert(!yasl_x11_surface_get_window(nullptr)); #endif +#if YASL_HAS_COCOA + assert(!yasl_cocoa_display_get_application(nullptr)); + assert(!yasl_cocoa_surface_get_window(nullptr)); + assert(!yasl_cocoa_surface_get_view(nullptr)); +#endif + return 0; } diff --git a/tests/wayland/test_clipboard.c b/tests/wayland/test_clipboard.c new file mode 100644 index 0000000..553e84d --- /dev/null +++ b/tests/wayland/test_clipboard.c @@ -0,0 +1,47 @@ +#include "../../src/wayland/clipboard.c" +#include + +struct received { int count; size_t len; char text[32]; }; + +static void receive(const char *text, size_t len, bool uri_list, + struct wl_surface *surface, void *ud) +{ + (void)surface; + struct received *r = ud; + assert(!uri_list && len < sizeof(r->text)); + memcpy(r->text, text, len); + r->text[len] = '\0'; + r->len = len; + r->count++; +} + +int main(void) +{ + struct received received = {0}; + struct clipboard c = {.loop = yasl_loop_create(), .receive = receive, .ud = &received}; + assert(c.loop); + const struct { const char *bytes; size_t len; bool valid; } cases[] = { + {"é字😀", sizeof("é字😀") - 1, true}, + {"", 0, true}, + {"\xff", 1, false}, + {"\xf0\x9f\x98", 3, false}, /* Incomplete non-BMP character. */ + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + received = (struct received){0}; + int fds[2]; + assert(pipe(fds) == 0); + assert(start_transfer(&c, fds[0], false, false)); + assert(write(fds[1], cases[i].bytes, cases[i].len) == (ssize_t)cases[i].len); + close(fds[1]); + for (int steps = 0; c.transfers && steps < 10; steps++) + assert(yasl_loop_step(c.loop, 0) >= 0); + assert(!c.transfers); + assert(received.count == (int)cases[i].valid); + if (cases[i].valid) { + assert(received.len == cases[i].len); + assert(memcmp(received.text, cases[i].bytes, received.len) == 0); + } + } + yasl_loop_destroy(c.loop); + return 0; +} diff --git a/tests/wayland/test_keyboard.c b/tests/wayland/test_keyboard.c new file mode 100644 index 0000000..dbe0a94 --- /dev/null +++ b/tests/wayland/test_keyboard.c @@ -0,0 +1,128 @@ +#include "../../src/wayland/keyboard.c" +#include + +static struct yasl_input_event next(struct wayland_surface *w, enum yasl_input_kind kind) +{ + struct yasl_input_event ev; + assert(yasl_surface_input_poll(&w->base, &ev)); + assert(ev.kind == kind && ev.surface == &w->base); + return ev; +} + +static void empty(struct wayland_surface *w) +{ + struct yasl_input_event ev; + assert(!yasl_surface_input_poll(&w->base, &ev)); +} + +static void layout(struct seat *s, const char *name, const char *variant) +{ + keyboard_text_reset(s); + if (s->xkb_state) xkb_state_unref(s->xkb_state); + if (s->xkb_keymap) xkb_keymap_unref(s->xkb_keymap); + struct xkb_rule_names names = {.layout = name, .variant = variant}; + s->xkb_keymap = xkb_keymap_new_from_names(s->conn->xkb_ctx, &names, 0); + assert(s->xkb_keymap); + s->xkb_state = xkb_state_new(s->xkb_keymap); + assert(s->xkb_state); + s->input_mods_ready = false; + s->mod_alt = xkb_keymap_mod_get_index(s->xkb_keymap, XKB_MOD_NAME_ALT); +} + +static void modifiers(struct seat *s, const char *name) +{ + xkb_mod_mask_t mask = name ? 1u << xkb_keymap_mod_get_index(s->xkb_keymap, name) : 0; + xkb_state_update_mask(s->xkb_state, mask, 0, 0, 0, 0, 0); +} + +static void key(struct seat *s, uint32_t code, bool down) +{ + keyboard_listener.key(s, NULL, 1, 0, code, down ? + WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED); +} + +int main(void) +{ + struct wayland_display d = {.base.ops = &yasl_wayland_backend}; + d.xkb_ctx = xkb_context_new(0); + assert(d.xkb_ctx); + struct wayland_surface w = {.base.display = &d.base, .conn = &d}; + wayland_surface_set_callbacks(&w, NULL, NULL); + struct seat s = {.conn = &d, .keyboard_focus = &w, .repeat_fd = -1, + .compose_attempted = true}; + const char compose[] = " : \"é\" U00E9\n"; + struct xkb_compose_table *table = xkb_compose_table_new_from_buffer(d.xkb_ctx, + compose, sizeof(compose) - 1, "C.UTF-8", XKB_COMPOSE_FORMAT_TEXT_V1, 0); + assert(table); + s.compose = xkb_compose_state_new(table, 0); + assert(s.compose); + xkb_compose_table_unref(table); + layout(&s, "us", NULL); + key(&s, KEY_A, true); + struct yasl_input_event ev = next(&w, YASL_INPUT_KEY); + assert(ev.key.key == YASL_KEY_A && ev.key.native_code == KEY_A); + assert(ev.key.codepoint == 'a' && ev.key.shifted == 'A' && !ev.key.mods); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "a") == 0); + modifiers(&s, XKB_MOD_NAME_SHIFT); + key(&s, KEY_A, false); /* Modifier changes cannot change physical identity. */ + ev = next(&w, YASL_INPUT_KEY); + assert(ev.key.key == YASL_KEY_A && ev.key.action == YASL_KEY_RELEASE); + assert(ev.key.mods == YASL_MOD_SHIFT); + empty(&w); + key(&s, KEY_A, true); + next(&w, YASL_INPUT_KEY); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0); + modifiers(&s, XKB_MOD_NAME_CAPS); + key(&s, KEY_A, true); + assert(next(&w, YASL_INPUT_KEY).key.mods == YASL_MOD_CAPS); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "A") == 0); + const char *shortcuts[] = {XKB_MOD_NAME_CTRL, XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO}; + for (size_t i = 0; i < sizeof(shortcuts)/sizeof(shortcuts[0]); i++) { + modifiers(&s, shortcuts[i]); + key(&s, KEY_A, true); + next(&w, YASL_INPUT_KEY); + empty(&w); /* Shortcut characters never leak into committed text. */ + } + layout(&s, "fr", NULL); + key(&s, KEY_A, true); + ev = next(&w, YASL_INPUT_KEY); + assert(ev.key.key == YASL_KEY_A && ev.key.codepoint == 'q'); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "q") == 0); + layout(&s, "de", NULL); + modifiers(&s, "Mod5"); /* AltGr is text input, not an Alt shortcut. */ + key(&s, KEY_E, true); + assert(next(&w, YASL_INPUT_KEY).key.key == YASL_KEY_E); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "€") == 0); + layout(&s, "us", "intl"); + key(&s, KEY_APOSTROPHE, true); + next(&w, YASL_INPUT_KEY); + empty(&w); + key(&s, KEY_E, true); + next(&w, YASL_INPUT_KEY); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "é") == 0); + assert(keyboard_dispatch_key(&s, &w, KEY_E, YASL_KEY_REPEAT)); + assert(next(&w, YASL_INPUT_KEY).key.action == YASL_KEY_REPEAT); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0); + key(&s, KEY_APOSTROPHE, true); + next(&w, YASL_INPUT_KEY); + keyboard_reset_keys(&s, &w); + key(&s, KEY_E, true); + next(&w, YASL_INPUT_KEY); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0); + key(&s, KEY_APOSTROPHE, true); + next(&w, YASL_INPUT_KEY); + s.ime_focus = &w; + text_input_listener.commit_string(&s, NULL, "字"); + text_input_listener.done(&s, NULL, 0); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "字") == 0); + key(&s, KEY_E, true); + next(&w, YASL_INPUT_KEY); + assert(strcmp(next(&w, YASL_INPUT_TEXT).text.text, "e") == 0); + empty(&w); + xkb_compose_state_unref(s.compose); + xkb_state_unref(s.xkb_state); + xkb_keymap_unref(s.xkb_keymap); + xkb_context_unref(d.xkb_ctx); + input_queue_clear(&w.input); + return 0; +} diff --git a/tests/wayland/test_tablet.c b/tests/wayland/test_tablet.c index 449c446..f8982b0 100644 --- a/tests/wayland/test_tablet.c +++ b/tests/wayland/test_tablet.c @@ -50,6 +50,7 @@ int main(void) tool_frame(&tool, NULL, 0); ev = next(&surface); assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_ERASER); + assert(ev.scroll.x == 0 && ev.scroll.y == 1); tablet_tool_proximity_out(&tool, NULL); ev = next(&surface); assert(ev.kind == YASL_PTR_LEAVE && ev.device == YASL_PTR_DEV_ERASER && !ev.pen.pressure); @@ -57,6 +58,42 @@ int main(void) assert(!(yasl_display_caps(&display.base) & YASL_CAP_TABLET)); display.tablet_mgr = (void *)&display; /* Capability depends on protocol availability. */ assert(yasl_display_caps(&display.base) & YASL_CAP_TABLET); + + seat.pointer_focus = &surface; + /* A compositor may describe the same detent in three ways. value120 wins, + * and both axes belong to one event, without whole-line truncation. */ + pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_int(10)); + pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 1); + pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 30); + pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, -60); + pointer_frame(&seat, NULL); + ev = next(&surface); + assert(ev.kind == YASL_PTR_SCROLL && ev.device == YASL_PTR_DEV_MOUSE); + assert(ev.scroll.x == -0.5 && ev.scroll.y == 0.25); + struct yasl_input_event unused; + assert(!yasl_surface_input_poll(&surface.base, &unused)); + pointer_frame(&seat, NULL); + assert(!yasl_surface_input_poll(&surface.base, &unused)); + + /* Stops preserve offsets from the same frame. Repeated tiny movements + * must be visible immediately rather than held until a whole detent. */ + for (int i = 0; i < 3; i++) { + pointer_axis_source(&seat, NULL, WL_POINTER_AXIS_SOURCE_FINGER); + pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(1.25)); + pointer_axis(&seat, NULL, 0, WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(-2.5)); + pointer_axis_stop(&seat, NULL, 0, WL_POINTER_AXIS_VERTICAL_SCROLL); + pointer_frame(&seat, NULL); + ev = next(&surface); + assert(ev.scroll.x == -0.25 && ev.scroll.y == 0.125); + } + pointer_axis_discrete(&seat, NULL, WL_POINTER_AXIS_HORIZONTAL_SCROLL, 2); + pointer_frame(&seat, NULL); + ev = next(&surface); + assert(ev.scroll.x == 2 && ev.scroll.y == 0); + pointer_axis_value120(&seat, NULL, WL_POINTER_AXIS_VERTICAL_SCROLL, 60); + seat_pointer_reset(&seat); + pointer_frame(&seat, NULL); + assert(!yasl_surface_input_poll(&surface.base, &unused)); input_queue_clear(&surface.input); return 0; } diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c new file mode 100644 index 0000000..11dca4f --- /dev/null +++ b/tests/x11/test_input.c @@ -0,0 +1,123 @@ +/* An isolated real X server supplies the keymap; disabling this surface's XIC + * deliberately exercises text insertion when no input method is available. */ +#include "../../src/x11/window.c" + +static void check_selection_encoding(struct x11_display *d, struct x11_surface *s) +{ + Window requestor = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + Atom property = XInternAtom(d->native, "YASL_TEST_STRING", False); + XSelectionRequestEvent request = {.display = d->native, .requestor = requestor, + .selection = d->clipboard, .target = XA_STRING, .property = property}; + yasl_display_copy(&d->base, YASL_CLIPBOARD, "café £"); + selection_request(d, &request); + Atom actual; + int format; + unsigned long count, left; + unsigned char *data = NULL; + assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True, + AnyPropertyType, &actual, &format, &count, &left, &data) == Success); + assert(actual == XA_STRING && format == 8 && count == 6); + assert(memcmp(data, "caf\xe9 \xa3", count) == 0); + XFree(data); + yasl_display_copy(&d->base, YASL_CLIPBOARD, "字"); + selection_request(d, &request); + assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True, + AnyPropertyType, &actual, &format, &count, &left, &data) == Success); + assert(actual == None); /* Never mislabel UTF-8 as Latin-1. */ + if (data) XFree(data); + request.target = d->targets; + selection_request(d, &request); + assert(XGetWindowProperty(d->native, requestor, property, 0, 100, True, + AnyPropertyType, &actual, &format, &count, &left, &data) == Success); + assert(actual == XA_ATOM && format == 32 && count == 2); + assert(((Atom *)data)[0] == d->targets && ((Atom *)data)[1] == d->utf8); + XFree(data); + XDestroyWindow(d->native, requestor); + + /* Simulate a legacy owner returning STRING to the paste conversion window, + * then an empty UTF-8 owner and malformed UTF-8. */ + const struct { Atom type; const char *bytes; size_t len; const char *expected; } cases[] = { + {XA_STRING, "caf\xe9 \xa3", 6, "café £"}, + {d->utf8, "", 0, ""}, + {d->utf8, "\xff", 1, NULL}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + struct paste_request *pending = calloc(1, sizeof(*pending)); + assert(pending); + pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + pending->target = s; + pending->next = d->pastes; + d->pastes = pending; + XChangeProperty(d->native, pending->window, property, cases[i].type, 8, + PropModeReplace, (const unsigned char *)cases[i].bytes, (int)cases[i].len); + XSelectionEvent reply = {.requestor = pending->window, .property = property}; + selection_notify(d, &reply); + struct yasl_input_event event; + if (cases[i].expected) { + assert(yasl_surface_input_poll(&s->base, &event)); + assert(event.kind == YASL_INPUT_PASTE); + assert(event.paste.len == strlen(cases[i].expected)); + assert(strcmp(event.paste.text, cases[i].expected) == 0); + } + assert(!yasl_surface_input_poll(&s->base, &event)); + } +} + +int main(void) +{ + struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; + struct yasl_display *base = yasl_display_open(NULL, &ds); + assert(base); + struct x11_display *d = (struct x11_display *)base; + struct yasl_surface_spec spec = {.title = "Keyboard fallback", .size = {100, 100}}; + struct yasl_surface *surface = yasl_surface_create_toplevel(base, &spec); + assert(surface); + struct x11_surface *s = (struct x11_surface *)surface; + XIC saved_ic = s->ic; + s->ic = NULL; + unsigned code = XKeysymToKeycode(d->native, XK_a); + int width = 0; + KeySym *saved = XGetKeyboardMapping(d->native, code, 1, &width); + assert(saved && width >= 2); + KeySym *symbols = calloc((size_t)width, sizeof(*symbols)); + assert(symbols); + const struct { KeySym symbol, shifted; const char *text; uint32_t label; } cases[] = { + {XK_eacute, XK_Eacute, "é", 0xe9}, + {0x01000416, 0x01000416, "Ж", 0x416}, + {0x0101f600, 0x0101f600, "😀", 0x1f600}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + symbols[0] = cases[i].symbol; + symbols[1] = cases[i].shifted; + XChangeKeyboardMapping(d->native, code, width, symbols, 1); + XSync(d->native, False); + dispatch(base); + input_queue_clear(&s->input); + XKeyEvent native = {.type = KeyPress, .display = d->native, + .window = s->window, .keycode = code}; + key_event(s, &native, false); + struct yasl_input_event event; + assert(yasl_surface_input_poll(surface, &event)); + assert(event.kind == YASL_INPUT_KEY && event.key.key == YASL_KEY_A); + assert(event.key.codepoint == cases[i].label); + assert(yasl_surface_input_poll(surface, &event)); + assert(event.kind == YASL_INPUT_TEXT && event.text.len == strlen(cases[i].text)); + assert(strcmp(event.text.text, cases[i].text) == 0); + assert(!yasl_surface_input_poll(surface, &event)); + native.type = KeyRelease; + key_event(s, &native, false); + assert(yasl_surface_input_poll(surface, &event) && event.kind == YASL_INPUT_KEY); + assert(!yasl_surface_input_poll(surface, &event)); + } + XChangeKeyboardMapping(d->native, code, width, saved, 1); + XSync(d->native, False); + free(symbols); + XFree(saved); + s->ic = saved_ic; + check_selection_encoding(d, s); + yasl_surface_destroy(surface); + yasl_display_close(base); + return 0; +} diff --git a/tests/x11/test_tablet.c b/tests/x11/test_tablet.c index c35a266..ba815cb 100644 --- a/tests/x11/test_tablet.c +++ b/tests/x11/test_tablet.c @@ -341,6 +341,19 @@ int main(void) xi_pointer_event(&surface, &sample); ev = next(&surface); assert(ev.device == YASL_PTR_DEV_MOUSE && ev.button == 2 && ev.pen.pressure == 0); + for (int button = 4; button <= 7; button++) { + sample.detail = button; + sample.evtype = XI_ButtonPress; + xi_pointer_event(&surface, &sample); + ev = next(&surface); + assert(ev.kind == YASL_PTR_SCROLL); + assert(ev.scroll.x == (button == 6 ? -1 : button == 7 ? 1 : 0)); + assert(ev.scroll.y == (button == 4 ? -1 : button == 5 ? 1 : 0)); + sample.evtype = XI_ButtonRelease; + xi_pointer_event(&surface, &sample); + struct yasl_input_event unused; + assert(!yasl_surface_input_poll(&surface.base, &unused)); + } assert(pointer_button(2) == 1 && pointer_button(8) == 3); pointer.pressure = (struct tablet_axis){2, 1, 1, 50}; assert(pressure_value(&pointer.pressure) == 0); -- cgit v1.2.3