From 624d1371304a5ae4b3429ad5e4267aac7cd03dfd Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 00:09:38 +0300 Subject: missing impls and hardening --- tests/cocoa/test_native.c | 14 +++- tests/cocoa/test_ownership.m | 45 +++++++++++ tests/common/test_backends.c | 92 +++++++++++++++++++-- tests/common/test_contracts.c | 175 ++++++++++++++++++++++++++++++++++++++++ tests/common/test_input_alloc.c | 48 +++++++++++ tests/meson.build | 28 +++++++ tests/wayland/test_input.c | 26 ++++++ 7 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 tests/cocoa/test_ownership.m create mode 100644 tests/common/test_contracts.c create mode 100644 tests/common/test_input_alloc.c create mode 100644 tests/wayland/test_input.c (limited to 'tests') diff --git a/tests/cocoa/test_native.c b/tests/cocoa/test_native.c index 03fde88..41df79a 100644 --- a/tests/cocoa/test_native.c +++ b/tests/cocoa/test_native.c @@ -19,7 +19,16 @@ static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *u struct yasl_vector2i size = yasl_surface_size(surface); assert(size.x == frame->target.size.x && size.y == frame->target.size.y); for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0; - struct yasl_damage_list damage = {.n = -1}; + struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1}; + assert(!yasl_surface_present(surface, &frame->target, &damage)); + damage.n = -1; + struct yasl_framebuffer invalid = frame->target; + invalid.buffer++; + assert(!yasl_surface_present(surface, &invalid, &damage)); + invalid = frame->target; + invalid.generation--; + assert(!yasl_surface_present(surface, &invalid, &damage)); + assert(!yasl_surface_present(surface, &frame->target, NULL)); assert(yasl_surface_present(surface, &frame->target, &damage)); assert(!yasl_surface_present(surface, &frame->target, &damage)); state->draws++; @@ -36,8 +45,9 @@ static void test_window(enum yasl_backend request, bool null_spec) struct yasl_surface_spec surface = {.title = "Native backend test", .size = {160, 100}}; state.surface = yasl_surface_create_toplevel(state.display, &surface); assert(state.surface); - static const struct yasl_surface_cbs callbacks = {.draw = draw}; + struct yasl_surface_cbs callbacks = {.draw = draw}; yasl_surface_set_callbacks(state.surface, &callbacks, &state); + callbacks.draw = NULL; yasl_surface_schedule_redraw(state.surface); uint64_t deadline = yasl_loop_now_ns() + 3000000000ull; while (!state.draws && yasl_loop_now_ns() < deadline) { diff --git a/tests/cocoa/test_ownership.m b/tests/cocoa/test_ownership.m new file mode 100644 index 0000000..aa75b1c --- /dev/null +++ b/tests/cocoa/test_ownership.m @@ -0,0 +1,45 @@ +#import +#include "yasl/yasl.h" +#include +#include +#include + +int main(void) +{ + struct yasl_display *d = yasl_display_open(NULL, NULL); + assert(d); + struct yasl_surface_spec spec = {.title = "Ownership test", .size = {160, 100}}; + struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec); + assert(s); + struct yasl_input_event event; + while (yasl_surface_input_poll(s, &event)) {} + @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); + memset(text, 'x', sizeof(text)); + } + } + /* All NSString/UTF8String temporaries have now expired. */ + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "paste-%d", i); + assert(yasl_surface_input_poll(s, &event)); + assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0); + } + const char *last = event.paste.text; + assert(!yasl_surface_input_poll(s, &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_input_poll(s, &event)); + assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]); + yasl_surface_destroy(s); + yasl_display_close(d); + return 0; +} diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c index 48a6b68..3372290 100644 --- a/tests/common/test_backends.c +++ b/tests/common/test_backends.c @@ -67,7 +67,24 @@ static void draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) assert(size.x == frame->target.size.x && size.y == frame->target.size.y); assert(frame->target.pixels); for (int i = 0; i < size.x * size.y; i++) frame->target.pixels[i] = 0xff4080c0; - struct yasl_damage_list damage = {.n = -1}; + struct yasl_damage_list damage = {.n = YASL_DAMAGE_MAX + 1}; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = -2; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = 1; + damage.rects[0].width = -1; + assert(!yasl_surface_present(s, &frame->target, &damage)); + damage.n = -1; + struct yasl_framebuffer invalid = frame->target; + invalid.buffer++; + assert(!yasl_surface_present(s, &invalid, &damage)); + invalid = frame->target; + invalid.generation--; + assert(!yasl_surface_present(s, &invalid, &damage)); + invalid = frame->target; + invalid.native_drawable = (void *)(uintptr_t)1; + assert(!yasl_surface_present(s, &invalid, &damage)); + assert(!yasl_surface_present(s, &frame->target, NULL)); assert(yasl_surface_present(s, &frame->target, &damage)); assert(!yasl_surface_present(s, &frame->target, &damage)); w->draws++; @@ -98,12 +115,22 @@ static const struct yasl_surface_cbs callbacks = { .draw = draw, .configure = configure, .close = close_window, }; +static void attach(struct window *w) +{ + struct yasl_surface_cbs temporary = callbacks; + yasl_surface_set_callbacks(w->surface, &temporary, w); + temporary.draw = NULL; + temporary.configure = NULL; + temporary.close = NULL; +} + static struct window create(struct yasl_display *d) { struct yasl_surface_spec spec = {.title = "Backend test", .size = {160, 100}}; struct yasl_surface *s = yasl_surface_create_toplevel(d, &spec); assert(s); assert(yasl_surface_display(s) == d); + assert(yasl_surface_timing(s).source == YASL_TIMING_UNKNOWN); return (struct window){.surface = s, .display = d}; } @@ -153,6 +180,61 @@ static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct } } assert(saw_key && saw_paste); + assert(yasl_surface_timing(w->surface).source == YASL_TIMING_SUBMISSION); + assert(!(yasl_display_caps(d) & YASL_CAP_FRAME_PACING)); + + /* Reuse caller storage, replace the selection, and exceed the old queue + * cap before polling: each paste must retain its own original contents. */ + for (int i = 0; i < 300; i++) { + char text[32]; + snprintf(text, sizeof(text), "owned-%d", i); + yasl_display_copy(d, YASL_CLIPBOARD, text); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + memset(text, 'x', sizeof(text)); + } + for (int i = 0; i < 300; i++) { + char text[32]; + snprintf(text, sizeof(text), "owned-%d", i); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_PASTE && strcmp(event.paste.text, text) == 0); + } + const char *last = event.paste.text; + assert(!yasl_surface_input_poll(w->surface, &event)); + yasl_display_copy(d, YASL_CLIPBOARD, "changed"); + pump(loop, d, NULL); + assert(strcmp(last, "owned-299") == 0); + yasl_display_copy(d, YASL_CLIPBOARD, ""); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + assert(yasl_surface_input_poll(w->surface, &event)); + assert(event.kind == YASL_INPUT_PASTE && !event.paste.len && !event.paste.text[0]); + + /* Concurrent external conversions must not share a mutable target slot. */ + struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; + struct yasl_display *owner = yasl_display_open(loop, &ds); + assert(owner); + yasl_display_copy(owner, YASL_CLIPBOARD, "external"); + struct window other = create(d), cancelled = create(d); + yasl_display_paste(d, YASL_CLIPBOARD, w->surface); + yasl_display_paste(d, YASL_CLIPBOARD, other.surface); + yasl_display_paste(d, YASL_CLIPBOARD, cancelled.surface); + yasl_surface_destroy(cancelled.surface); + pump(loop, d, owner); + int first_pastes = 0, second_pastes = 0; + while (yasl_surface_input_poll(w->surface, &event)) { + if (event.kind == YASL_INPUT_PASTE) { + assert(strcmp(event.paste.text, "external") == 0); + first_pastes++; + } + } + while (yasl_surface_input_poll(other.surface, &event)) { + if (event.kind == YASL_INPUT_PASTE) { + assert(strcmp(event.paste.text, "external") == 0); + second_pastes++; + } + } + assert(first_pastes == 1 && second_pastes == 1); + yasl_surface_destroy(other.surface); + yasl_display_close(owner); key.xclient = (XClientMessageEvent){.type = ClientMessage, .display = native, .window = window, .message_type = XInternAtom(native, "WM_PROTOCOLS", False), .format = 32, .data.l = {(long)XInternAtom(native, "WM_DELETE_WINDOW", False)}}; @@ -177,8 +259,8 @@ int main(int argc, char **argv) struct yasl_display *b = yasl_display_open(loop, &spec); assert(a && b); struct window wa = create(a), wb = create(b); - yasl_surface_set_callbacks(wa.surface, &callbacks, &wa); - yasl_surface_set_callbacks(wb.surface, &callbacks, &wb); + attach(&wa); + attach(&wb); pump(loop, a, b); assert(wa.draws && wb.draws); yasl_surface_destroy(wa.surface); @@ -195,7 +277,7 @@ int main(int argc, char **argv) struct window w = create(d); pump(loop, d, NULL); assert(w.draws == 0); - yasl_surface_set_callbacks(w.surface, &callbacks, &w); + attach(&w); yasl_surface_schedule_redraw(w.surface); pump(loop, d, NULL); assert(w.draws > 0); @@ -204,7 +286,7 @@ int main(int argc, char **argv) if (w.surface) yasl_surface_destroy(w.surface); w = create(d); w.destroy_on_draw = true; - yasl_surface_set_callbacks(w.surface, &callbacks, &w); + attach(&w); pump(loop, d, NULL); assert(w.draws && !w.surface); yasl_display_close(d); diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c new file mode 100644 index 0000000..65eebcd --- /dev/null +++ b/tests/common/test_contracts.c @@ -0,0 +1,175 @@ +#include "common/backend.h" +#include "common/input.h" + +#include +#include +#include +#include +#include + +static void test_queue(void) +{ + struct input_queue q = {0}; + struct yasl_surface surface = {0}; + const enum yasl_input_kind kinds[] = { + YASL_INPUT_TEXT, YASL_INPUT_PASTE, YASL_INPUT_DROP_FILES, + }; + for (size_t k = 0; k < sizeof(kinds) / sizeof(kinds[0]); k++) { + char *source = malloc(4); + assert(source); + memcpy(source, "a\0b", 4); + struct yasl_input_event in = {.surface = &surface, .kind = kinds[k]}; + if (in.kind == YASL_INPUT_TEXT) { in.text.text = source; in.text.len = 3; } + if (in.kind == YASL_INPUT_PASTE) { in.paste.text = source; in.paste.len = 3; } + if (in.kind == YASL_INPUT_DROP_FILES) { in.drop_files.files = source; in.drop_files.len = 3; } + assert(input_queue_push(&q, &in)); + memset(source, 'x', 4); + free(source); + struct yasl_input_event out; + assert(input_queue_pop(&q, &out)); + const char *payload = out.kind == YASL_INPUT_TEXT ? out.text.text : + out.kind == YASL_INPUT_PASTE ? out.paste.text : out.drop_files.files; + assert(out.surface == &surface && out.kind == kinds[k]); + assert(memcmp(payload, "a\0b", 4) == 0); + assert(!input_queue_pop(&q, &out)); + assert(memcmp(payload, "a\0b", 4) == 0); + /* Growth/wraparound must not invalidate a previously polled payload. */ + for (unsigned i = 0; i < 4096; i++) { + in = (struct yasl_input_event){.kind = YASL_INPUT_KEY, .key.key = i}; + assert(input_queue_push(&q, &in)); + } + assert(memcmp(payload, "a\0b", 4) == 0); + for (unsigned i = 0; i < 2048; i++) { + assert(input_queue_pop(&q, &out)); + assert(out.key.key == i); + } + for (unsigned i = 4096; i < 6144; i++) { + in.key.key = i; + assert(input_queue_push(&q, &in)); + } + for (unsigned i = 2048; i < 6144; i++) { + assert(input_queue_pop(&q, &out)); + assert(out.key.key == i); + } + assert(!input_queue_pop(&q, &out)); + } + struct yasl_input_event empty = {.surface = &surface, .kind = YASL_INPUT_PASTE}; + struct yasl_input_event out; + assert(input_queue_push(&q, &empty)); + assert(input_queue_pop(&q, &out)); + assert(out.paste.text && !out.paste.len && out.paste.text[0] == '\0'); + struct yasl_input_event in = {.surface = &surface, .kind = YASL_INPUT_TEXT, + .text = {"owned", 5}}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out)); + const char *last = out.text.text; + assert(input_queue_push(&q, &in)); + /* Exercise the allocation-failure reset path without allocating huge RAM. */ + in.text.len = SIZE_MAX; + assert(!input_queue_push(&q, &in)); + assert(strcmp(last, "owned") == 0); + in = (struct yasl_input_event){.surface = &surface, .kind = YASL_INPUT_KEY}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(out.surface == &surface); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_KEY); + assert(!input_queue_pop(&q, &out)); + in = (struct yasl_input_event){.kind = YASL_INPUT_PASTE, .paste = {"pending", 7}}; + assert(input_queue_push(&q, &in)); + input_queue_clear(&q); + input_queue_clear(&q); + assert(!q.events && !q.last_text && !q.count && !q.reset_pending); +} + +static int presents, copies, pastes, creates; +static struct yasl_damage_list received; +static bool present(struct yasl_surface *s, const struct yasl_framebuffer *fb, + const struct yasl_damage_list *damage) +{ + (void)s; (void)fb; + presents++; + received = *damage; + return true; +} +static uint32_t caps(const struct yasl_display *d) { (void)d; return 0; } +static void copy(struct yasl_display *d, enum yasl_selection sel, const char *text) +{ (void)d; (void)sel; (void)text; copies++; } +static void paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *s) +{ (void)d; (void)sel; (void)s; pastes++; } +static struct yasl_surface *create(struct yasl_display *d, const struct yasl_surface_spec *spec) +{ (void)d; (void)spec; creates++; return NULL; } + +static void test_validation(void) +{ + const struct yasl_backend_ops ops = {.surface_present = present, + .display_caps = caps, .display_copy = copy, .display_paste = paste, + .surface_create_toplevel = create}; + struct yasl_display d = {.ops = &ops}; + struct yasl_surface s = {.display = &d, .frame_generation = 1}; + struct yasl_framebuffer fb = {.generation = 1, .size = {100, 80}}; + struct yasl_damage_list damage = {.n = -2}; + assert(!yasl_surface_present(&s, &fb, &damage)); + damage.n = YASL_DAMAGE_MAX + 1; + assert(!yasl_surface_present(&s, &fb, &damage)); + damage.n = 1; + damage.rects[0] = (struct yasl_rect2i){0, 0, -1, 1}; + assert(!yasl_surface_present(&s, &fb, &damage)); + assert(!yasl_surface_present(&s, &fb, NULL)); + assert(!presents); + damage.n = -1; + fb.generation = 0; + assert(!yasl_surface_present(&s, &fb, &damage)); + fb.generation = 2; + assert(!yasl_surface_present(&s, &fb, &damage)); + fb.generation = 1; + damage.n = 4; + damage.rects[0] = (struct yasl_rect2i){-10, -10, 30, 30}; + damage.rects[1] = (struct yasl_rect2i){90, 70, INT_MAX, INT_MAX}; + damage.rects[2] = (struct yasl_rect2i){INT_MAX, INT_MAX, INT_MAX, INT_MAX}; + damage.rects[3] = (struct yasl_rect2i){INT_MIN, INT_MIN, INT_MAX, INT_MAX}; + assert(yasl_surface_present(&s, &fb, &damage)); + assert(received.n == 2 && received.rects[0].x == 0 && received.rects[0].width == 20); + assert(received.rects[1].width == 10 && received.rects[1].height == 10); + assert(damage.n == 4 && damage.rects[0].x == -10); + damage.n = -1; + assert(yasl_surface_present(&s, &fb, &damage) && received.n == -1); + damage.n = 0; + assert(yasl_surface_present(&s, &fb, &damage) && received.n == 0); + yasl_display_copy(&d, YASL_PRIMARY, "no alias"); + yasl_display_copy(&d, (enum yasl_selection)255, "invalid"); + yasl_display_paste(&d, YASL_PRIMARY, &s); + yasl_display_paste(&d, (enum yasl_selection)255, &s); + assert(!copies && !pastes); + yasl_display_copy(&d, YASL_CLIPBOARD, "yes"); + yasl_display_paste(&d, YASL_CLIPBOARD, &s); + assert(copies == 1 && pastes == 1); + struct yasl_surface_spec spec = {.size = {100, 80}, .graphics_device = &d}; + assert(!yasl_surface_create_toplevel(&d, &spec) && errno == ENOTSUP); + spec.graphics_device = NULL; + spec.size.x = 0; + assert(!yasl_surface_create_toplevel(&d, &spec) && errno == EINVAL); + assert(!creates); +} + +static void test_scale(void) +{ + assert(yasl_scale_to_device(-10, 120) == -10); + assert(yasl_scale_to_device(-0.5, 120) == -1); + assert(yasl_scale_to_device(0.5, 120) == 1); + assert(yasl_scale_to_device(NAN, 120) == 0); + assert(yasl_scale_to_device(INFINITY, 120) == 0); + assert(yasl_scale_to_device(DBL_MAX, 240) == INT_MAX); + assert(yasl_scale_to_logical(-1, 240) == -1); + assert(yasl_scale_to_logical(1, 240) == 1); + assert(yasl_scale_to_logical(INT_MAX, 1) == INT_MAX); + assert(yasl_scale_to_logical(INT_MIN, 1) == INT_MIN); + assert(yasl_scale_to_logical(1, 0) == 0); +} + +int main(void) +{ + test_queue(); + test_validation(); + test_scale(); + return 0; +} diff --git a/tests/common/test_input_alloc.c b/tests/common/test_input_alloc.c new file mode 100644 index 0000000..e311dc0 --- /dev/null +++ b/tests/common/test_input_alloc.c @@ -0,0 +1,48 @@ +#include "common/input.h" +#include +#include +#include + +/* This executable compiles input.c with malloc redirected here. */ +static int allocations = -1; +void *yasl_test_malloc(size_t bytes) +{ + if (allocations == 0) return NULL; + if (allocations > 0) allocations--; + return calloc(1, bytes); +} + +int main(void) +{ + struct yasl_surface *surface = (struct yasl_surface *)(uintptr_t)1; + for (int failure = 0; failure < 2; failure++) { + struct input_queue q = {0}; + struct yasl_input_event in = {.surface = surface, .kind = YASL_INPUT_PASTE, + .paste = {"payload", 7}}, out; + /* Fail either payload allocation or the initial event-array allocation. */ + allocations = failure; + assert(!input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(out.surface == surface); + assert(!input_queue_pop(&q, &out)); + input_queue_clear(&q); + + allocations = -1; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out)); + const char *last = out.paste.text; + for (size_t i = 0; i < q.capacity; i++) assert(input_queue_push(&q, &in)); + allocations = failure; + assert(!input_queue_push(&q, &in)); + assert(strcmp(last, "payload") == 0); + assert(!q.count && q.reset_pending); + allocations = -1; + in = (struct yasl_input_event){.surface = surface, .kind = YASL_INPUT_KEY, + .key.action = YASL_KEY_RELEASE}; + assert(input_queue_push(&q, &in)); + assert(input_queue_pop(&q, &out) && out.kind == YASL_INPUT_RESET); + assert(input_queue_pop(&q, &out) && out.key.action == YASL_KEY_RELEASE); + input_queue_clear(&q); + } + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index 950f5f5..1fcac3e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -12,6 +12,22 @@ add_test_setup('msan') test('basic', test_exe) +contracts_test = executable( + 'yasl-contracts-test', + 'common/test_contracts.c', + include_directories: inc, + dependencies: yasl_dep, +) +test('contracts', contracts_test) + +input_alloc_test = executable( + 'yasl-input-alloc-test', + ['common/test_input_alloc.c', '../src/common/input.c'], + include_directories: inc, + c_args: ['-Dmalloc=yasl_test_malloc'], +) +test('input-allocation-failure', input_alloc_test) + selection_test = executable( 'yasl-selection-test', 'common/test_selection.c', @@ -19,12 +35,24 @@ selection_test = executable( ) test('selection', selection_test) +if has_wayland + ime_test = executable('yasl-ime-test', 'wayland/test_input.c', + include_directories: inc, + dependencies: yasl_dep, + ) + test('ime-ownership', ime_test) +endif + if get_option('integration_tests') if has_cocoa native_test = executable('yasl-native-test', 'cocoa/test_native.c', dependencies: yasl_dep, ) test('native', native_test, timeout: 20) + ownership_test = executable('yasl-ownership-test', 'cocoa/test_ownership.m', + dependencies: yasl_dep, + ) + test('native-ownership', ownership_test, timeout: 20) else if not has_wayland or not has_x11 error('Linux integration tests require Wayland and X11.') diff --git a/tests/wayland/test_input.c b/tests/wayland/test_input.c new file mode 100644 index 0000000..f421e6c --- /dev/null +++ b/tests/wayland/test_input.c @@ -0,0 +1,26 @@ +#include "wayland/internal.h" +#include + +int main(void) +{ + struct wayland_surface w = {0}; + struct seat seat = {.ime_focus = &w}; + wayland_surface_set_callbacks(&w, NULL, NULL); + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "commit-%d", i); + text_input_listener.commit_string(&seat, NULL, text); + text_input_listener.done(&seat, NULL, (uint32_t)i); + assert(!w.ime_commit); + } + for (int i = 0; i < 600; i++) { + char text[32]; + snprintf(text, sizeof(text), "commit-%d", i); + struct yasl_input_event event; + assert(wayland_surface_input_poll(&w, &event)); + assert(event.kind == YASL_INPUT_TEXT && event.surface == &w.base); + assert(strcmp(event.text.text, text) == 0); + } + input_queue_clear(&w.input); + return 0; +} -- cgit v1.2.3