From 6eac0a5f2c552728efca420b1129fae7d4fea773 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Sun, 11 Oct 2026 23:40:57 +0300 Subject: feat: backend selection (Wayland, X11, Cocoa) --- tests/cocoa/test_native.c | 58 ++++++++++++ tests/common/test_backends.c | 215 ++++++++++++++++++++++++++++++++++++++++++ tests/common/test_selection.c | 34 +++++++ tests/helpers/run_backends.py | 93 ++++++++++++++++++ tests/meson.build | 44 +++++++++ 5 files changed, 444 insertions(+) create mode 100644 tests/cocoa/test_native.c create mode 100644 tests/common/test_backends.c create mode 100644 tests/common/test_selection.c create mode 100644 tests/helpers/run_backends.py create mode 100644 tests/meson.build (limited to 'tests') diff --git a/tests/cocoa/test_native.c b/tests/cocoa/test_native.c new file mode 100644 index 0000000..03fde88 --- /dev/null +++ b/tests/cocoa/test_native.c @@ -0,0 +1,58 @@ +#define _POSIX_C_SOURCE 200809L +#include "yasl/yasl.h" + +#include +#include + +struct state { + struct yasl_display *display; + struct yasl_surface *surface; + unsigned draws; +}; + +static void draw(struct yasl_surface *surface, struct yasl_frame *frame, void *ud) +{ + struct state *state = ud; + assert(surface == state->surface && frame->surface == surface); + assert(yasl_surface_display(surface) == state->display); + assert(frame->target.pixels); + 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}; + assert(yasl_surface_present(surface, &frame->target, &damage)); + assert(!yasl_surface_present(surface, &frame->target, &damage)); + state->draws++; + yasl_surface_destroy(surface); + state->surface = NULL; +} + +static void test_window(enum yasl_backend request, bool null_spec) +{ + struct yasl_display_spec spec = {.backend = request}; + struct state state = {.display = yasl_display_open(NULL, null_spec ? NULL : &spec)}; + assert(state.display); + assert(yasl_display_backend(state.display) == YASL_BACKEND_COCOA); + 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}; + yasl_surface_set_callbacks(state.surface, &callbacks, &state); + yasl_surface_schedule_redraw(state.surface); + uint64_t deadline = yasl_loop_now_ns() + 3000000000ull; + while (!state.draws && yasl_loop_now_ns() < deadline) { + yasl_display_dispatch(state.display); + yasl_display_flush(state.display); + nanosleep(&(struct timespec){.tv_nsec = 1000000}, NULL); + } + assert(state.draws == 1 && !state.surface); + yasl_display_close(state.display); +} + +int main(void) +{ + test_window(YASL_BACKEND_AUTO, true); + test_window(YASL_BACKEND_AUTO, false); + test_window(YASL_BACKEND_COCOA, false); + return 0; +} diff --git a/tests/common/test_backends.c b/tests/common/test_backends.c new file mode 100644 index 0000000..48a6b68 --- /dev/null +++ b/tests/common/test_backends.c @@ -0,0 +1,215 @@ +#define _POSIX_C_SOURCE 200809L +#include "yasl/yasl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct window { + struct yasl_surface *surface; + struct yasl_display *display; + int draws, configures, closes; + bool destroy_on_draw, destroy_on_configure; +}; + +struct pump { + struct yasl_loop *loop; + struct yasl_display *displays[2]; + int ticks; +}; + +static int tick(int fd, short events, void *ud) +{ + (void)events; + uint64_t expirations; + assert(read(fd, &expirations, sizeof(expirations)) == sizeof(expirations)); + struct pump *p = ud; + for (int i = 0; i < 2; i++) { + if (p->displays[i]) { + yasl_display_dispatch(p->displays[i]); + yasl_display_flush(p->displays[i]); + } + } + if (--p->ticks == 0) yasl_loop_quit(p->loop); + return 0; +} + +static void pump(struct yasl_loop *loop, struct yasl_display *a, struct yasl_display *b) +{ + int timer = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + assert(timer >= 0); + struct pump p = {.loop = loop, .displays = {a, b}, .ticks = 20}; + struct itimerspec spec = {.it_value.tv_nsec = 10000000, .it_interval.tv_nsec = 10000000}; + assert(timerfd_settime(timer, 0, &spec, NULL) == 0); + int watch = yasl_loop_add_fd(loop, timer, POLLIN, tick, &p); + assert(watch > 0); + assert(yasl_loop_run(loop) == 0); + assert(yasl_loop_del_fd(loop, watch) == 0); + close(timer); +} + +static void draw(struct yasl_surface *s, struct yasl_frame *frame, void *ud) +{ + struct window *w = ud; + assert(s == w->surface); + assert(frame->surface == s); + assert(yasl_surface_display(s) == w->display); + struct yasl_vector2i size = yasl_surface_size(s); + 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}; + assert(yasl_surface_present(s, &frame->target, &damage)); + assert(!yasl_surface_present(s, &frame->target, &damage)); + w->draws++; + if (w->destroy_on_draw) { yasl_surface_destroy(s); w->surface = NULL; } +} + +static void configure(struct yasl_surface *s, struct yasl_geometry geometry, void *ud) +{ + struct window *w = ud; + assert(w->surface == s); + struct yasl_vector2i size = yasl_surface_size(s); + assert(geometry.size.x == size.x && geometry.size.y == size.y); + assert(geometry.scale_120 == yasl_surface_scale_120(s)); + w->configures++; + if (w->destroy_on_configure) { yasl_surface_destroy(s); w->surface = NULL; } +} + +static void close_window(struct yasl_surface *s, void *ud) +{ + struct window *w = ud; + assert(s == w->surface); + w->closes++; + yasl_surface_destroy(s); + w->surface = NULL; +} + +static const struct yasl_surface_cbs callbacks = { + .draw = draw, .configure = configure, .close = close_window, +}; + +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); + return (struct window){.surface = s, .display = d}; +} + +static void x11_behavior(struct yasl_loop *loop, struct yasl_display *d, struct window *w) +{ + Display *native = yasl_display_get_native_handle(d); + Window window = (Window)(uintptr_t)yasl_surface_get_native_handle(w->surface); + assert(native && window); + XSync(native, False); + XImage *image = XGetImage(native, window, 0, 0, 1, 1, AllPlanes, ZPixmap); + assert(image && (XGetPixel(image, 0, 0) & 0xffffff) == 0x4080c0); + XDestroyImage(image); + yasl_surface_set_title(w->surface, "Updated title"); + char *title = NULL; + assert(XFetchName(native, window, &title)); + assert(strcmp(title, "Updated title") == 0); + XFree(title); + XResizeWindow(native, window, 200, 120); + XFlush(native); + pump(loop, d, NULL); + assert(w->configures > 0); + struct yasl_vector2i size = yasl_surface_size(w->surface); + assert(size.x == 200 && size.y == 120); + XEvent key = {0}; + key.xkey = (XKeyEvent){.type = KeyPress, .display = native, .window = window, + .root = DefaultRootWindow(native), .same_screen = True, + .keycode = XKeysymToKeycode(native, XK_a)}; + XSendEvent(native, window, False, KeyPressMask, &key); + XFlush(native); + 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; + 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; + } + 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; + } + } + assert(saw_key && saw_paste); + 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)}}; + XSendEvent(native, window, False, NoEventMask, &key); + XFlush(native); + pump(loop, d, NULL); + assert(w->closes == 1 && !w->surface); +} + +int main(int argc, char **argv) +{ + assert(argc >= 2); + struct yasl_loop *loop = yasl_loop_create(); + assert(loop); + if (strcmp(argv[1], "failure") == 0) { + struct yasl_display_spec spec = {.backend = (enum yasl_backend)atoi(argv[2])}; + assert(!yasl_display_open(loop, &spec)); + } else if (strcmp(argv[1], "mixed") == 0) { + struct yasl_display_spec spec = {.backend = YASL_BACKEND_WAYLAND}; + struct yasl_display *a = yasl_display_open(loop, &spec); + spec.backend = YASL_BACKEND_X11; + 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); + pump(loop, a, b); + assert(wa.draws && wb.draws); + yasl_surface_destroy(wa.surface); + yasl_surface_destroy(wb.surface); + yasl_display_close(a); + yasl_display_close(b); + } else { + assert(argc == 4); + struct yasl_display_spec spec = {.backend = (enum yasl_backend)atoi(argv[2])}; + struct yasl_display *d = strcmp(argv[1], "null") == 0 ? + yasl_display_open(loop, NULL) : yasl_display_open(loop, &spec); + assert(d); + assert(yasl_display_backend(d) == (enum yasl_backend)atoi(argv[3])); + struct window w = create(d); + pump(loop, d, NULL); + assert(w.draws == 0); + yasl_surface_set_callbacks(w.surface, &callbacks, &w); + yasl_surface_schedule_redraw(w.surface); + pump(loop, d, NULL); + assert(w.draws > 0); + if (yasl_display_backend(d) == YASL_BACKEND_X11) + x11_behavior(loop, d, &w); + if (w.surface) yasl_surface_destroy(w.surface); + w = create(d); + w.destroy_on_draw = true; + yasl_surface_set_callbacks(w.surface, &callbacks, &w); + pump(loop, d, NULL); + assert(w.draws && !w.surface); + yasl_display_close(d); + } + yasl_loop_destroy(loop); + puts("Backend test passed."); + return 0; +} diff --git a/tests/common/test_selection.c b/tests/common/test_selection.c new file mode 100644 index 0000000..d1b3eb8 --- /dev/null +++ b/tests/common/test_selection.c @@ -0,0 +1,34 @@ +#define _POSIX_C_SOURCE 200809L +#include "yasl/yasl.h" + +#include +#include +#include + +int main(void) +{ + struct yasl_display_spec spec = {.backend = (enum yasl_backend)999}; + errno = 0; + assert(!yasl_display_open(NULL, &spec)); + assert(errno == EINVAL); + assert(yasl_display_backend(NULL) == YASL_BACKEND_AUTO); +#if !YASL_HAS_COCOA + spec.backend = YASL_BACKEND_COCOA; + errno = 0; + assert(!yasl_display_open(NULL, &spec)); + assert(errno == ENOSYS); +#endif +#if !YASL_HAS_WAYLAND + spec.backend = YASL_BACKEND_WAYLAND; + errno = 0; + assert(!yasl_display_open(NULL, &spec)); + assert(errno == ENOSYS); +#endif +#if !YASL_HAS_X11 + spec.backend = YASL_BACKEND_X11; + errno = 0; + assert(!yasl_display_open(NULL, &spec)); + assert(errno == ENOSYS); +#endif + return 0; +} diff --git a/tests/helpers/run_backends.py b/tests/helpers/run_backends.py new file mode 100644 index 0000000..9ad9b0d --- /dev/null +++ b/tests/helpers/run_backends.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Test public window behavior on isolated display servers.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time + + +def wait_for(process, ready): + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError("The display server stopped.") + if ready(): + return + time.sleep(0.02) + raise TimeoutError("The display server did not start.") + + +def main(): + weston, xwayland, test = sys.argv[1:] + servers = [] + with tempfile.TemporaryDirectory(prefix="yasl-backends-") as directory: + root = Path(directory) + env = os.environ.copy() + env.pop("WAYLAND_SOCKET", None) + env.update(XDG_RUNTIME_DIR=directory, WAYLAND_DISPLAY="yasl-test", + XDG_SESSION_TYPE="wayland") + with (root / "weston.log").open("w+") as wl_log, \ + (root / "xwayland.log").open("w+") as x_log: + try: + wl = subprocess.Popen([ + weston, "--backend=headless", "--renderer=pixman", + "--socket=yasl-test", "--idle-time=0", "--no-config", + ], env=env, stdout=wl_log, stderr=wl_log) + servers.append(wl) + wait_for(wl, lambda: (root / "yasl-test").is_socket()) + display_file = root / "display" + with display_file.open("w") as display_fd: + x = subprocess.Popen([ + xwayland, "-displayfd", str(display_fd.fileno()), + "-shm", "-noreset", "-nolisten", "tcp", + ], env=env, pass_fds=(display_fd.fileno(),), stdout=x_log, stderr=x_log) + servers.append(x) + wait_for(x, lambda: bool(display_file.read_text().strip())) + env["DISPLAY"] = ":" + display_file.read_text().strip() + + def run(mode, request=None, expected=None, **changes): + case_env = env.copy() + for key, value in changes.items(): + if value is None: + case_env.pop(key, None) + else: + case_env[key] = value + args = [test, mode] + if request is not None: + args.append(str(request)) + if expected is not None: + args.append(str(expected)) + print("Test:", mode, request, expected, changes, flush=True) + subprocess.run(args, env=case_env, check=True, timeout=8) + + run("open", 1, 1) + run("open", 2, 2) + run("open", 0, 1) + run("null", 0, 1) + run("open", 0, 2, WAYLAND_DISPLAY=None, XDG_SESSION_TYPE="x11") + run("open", 0, 2, WAYLAND_DISPLAY="missing-socket") + run("failure", 1, WAYLAND_DISPLAY="missing-socket") + run("failure", 2, DISPLAY=":59999") + run("failure", 0, WAYLAND_DISPLAY="missing-socket", DISPLAY=":59999") + run("mixed") + except BaseException: + for log in (wl_log, x_log): + log.flush() + log.seek(0) + print(log.read(), file=sys.stderr) + raise + finally: + for server in reversed(servers): + server.terminate() + try: + server.wait(timeout=5) + except subprocess.TimeoutExpired: + server.kill() + server.wait() + + +if __name__ == "__main__": + main() diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..a78ae65 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,44 @@ +test_exe = executable( + 'yasl-test', + 'test_lifecycle.c', + include_directories: inc, + link_with: yasl_lib.get_static_lib(), +) + +add_test_setup('asan') +add_test_setup('tsan') +add_test_setup('msan') + +test('basic', test_exe) + +selection_test = executable( + 'yasl-selection-test', + 'common/test_selection.c', + dependencies: yasl_dep, +) +test('selection', selection_test) + +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) + else + if not has_wayland or not has_x11 + error('Linux integration tests require Wayland and X11.') + endif + backend_test = executable( + 'yasl-backend-test', + 'common/test_backends.c', + dependencies: yasl_dep, + ) + python = find_program('python3') + weston = find_program('weston') + xwayland = find_program('Xwayland') + test('backends', python, + args: [files('helpers/run_backends.py'), weston, xwayland, backend_test], + timeout: 60, + ) + endif +endif -- cgit v1.2.3