From dd1775a9927922c14f8679ccbc4d3ca8d5917e20 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 21:23:03 +0300 Subject: public api refactor --- tests/cocoa/test_ownership.m | 75 +++++++++--- tests/common/test_app.c | 72 +++++++++--- tests/common/test_events.c | 209 +++++++++++++++++++++++++++++++++ tests/common/test_hello_example.c | 2 +- tests/helpers/run_backends.py | 7 +- tests/meson.build | 7 +- tests/test_api.cpp | 16 +++ tests/wayland/test_app_input.c | 17 ++- tests/wayland/test_clipboard.c | 75 +++++++++++- tests/x11/test_input.c | 236 ++++++++++++++++++++++++++++++++++++-- tests/x11/test_tablet_native.c | 10 +- 11 files changed, 666 insertions(+), 60 deletions(-) create mode 100644 tests/common/test_events.c (limited to 'tests') diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m index 5fd5cbf..6d69907 100644 --- a/tests/cocoa/test_ownership.m +++ b/tests/cocoa/test_ownership.m @@ -3,6 +3,7 @@ #import #include "yasl/yasl.h" #include +#include #include #include @@ -20,13 +21,49 @@ int main(void) assert(yasl_cocoa_surface_get_view(s) == window.contentView); assert([window isKindOfClass:[NSWindow class]] && window.contentView); struct yasl_event event; - while (yasl_surface_poll_event(s, &event)) {} + while (yasl_display_poll_event(d, &event)) {} + + assert(yasl_surface_set_size(s, (struct yasl_vector2i){180, 110})); + assert(yasl_surface_set_fullscreen(s, false)); + assert(yasl_surface_set_maximized(s, false)); + assert(!yasl_surface_set_size(s, (struct yasl_vector2i){0, 100})); + assert(errno == EINVAL); + struct yasl_popup_spec popup_spec = { + .size = {80, 40}, .anchor = {10, 10, 2, 2}, + }; + struct yasl_surface *popup = yasl_surface_create_popup(s, &popup_spec); + assert(popup); + assert(!yasl_surface_set_size(popup, (struct yasl_vector2i){80, 40})); + assert(errno == EINVAL); + assert(!yasl_surface_set_fullscreen(popup, true) && errno == EINVAL); + assert(!yasl_surface_set_maximized(popup, true) && errno == EINVAL); + assert(!yasl_surface_minimize(popup) && errno == EINVAL); + yasl_surface_destroy(popup); + while (yasl_display_poll_event(d, &event)) {} + + id text_input = (id)yasl_cocoa_surface_get_view(s); + [text_input setMarkedText:@"é漢" selectedRange:NSMakeRange(1, 1) + replacementRange:NSMakeRange(NSNotFound, 0)]; + assert(strcmp(yasl_surface_ime_preedit(s), "é漢") == 0); + [text_input unmarkText]; + assert(yasl_surface_ime_preedit(s) == NULL); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_COMPOSITION && event.composition.active); + assert(strcmp(event.composition.data, "é漢") == 0); + assert(event.composition.begin == 2 && event.composition.end == 5); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_COMPOSITION && !event.composition.active); + assert(event.composition.begin == -1 && event.composition.end == -1); + while (yasl_display_poll_event(d, &event)) {} + + uint64_t request_ids[600]; @autoreleasepool { for (int i = 0; i < 600; i++) { char text[32]; snprintf(text, sizeof(text), "paste-%d", i); - yasl_display_copy(d, YASL_CLIPBOARD, text); - yasl_display_paste(d, YASL_CLIPBOARD, s); + assert(yasl_clipboard_set(d, YASL_CLIPBOARD, text)); + request_ids[i] = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(request_ids[i] != 0); memset(text, 'x', sizeof(text)); } } @@ -34,19 +71,31 @@ int main(void) for (int i = 0; i < 600; i++) { char text[32]; snprintf(text, sizeof(text), "paste-%d", i); - assert(yasl_surface_poll_event(s, &event)); - assert(event.type == YASL_EVENT_PASTE && strcmp(event.text.data, text) == 0); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == request_ids[i]); + assert(event.paste.status == YASL_TRANSFER_OK && strcmp(event.paste.data, text) == 0); } - const char *last = event.text.data; - assert(!yasl_surface_poll_event(s, &event)); + const char *last = event.paste.data; + assert(!yasl_display_poll_event(d, &event)); @autoreleasepool { yasl_display_copy(d, YASL_CLIPBOARD, "new selection"); } assert(strcmp(last, "paste-599") == 0); - @autoreleasepool { - yasl_display_copy(d, YASL_CLIPBOARD, ""); - yasl_display_paste(d, YASL_CLIPBOARD, s); - } - assert(yasl_surface_poll_event(s, &event)); - assert(event.type == YASL_EVENT_PASTE && !event.text.len && !event.text.data[0]); + assert(!yasl_clipboard_set(d, YASL_CLIPBOARD, "\xc3")); + assert(errno == EILSEQ); + assert(yasl_clipboard_set(d, YASL_CLIPBOARD, "")); + uint64_t empty_id = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(empty_id && yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == empty_id); + assert(event.paste.status == YASL_TRANSFER_OK && !event.paste.len && !event.paste.data[0]); + [NSPasteboard.generalPasteboard clearContents]; + uint64_t unavailable_id = yasl_clipboard_request(s, YASL_CLIPBOARD); + assert(unavailable_id && yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == unavailable_id); + assert(event.paste.status == YASL_TRANSFER_UNAVAILABLE && !event.paste.len); + yasl_display_copy(d, YASL_CLIPBOARD, "legacy paste"); + yasl_display_paste(d, YASL_CLIPBOARD, s); + assert(yasl_display_poll_event(d, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == 0); + assert(event.paste.status == YASL_TRANSFER_OK && strcmp(event.text.data, "legacy paste") == 0); yasl_surface_destroy(s); yasl_display_close(d); return 0; diff --git a/tests/common/test_app.c b/tests/common/test_app.c index f61a957..214d8f2 100644 --- a/tests/common/test_app.c +++ b/tests/common/test_app.c @@ -2,6 +2,7 @@ #include "yasl/app.h" #include +#include #include #include #include @@ -16,13 +17,16 @@ struct test_state { bool quit_called; int frame_count; yasl_timer_id timer_id; + pthread_t worker; + uint64_t target_id; }; static void *worker_thread(void *arg) { struct test_state *state = arg; usleep(50000); /* 50ms */ - bool ok = yasl_post_event(state->display, state->surface, 42, (void *)0x1234, 0); + bool ok = yasl_display_post(state->display, &(struct yasl_message){ + .target_id = state->target_id, .code = 42, .payload = (void *)0x1234}); assert(ok); return NULL; } @@ -54,13 +58,13 @@ static enum yasl_app_result on_init(void *ud, struct yasl_display *d) yasl_surface_set_pointer_mode(state->surface, YASL_POINTER_RELATIVE); yasl_surface_set_pointer_mode(state->surface, YASL_POINTER_NORMAL); - state->timer_id = yasl_app_add_timer(d, YASL_MS_TO_NS(20), on_timer, state); + state->timer_id = yasl_loop_add_timer(yasl_display_loop(d), YASL_MS_TO_NS(20), on_timer, state); assert(state->timer_id > 0); - pthread_t thread; - int err = pthread_create(&thread, NULL, worker_thread, state); + state->target_id = yasl_surface_id(state->surface); + int err = pthread_create(&state->worker, NULL, worker_thread, state); assert(err == 0); - pthread_detach(thread); + yasl_surface_request_frame(state->surface); return YASL_APP_CONTINUE; @@ -80,7 +84,7 @@ static enum yasl_app_result on_event(void *ud, const struct yasl_event *ev) return YASL_APP_CONTINUE; } -static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { struct test_state *state = ud; assert(s == state->surface); @@ -88,12 +92,12 @@ static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context state->frame_called = true; state->frame_count++; - if (ctx->cpu.pixels) { - size_t count = (size_t)ctx->size.x * ctx->size.y; + if (ctx->target.pixels) { + size_t count = (size_t)ctx->target.size.x * ctx->target.size.y; for (size_t i = 0; i < count; i++) { - ctx->cpu.pixels[i] = 0xff204060; + ctx->target.pixels[i] = 0xff204060; } - bool ok = yasl_frame_present_cpu(ctx, NULL, 0); + bool ok = yasl_frame_present(ctx, NULL, 0); assert(ok); } @@ -106,14 +110,15 @@ static void on_quit(void *ud, struct yasl_display *d) { (void)d; struct test_state *state = ud; + assert(!pthread_join(state->worker, NULL)); state->quit_called = true; } -static void present_once(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) +static void present_once(void *ud, struct yasl_surface *s, struct yasl_frame *ctx) { (*(int *)ud)++; - if (ctx->cpu.pixels) - assert(yasl_frame_present_cpu(ctx, NULL, 0)); + if (ctx->target.pixels) + assert(yasl_frame_present(ctx, NULL, 0)); yasl_surface_destroy(s); } @@ -132,6 +137,43 @@ static void test_window_without_init(void) assert(frames == 1); } + +static enum yasl_app_result fail_init(void *ud, struct yasl_display *d) +{ + (void)ud; (void)d; + return YASL_APP_ERROR; +} +static void count_quit(void *ud, struct yasl_display *d) +{ + (void)d; + (*(int *)ud)++; +} +static enum yasl_app_result inspect_initial_window(void *ud, struct yasl_display *d) +{ + struct yasl_surface *s = yasl_app_window(d); + assert(s && yasl_surface_userdata(s) == ud); + errno = 0; + assert(!yasl_surface_set_callbacks(s, NULL, NULL) && errno == EBUSY); + return YASL_APP_CONTINUE; +} +static void fail_frame(void *ud, struct yasl_surface *s, struct yasl_frame *frame) +{ + (void)ud; + assert(frame->surface == s && frame->target.stride_bytes == frame->target.size.x * 4); + yasl_app_exit(yasl_surface_display(s), YASL_APP_ERROR); +} +static void test_error_lifecycle(void) +{ + int quits = 0; + assert(yasl_run(&(struct yasl_app_desc){.userdata = &quits, + .init = fail_init, .quit = count_quit}) < 0); + assert(quits == 1 && errno == EIO); + struct yasl_window_spec window = {.size_logical = {160, 120}, .userdata = &quits}; + assert(yasl_run(&(struct yasl_app_desc){.userdata = &quits, .window = &window, + .init = inspect_initial_window, .frame = fail_frame, .quit = count_quit}) < 0); + assert(quits == 2 && errno == EIO); +} + int main(void) { struct test_state state = {0}; @@ -146,6 +188,9 @@ int main(void) }; int res = yasl_run(&desc); + if (res < 0 && !state.init_called && + (errno == EPERM || errno == EACCES || errno == ENOENT || + errno == ECONNREFUSED || errno == ENODEV)) return 77; assert(res == 0); assert(state.init_called); assert(state.frame_called); @@ -153,6 +198,7 @@ int main(void) assert(state.timer_called); assert(state.quit_called); test_window_without_init(); + test_error_lifecycle(); printf("App runner tests passed!\n"); return 0; } diff --git a/tests/common/test_events.c b/tests/common/test_events.c new file mode 100644 index 0000000..c8660cb --- /dev/null +++ b/tests/common/test_events.c @@ -0,0 +1,209 @@ +#include "common/backend.h" +#include "yasl/app.h" + +#include +#include +#include +#include +#include + +static struct input_queue *queue(struct yasl_surface *s) { return &s->input; } +static const struct yasl_backend_ops ops = {.surface_input_queue = queue}; +static int released; +static pthread_t owner; +static void release(void *payload) +{ + assert(pthread_equal(owner, pthread_self())); + assert(payload == &released); + released++; +} + +static void setup(struct yasl_display *d, struct yasl_surface *a, struct yasl_surface *b) +{ + *d = (struct yasl_display){.ops = &ops}; + *a = (struct yasl_surface){.display = d, .scale_120 = 120}; + *b = (struct yasl_surface){.display = d, .scale_120 = 120}; + assert(yasl_events_open(d)); + assert(yasl_events_register(a)); + assert(yasl_events_register(b)); +} + +static void teardown(struct yasl_display *d, struct yasl_surface *a, struct yasl_surface *b) +{ + yasl_events_unregister(a); + yasl_events_unregister(b); + input_queue_clear(&a->input); + input_queue_clear(&b->input); + yasl_events_close(d); +} + +static void test_order_and_lifetimes(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + uint64_t aid = yasl_surface_id(&a), bid = yasl_surface_id(&b); + assert(aid && bid && aid != bid); + yasl_surface_set_userdata(&a, &b); + assert(yasl_surface_userdata(&a) == &b); + char text[] = "text"; + assert(input_queue_push(&a.input, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {text, 4}})); + memset(text, 'x', 4); + yasl_surface_notify_state(&b, YASL_STATE_MAXIMIZED); + yasl_surface_notify_configure(&a, (struct yasl_geometry){{640, 480}, 180}); + yasl_surface_notify_drag(&b, YASL_DRAG_OVER, (struct yasl_vector2i){3, 4}); + yasl_surface_notify_composition(&a, "hello", 1, 3); + yasl_surface_notify_composition(&a, NULL, -1, -1); + yasl_surface_complete_paste(&b, 42, YASL_TRANSFER_UNAVAILABLE, NULL, 0); + const enum yasl_event_type expected[] = {YASL_EVENT_TEXT, YASL_EVENT_WINDOW_STATE, + YASL_EVENT_WINDOW_RESIZE, YASL_EVENT_DRAG, YASL_EVENT_COMPOSITION, + YASL_EVENT_COMPOSITION, YASL_EVENT_PASTE}; + struct yasl_event ev; + uint64_t sequence = 0; + for (size_t i = 0; i < sizeof(expected) / sizeof(*expected); i++) { + assert(yasl_display_poll_event(&d, &ev)); + assert(ev.type == expected[i] && ev.sequence > sequence && ev.time_ns); + sequence = ev.sequence; + if (i == 0) assert(!strcmp(ev.text.data, "text") && ev.surface_id == aid); + if (i == 2) assert(ev.resize.scale_changed && ev.resize.geometry.size.x == 640); + if (i == 4) assert(ev.composition.active && ev.composition.begin == 1 && !strcmp(ev.composition.data, "hello")); + if (i == 5) assert(!ev.composition.active && !ev.composition.len); + if (i == 6) assert(ev.paste.request_id == 42 && ev.paste.status == YASL_TRANSFER_UNAVAILABLE); + } + assert(!yasl_display_poll_event(&d, &ev)); + assert(input_queue_push(&a.input, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {"kept", 4}})); + assert(yasl_display_poll_event(&d, &ev)); + const char *borrowed = ev.text.data; + assert(input_queue_push(&a.input, &(struct yasl_event){.type = YASL_EVENT_KEY, .surface = &a})); + yasl_events_unregister(&a); + assert(!strcmp(borrowed, "kept")); + assert(!yasl_display_poll_event(&d, &ev)); + /* Reusing object memory must never reuse the identity. */ + assert(yasl_events_register(&a)); + assert(a.id != aid); + errno = 0; + assert(!yasl_display_post(&d, &(struct yasl_message){.target_id = aid})); + assert(errno == ENOENT); + teardown(&d, &a, &b); +} + +struct producer { struct yasl_display *display; uint64_t target; }; +static void *post_worker(void *ud) +{ + struct producer *producer = ud; + for (uint32_t i = 0; i < 32; i++) + assert(yasl_display_post(producer->display, &(struct yasl_message){ + .target_id = producer->target, .code = i, .payload = &released, .release = release})); + return NULL; +} + +static void test_post_cleanup(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + owner = pthread_self(); + released = 0; + pthread_t worker; + struct producer producer = {&d, a.id}; + assert(!pthread_create(&worker, NULL, post_worker, &producer)); + assert(!pthread_join(worker, NULL)); + struct yasl_event ev; + for (uint32_t i = 0; i < 8; i++) { + assert(yasl_display_poll_event(&d, &ev)); + assert(ev.user.code == i && ev.surface == &a); + } + assert(released == 7); /* Last event remains borrowed. */ + yasl_events_unregister(&a); + assert(released == 31); /* Undelivered target messages canceled. */ + assert(!yasl_display_poll_event(&d, &ev)); + assert(yasl_display_post(&d, &(struct yasl_message){.payload = &released, .release = release})); + yasl_events_close(&d); + assert(released == 33); /* Pending message and last consumed message. */ + errno = 0; + assert(!yasl_display_post(&d, &(struct yasl_message){.payload = &released, .release = release})); + assert(errno == EINVAL && released == 33); +} + +static int damage_count; +static bool present(struct yasl_surface *s, const struct yasl_framebuffer *target, + const struct yasl_damage_list *damage) +{ + (void)s; (void)target; + damage_count = damage->n; + return true; +} +static void test_damage(void) +{ + struct yasl_backend_ops present_ops = {.surface_present = present}; + struct yasl_display d = {.ops = &present_ops}; + struct yasl_surface s = {.display = &d, .frame_generation = 1, .drawing = true}; + struct yasl_frame frame = {.surface = &s, .target = {.generation = 1, .size = {8, 8}}}; + s.active_frame = &frame; + struct yasl_rect2i rects[YASL_DAMAGE_MAX + 1] = {{0}}; + assert(yasl_frame_present(&frame, NULL, 0) && damage_count == -1); + assert(!yasl_frame_present(&frame, NULL, 0) && errno == EALREADY); + s.frame_presented = false; + assert(yasl_frame_present(&frame, rects, 0) && damage_count == 0); + s.frame_presented = false; + assert(yasl_frame_present(&frame, rects, YASL_DAMAGE_MAX + 1) && damage_count == -1); + s.frame_presented = false; + rects[YASL_DAMAGE_MAX].width = -1; + errno = 0; + assert(!yasl_frame_present(&frame, rects, YASL_DAMAGE_MAX + 1) && errno == EINVAL); + assert(!yasl_frame_present(&frame, NULL, 1)); + s.drawing = false; + assert(!yasl_frame_present(&frame, NULL, 0)); +} + +static void test_failures(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + assert(!yasl_surface_set_size(&a, (struct yasl_vector2i){0, 10}) && errno == EINVAL); + assert(!yasl_surface_set_size(&a, (struct yasl_vector2i){10, 10}) && errno == ENOTSUP); + assert(!yasl_surface_set_fullscreen(&a, true) && errno == ENOTSUP); + assert(!yasl_surface_set_maximized(&a, true) && errno == ENOTSUP); + assert(!yasl_surface_minimize(&a) && errno == ENOTSUP); + a.popup = true; + assert(!yasl_surface_minimize(&a) && errno == EINVAL); + assert(!yasl_clipboard_set(&d, YASL_CLIPBOARD, "text") && errno == ENOTSUP); + assert(!yasl_clipboard_request(&a, YASL_CLIPBOARD) && errno == ENOTSUP); + /* Queued native event allocation/validation failure must be observable. */ + assert(!yasl_events_push(&d, &a, &(struct yasl_event){ + .type = YASL_EVENT_TEXT, .surface = &a, .text = {"x", SIZE_MAX}})); + assert(yasl_events_error(&d) == ENOMEM); + teardown(&d, &a, &b); +} + + +static void test_legacy_poll_does_not_spin(void) +{ + struct yasl_display d; + struct yasl_surface a, b; + setup(&d, &a, &b); + yasl_surface_notify_state(&a, YASL_STATE_ACTIVATED); + yasl_surface_notify_close(&b); + assert(yasl_events_pending(&d)); + struct yasl_event ev; + assert(!yasl_surface_poll_event(&a, &ev)); + assert(!yasl_events_pending(&d)); + assert(input_queue_push(&b.input, &(struct yasl_event){.type = YASL_EVENT_KEY, .surface = &b})); + assert(!yasl_surface_poll_event(&a, &ev)); + assert(yasl_surface_poll_event(&b, &ev) && ev.surface == &b); + assert(!yasl_events_pending(&d)); + teardown(&d, &a, &b); +} + +int main(void) +{ + test_order_and_lifetimes(); + test_post_cleanup(); + test_damage(); + test_failures(); + test_legacy_poll_does_not_spin(); + return 0; +} diff --git a/tests/common/test_hello_example.c b/tests/common/test_hello_example.c index 101e2d4..350b3bb 100644 --- a/tests/common/test_hello_example.c +++ b/tests/common/test_hello_example.c @@ -3,7 +3,7 @@ #include "../../examples/hello.c" #undef main -static void draw_once(void *ud, struct yasl_surface *surface, struct yasl_frame_context *frame) +static void draw_once(void *ud, struct yasl_surface *surface, struct yasl_frame *frame) { draw(ud, surface, frame); if (!failed) yasl_surface_destroy(surface); diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py index 76fe8ab..af7e02d 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, hello, input_x11, interactive = sys.argv[1:] + weston, xwayland, test, timers, tablet, hello, input_x11, interactive, app, app_input = sys.argv[1:] servers = [] with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: root = Path(directory) @@ -82,6 +82,11 @@ def main(): 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) + print("Test: Wayland runner lifecycle and event destruction", flush=True) + subprocess.run([app], env=env, check=True, timeout=8) + subprocess.run([app_input], env=env, check=True, timeout=8) + print("Test: X11 runner lifecycle", flush=True) + subprocess.run([app], 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"], diff --git a/tests/meson.build b/tests/meson.build index 84b6847..cac62db 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -279,8 +279,13 @@ if get_option('integration_tests') test('backends', python, args: [files('helpers/run_backends.py'), weston, xwayland, backend_test, timer_test, tablet_example, hello_example_test, input_x11_test, - interactive_example], + interactive_example, app_test, app_input_test], timeout: 60, ) endif endif + +events_test = executable('yasl-events-test', 'common/test_events.c', + include_directories: inc, dependencies: [yasl_dep, dependency('threads')], +) +test('event-contracts', events_test) diff --git a/tests/test_api.cpp b/tests/test_api.cpp index bc54323..c5859cd 100644 --- a/tests/test_api.cpp +++ b/tests/test_api.cpp @@ -1,4 +1,5 @@ #include "yasl/native.h" +#include "yasl/app.h" #if YASL_HAS_WAYLAND #include "yasl/native/wayland.h" #endif @@ -39,6 +40,21 @@ int main() assert(yasl_loop_step(loop, 0) >= 0); yasl_loop_destroy(loop); + yasl_message message{}; + assert(!yasl_display_post(nullptr, &message)); + assert(!yasl_frame_present(nullptr, nullptr, 0)); + assert(!yasl_display_poll_event(nullptr, nullptr)); + assert(!yasl_surface_set_size(nullptr, {640, 480})); + assert(!yasl_surface_set_fullscreen(nullptr, true)); + assert(!yasl_surface_set_maximized(nullptr, true)); + assert(!yasl_surface_minimize(nullptr)); + assert(!yasl_surface_set_pacing(nullptr, YASL_PACING_CONTINUOUS)); + assert(!yasl_surface_set_callbacks(nullptr, nullptr, nullptr)); + assert(!yasl_clipboard_set(nullptr, YASL_CLIPBOARD, "")); + assert(!yasl_clipboard_request(nullptr, YASL_CLIPBOARD)); + assert(!yasl_app_window(nullptr)); + yasl_app_exit(nullptr, YASL_APP_ERROR); + yasl_event event{}; event.key.key = YASL_KEY_A; assert(event.key.key == YASL_KEY_A); diff --git a/tests/wayland/test_app_input.c b/tests/wayland/test_app_input.c index 88f9ead..7e0e9a0 100644 --- a/tests/wayland/test_app_input.c +++ b/tests/wayland/test_app_input.c @@ -5,9 +5,11 @@ #include "yasl/app.h" #include +#include #include struct test_state { + bool initialized; int keys; int buttons, motions, resets; int end; @@ -26,21 +28,20 @@ static void push_key(struct yasl_surface *s) static enum yasl_app_result on_init(void *ud, struct yasl_display *d) { struct test_state *state = ud; + state->initialized = true; if (state->end == 3) { /* With no event callback, a close request alone stops the runner. */ struct yasl_surface *s = yasl_surface_create_toplevel(d, &(struct yasl_window_spec){ .title = "App Close Test", .size = {64, 64} }); assert(s); - struct wayland_surface *w = (struct wayland_surface *)s; - w->cbs->close(s, w->ud); + yasl_surface_notify_close(s); return YASL_APP_CONTINUE; } if (state->end) { - struct wayland_display *display = (struct wayland_display *)d; if (state->end == 1) - display->cbs->quit(display->ud); + yasl_display_notify_event(d, YASL_EVENT_DISPLAY_QUIT); else - display->cbs->lost(display->ud); + yasl_display_notify_event(d, YASL_EVENT_DISPLAY_LOST); return YASL_APP_CONTINUE; } struct yasl_surface *s = yasl_surface_create_toplevel(d, @@ -106,6 +107,12 @@ int main(void) .init = on_init, .event = on_event, }); + /* A display variable may exist even when the test sandbox cannot connect + * to that compositor. Keep this as an integration test in that case. */ + if (res < 0 && !state.initialized && + (errno == EACCES || errno == EPERM || errno == ENOENT || + errno == ECONNREFUSED || errno == ENODEV)) + return 77; assert(res == 0); assert(state.keys == 1); assert(state.buttons == 1 && state.motions == 2 && state.resets == 1); diff --git a/tests/wayland/test_clipboard.c b/tests/wayland/test_clipboard.c index ff84209..1778ad2 100644 --- a/tests/wayland/test_clipboard.c +++ b/tests/wayland/test_clipboard.c @@ -1,24 +1,46 @@ #include "../../src/platform/wayland/clipboard.c" +#include "common/loop.h" #include -struct received { int count; size_t len; char text[32]; }; +struct received { + int count; + size_t len; + uint64_t request_id; + enum yasl_transfer_status status; + struct clipboard *clipboard; + bool forget_on_receive; + char text[32]; +}; static void receive(const char *text, size_t len, bool uri_list, - struct wl_surface *surface, void *ud) + struct wl_surface *surface, uint64_t request_id, + enum yasl_transfer_status status, void *ud) { (void)surface; struct received *r = ud; assert(!uri_list && len < sizeof(r->text)); - memcpy(r->text, text, len); - r->text[len] = '\0'; + if (status == YASL_TRANSFER_OK) { + assert(text); + memcpy(r->text, text, len); + r->text[len] = '\0'; + } else { + assert(!text && len == 0); + } r->len = len; + r->request_id = request_id; + r->status = status; r->count++; + if (r->forget_on_receive) { + r->forget_on_receive = false; + clipboard_forget_surface(r->clipboard, surface); + } } int main(void) { struct received received = {0}; struct clipboard c = {.loop = yasl_loop_create(), .receive = receive, .ud = &received}; + received.clipboard = &c; assert(c.loop); const struct { const char *bytes; size_t len; bool valid; } cases[] = { {"é字😀", sizeof("é字😀") - 1, true}, @@ -28,20 +50,61 @@ int main(void) }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { received = (struct received){0}; + received.clipboard = &c; int fds[2]; assert(pipe(fds) == 0); - assert(start_transfer_text(&c, fds[0], false, false, c.text)); + struct transfer *transfer = start_transfer_text(&c, fds[0], false, false, c.text); + assert(transfer); + yasl_timer_id timeout_id = transfer->timeout; + assert(timeout_id && yasl_loop_timer_armed(c.loop, timeout_id)); + transfer->paste = true; + transfer->request_id = 42; 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); + assert(!yasl_loop_timer_armed(c.loop, timeout_id)); + assert(received.count == 1); + assert(received.request_id == 42); + assert(received.status == (cases[i].valid ? YASL_TRANSFER_OK : YASL_TRANSFER_FAILED)); if (cases[i].valid) { assert(received.len == cases[i].len); assert(memcmp(received.text, cases[i].bytes, received.len) == 0); } } + received = (struct received){0}; + received.clipboard = &c; + int cancel_fds[2]; + assert(pipe(cancel_fds) == 0); + struct transfer *pending = start_transfer_text(&c, cancel_fds[0], false, false, NULL); + assert(pending); + yasl_timer_id cancel_timeout_id = pending->timeout; + assert(cancel_timeout_id && yasl_loop_timer_armed(c.loop, cancel_timeout_id)); + pending->paste = true; + pending->request_id = 99; + struct wl_surface *target = (struct wl_surface *)(uintptr_t)1; + pending->drop_surface = target; + clipboard_forget_surface(&c, target); + close(cancel_fds[1]); + assert(!c.transfers && received.count == 0); + assert(!yasl_loop_timer_armed(c.loop, cancel_timeout_id)); + + received.forget_on_receive = true; + int timeout_fds[2]; + assert(pipe(timeout_fds) == 0); + struct transfer *timed = start_transfer_text(&c, timeout_fds[0], false, false, NULL); + assert(timed && timed->timeout); + timed->paste = true; + timed->request_id = 100; + struct wl_surface *timeout_target = (struct wl_surface *)(uintptr_t)2; + timed->drop_surface = timeout_target; + yasl_timer_id timeout_id = timed->timeout; + assert(yasl_loop_del_timer(c.loop, timeout_id)); + (void)transfer_timeout(timed, timeout_id, CLIPBOARD_TRANSFER_TIMEOUT_NS); + close(timeout_fds[1]); + assert(!c.transfers && received.count == 1); + assert(received.request_id == 100 && received.status == YASL_TRANSFER_FAILED); yasl_loop_destroy(c.loop); return 0; } diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c index 95943d1..c2eca37 100644 --- a/tests/x11/test_input.c +++ b/tests/x11/test_input.c @@ -1,5 +1,7 @@ /* An isolated real X server supplies the keymap; disabling this surface's XIC * deliberately exercises text insertion when no input method is available. */ +#define _POSIX_C_SOURCE 200809L +#include #include "../../src/platform/x11/window.c" static void check_selection_encoding(struct x11_display *d, struct x11_surface *s) @@ -35,34 +37,72 @@ static void check_selection_encoding(struct x11_display *d, struct x11_surface * 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}, + /* Simulate legacy STRING, empty UTF-8, malformed UTF-8, and rejected INCR. */ + const struct { + Atom type; + const void *bytes; + size_t len; + int format; + const char *expected; + enum yasl_transfer_status status; + } cases[] = { + {XA_STRING, "caf\xe9 \xa3", 6, 8, "café £", YASL_TRANSFER_OK}, + {d->utf8, "", 0, 8, "", YASL_TRANSFER_OK}, + {d->utf8, "\xff", 1, 8, NULL, YASL_TRANSFER_FAILED}, + {d->incr, NULL, 1, 32, NULL, YASL_TRANSFER_FAILED}, }; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { struct paste_request *pending = calloc(1, sizeof(*pending)); assert(pending); + pending->display = d; pending->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), 0, 0, 1, 1, 0, 0, 0); pending->target = s; + pending->id = (uint64_t)i + 1; + pending->timeout = yasl_loop_add_timer(d->loop, PASTE_TIMEOUT_NS, + paste_timeout, pending); + assert(pending->timeout); 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); + unsigned long incr_bytes = 4096; + const void *bytes = cases[i].bytes; + if (cases[i].type == d->incr) bytes = &incr_bytes; + XChangeProperty(d->native, pending->window, property, cases[i].type, + cases[i].format, PropModeReplace, bytes, (int)cases[i].len); XSelectionEvent reply = {.requestor = pending->window, .property = property}; selection_notify(d, &reply); struct yasl_event event; - if (cases[i].expected) { - assert(yasl_surface_poll_event(&s->base, &event)); - assert(event.type == YASL_EVENT_PASTE); + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == i + 1); + assert(event.paste.status == cases[i].status); + if (cases[i].status == YASL_TRANSFER_OK) { assert(event.text.len == strlen(cases[i].expected)); assert(strcmp(event.text.data, cases[i].expected) == 0); } + if (cases[i].type == d->incr) { + assert(d->pastes == pending && !pending->target); + assert(yasl_loop_del_timer(d->loop, pending->timeout)); + pending->timeout = 0; + paste_timeout(pending, 0, PASTE_TIMEOUT_NS); + } assert(!yasl_surface_poll_event(&s->base, &event)); } + + struct paste_request *rejected = calloc(1, sizeof(*rejected)); + assert(rejected); + rejected->window = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + rejected->target = s; + rejected->selection = d->clipboard; + rejected->id = 99; + rejected->next = d->pastes; + d->pastes = rejected; + XSelectionEvent no_property = {.requestor = rejected->window, .property = None}; + selection_notify(d, &no_property); + struct yasl_event event; + assert(yasl_surface_poll_event(&s->base, &event)); + assert(event.type == YASL_EVENT_PASTE && event.paste.request_id == 99); + assert(event.paste.status == YASL_TRANSFER_FAILED); } /* Own the XSETTINGS selection with one Net/DoubleClickTime value. */ @@ -96,6 +136,173 @@ static void check_double_click(struct x11_display *d) assert(yasl_display_double_click_ms(&d->base) == YASL_DOUBLE_CLICK_MS_DEFAULT); } +static struct yasl_event wait_paste(struct yasl_display *display, uint64_t id) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_PASTE && event.paste.request_id == id) + return event; + assert(!"expected paste completion was not queued"); + return (struct yasl_event){0}; +} + +static void check_request_completion(struct x11_display *d, struct x11_surface *s) +{ + assert(surface_input_queue(&s->base) == &s->input); + assert(copy_text(&d->base, YASL_CLIPBOARD, "")); + XSync(d->native, False); + assert(XGetSelectionOwner(d->native, d->clipboard) == d->selection_window); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 101)); + struct yasl_event event = wait_paste(&d->base, 101); + assert(event.paste.status == YASL_TRANSFER_OK && event.paste.len == 0); + assert(event.paste.data && !event.paste.data[0]); + + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 102)); + XSync(d->native, False); + dispatch(&d->base); + event = wait_paste(&d->base, 102); + assert(event.paste.status == YASL_TRANSFER_UNAVAILABLE); + + Window silent_owner = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + XSetSelectionOwner(d->native, d->clipboard, silent_owner, CurrentTime); + XSync(d->native, False); + assert(request_paste(&d->base, YASL_CLIPBOARD, &s->base, 103)); + struct paste_request *timed = d->pastes; + assert(timed && timed->id == 103 && timed->timeout); + assert(yasl_loop_del_timer(d->loop, timed->timeout)); + timed->timeout = 0; + paste_timeout(timed, 0, PASTE_TIMEOUT_NS); + event = wait_paste(&d->base, 103); + assert(event.paste.status == YASL_TRANSFER_FAILED); + + struct yasl_window_spec spec = {.title = "Cancelled paste", .size = {80, 60}}; + struct yasl_surface *cancel_target = yasl_surface_create_toplevel(&d->base, &spec); + assert(cancel_target); + assert(request_paste(&d->base, YASL_CLIPBOARD, cancel_target, 104)); + assert(d->pastes && d->pastes->target == (struct x11_surface *)cancel_target); + yasl_timer_id timeout = d->pastes->timeout; + yasl_surface_destroy(cancel_target); + assert(d->pastes && !d->pastes->target && d->pastes->id == 0); + assert(yasl_loop_del_timer(d->loop, timeout)); + d->pastes->timeout = 0; + XSelectionEvent late_reply = { + .requestor = d->pastes->window, + .selection = d->pastes->selection, + .property = None, + }; + selection_notify(d, &late_reply); + assert(!d->pastes); + XDestroyWindow(d->native, silent_owner); + XSetSelectionOwner(d->native, d->clipboard, None, CurrentTime); + XSync(d->native, False); +} + +static struct yasl_event next_composition(struct yasl_display *display) +{ + struct yasl_event event; + for (int i = 0; i < 256 && yasl_display_poll_event(display, &event); i++) + if (event.type == YASL_EVENT_COMPOSITION) + return event; + assert(!"expected composition snapshot was not queued"); + return (struct yasl_event){0}; +} + +static void drain_display_events(struct yasl_display *display) +{ + struct yasl_event event; + while (yasl_display_poll_event(display, &event)) {} +} + +static void check_xim_callbacks(struct yasl_display *display, struct x11_surface *s) +{ + const char *old_locale = setlocale(LC_CTYPE, NULL); + char *saved_locale = old_locale ? strdup(old_locale) : NULL; + const char *utf8_locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!utf8_locale) utf8_locale = setlocale(LC_CTYPE, "C.utf8"); + assert(utf8_locale); + + drain_display_events(display); + assert(xim_preedit_start(NULL, (XPointer)s, NULL) == -1); + + wchar_t initial[] = {L'A', (wchar_t)0x1f600, L'B'}; + XIMText wide = {.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + XIMPreeditDrawCallbackStruct draw = { + .caret = 2, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "A😀B") == 0); + struct yasl_event event = next_composition(display); + assert(event.composition.active && event.composition.len == 6); + assert(event.composition.begin == 5 && event.composition.end == 5); + + /* XIMText.length counts characters. The data need not have a terminator. */ + char multibyte[] = {(char)0xc3, (char)0xa9, 'Z', 'x'}; + XIMText mb = {.length = 2, .encoding_is_wchar = False, + .string.multi_byte = multibyte}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 3, .chg_first = 1, .chg_length = 1, .text = &mb, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(s->ime_preedit && strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 4); + assert(event.composition.end == 4); + + /* A NULL text string updates feedback/caret while preserving the snapshot. */ + XIMText feedback = {.length = 4, .encoding_is_wchar = False, + .string.multi_byte = NULL}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 4, .text = &feedback, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + event = next_composition(display); + assert(event.composition.active && event.composition.begin == 1); + + char malformed[] = {(char)0xff, 'x'}; + XIMText invalid = {.length = 1, .encoding_is_wchar = False, + .string.multi_byte = malformed}; + draw.text = &invalid; + draw.chg_first = 0; + draw.chg_length = 0; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + assert(strcmp(s->ime_preedit, "AéZB") == 0); + struct yasl_event unused; + assert(!yasl_display_poll_event(display, &unused)); + + wchar_t empty[] = {0}; + XIMText deletion = {.length = 0, .encoding_is_wchar = True, + .string.wide_char = empty}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 0, .chg_first = 0, .chg_length = 4, .text = &deletion, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + event = next_composition(display); + assert(!event.composition.active && event.composition.len == 0); + assert(!s->ime_preedit); + + /* Done also publishes an explicit end snapshot. */ + wide = (XIMText){.length = 3, .encoding_is_wchar = True, + .string.wide_char = initial}; + draw = (XIMPreeditDrawCallbackStruct){ + .caret = 1, .chg_first = 0, .chg_length = 0, .text = &wide, + }; + xim_preedit_draw(NULL, (XPointer)s, (XPointer)&draw); + (void)next_composition(display); + xim_preedit_done(NULL, (XPointer)s, NULL); + event = next_composition(display); + assert(!event.composition.active && !s->ime_preedit); + + if (saved_locale) { + setlocale(LC_CTYPE, saved_locale); + free(saved_locale); + } +} + int main(void) { struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; @@ -126,10 +333,13 @@ int main(void) XSync(d->native, False); dispatch(base); input_queue_clear(&s->input); + /* Native creation and keymap updates may already have queued focus or + * reset input on the shared display stream. Isolate this synthetic key. */ + struct yasl_event event; + while (yasl_surface_poll_event(surface, &event)) {} XKeyEvent native = {.type = KeyPress, .display = d->native, .window = s->window, .keycode = code}; key_event(s, &native, false); - struct yasl_event event; assert(yasl_surface_poll_event(surface, &event)); assert(event.type == YASL_EVENT_KEY && event.key.key == YASL_KEY_A); assert(event.key.codepoint == cases[i].label); @@ -149,6 +359,8 @@ int main(void) s->ic = saved_ic; check_selection_encoding(d, s); check_double_click(d); + check_request_completion(d, s); + check_xim_callbacks(base, s); /* Test CSD interactive actions, window menu, and IME caret */ assert(yasl_display_caps(base) & YASL_CAP_CSD); diff --git a/tests/x11/test_tablet_native.c b/tests/x11/test_tablet_native.c index b0d1366..706a5cc 100644 --- a/tests/x11/test_tablet_native.c +++ b/tests/x11/test_tablet_native.c @@ -57,14 +57,8 @@ static int connect_tool(const char *path) static void native_draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) { - struct yasl_frame_context context = { - .surface = surface, .size = frame->target.size, - .scale_120 = yasl_surface_geometry(surface).scale_120, - .cpu = {.pixels = frame->target.pixels, - .stride_bytes = frame->target.size.x * (int)sizeof(uint32_t)}, - .internal_handle = frame, - }; - draw(ud, surface, &context); + (void)surface; + draw(ud, surface, frame); } static void native_configure(struct yasl_surface *surface, struct yasl_geometry geometry, void *ud) -- cgit v1.2.3