From 648790f43a6ac92edbc77761d071d4e5df66b6d6 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 17:12:41 +0100 Subject: [PATCH] tests: Initial test and benchmark setup Add a unit test for wlr_box and benchmark for wlr_scene_node_at as our first test examples, lowering the barrier for adding more tests as suitable. --- meson.build | 4 ++ meson.options | 1 + test/bench_scene.c | 154 +++++++++++++++++++++++++++++++++++++++++++++ test/meson.build | 10 +++ test/test_box.c | 149 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 test/bench_scene.c create mode 100644 test/meson.build create mode 100644 test/test_box.c diff --git a/meson.build b/meson.build index d9e215afd..27aae8e9f 100644 --- a/meson.build +++ b/meson.build @@ -178,6 +178,10 @@ if get_option('examples') subdir('tinywl') endif +if get_option('tests') + subdir('test') +endif + pkgconfig = import('pkgconfig') pkgconfig.generate( lib_wlr, diff --git a/meson.options b/meson.options index 5863764aa..d8a8ca940 100644 --- a/meson.options +++ b/meson.options @@ -7,5 +7,6 @@ option('backends', type: 'array', choices: ['auto', 'drm', 'libinput', 'x11'], v option('allocators', type: 'array', choices: ['auto', 'gbm', 'udmabuf'], value: ['auto'], description: 'Select built-in allocators') option('session', type: 'feature', value: 'auto', description: 'Enable session support') +option('tests', type: 'boolean', value: true, description: 'Build tests and benchmarks') option('color-management', type: 'feature', value: 'auto', description: 'Enable support for color management') option('libliftoff', type: 'feature', value: 'auto', description: 'Enable support for libliftoff') diff --git a/test/bench_scene.c b/test/bench_scene.c new file mode 100644 index 000000000..6d6b270f0 --- /dev/null +++ b/test/bench_scene.c @@ -0,0 +1,154 @@ +#include +#include +#include + +struct tree_spec { + // Parameters for the tree we'll construct + int depth; + int branching; + int rect_size; + int spread; + + // Stats around the tree we built + int tree_count; + int rect_count; + int max_x; + int max_y; +}; + +static int max(int a, int b) { + return a > b ? a : b; +} + +static double timespec_diff_msec(struct timespec *start, struct timespec *end) { + return (double)(end->tv_sec - start->tv_sec) * 1e3 + + (double)(end->tv_nsec - start->tv_nsec) / 1e6; +} + +static bool build_tree(struct wlr_scene_tree *parent, struct tree_spec *spec, + int depth, int x, int y) { + + if (depth == spec->depth) { + float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + struct wlr_scene_rect *rect = + wlr_scene_rect_create(parent, spec->rect_size, spec->rect_size, color); + if (rect == NULL) { + fprintf(stderr, "wlr_scene_rect_create failed\n"); + return false; + } + wlr_scene_node_set_position(&rect->node, x, y); + spec->max_x = max(spec->max_x, x + spec->rect_size); + spec->max_y = max(spec->max_y, y + spec->rect_size); + spec->rect_count++; + return true; + } + + for (int i = 0; i < spec->branching; i++) { + struct wlr_scene_tree *child = wlr_scene_tree_create(parent); + if (child == NULL) { + fprintf(stderr, "wlr_scene_tree_create failed\n"); + return false; + } + spec->tree_count++; + int offset = i * spec->spread; + wlr_scene_node_set_position(&child->node, offset, offset); + if (!build_tree(child, spec, depth + 1, x + offset, y + offset)) { + return false; + } + } + return true; +} + +static bool bench_create_tree(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + if (!build_tree(&scene->tree, spec, 0, 0, 0)) { + fprintf(stderr, "build_tree failed\n"); + return false; + } + clock_gettime(CLOCK_MONOTONIC, &end); + + printf("Built tree with %d tree nodes, %d rect nodes\n\n", + spec->tree_count, spec->rect_count); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = spec->tree_count + spec->rect_count; + printf("create test tree: %d nodes, %.3f ms, %.0f nodes/ms\n", + nodes, elapsed, nodes / elapsed); + return true; +} + +static void bench_scene_node_at(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + int iters = 10000; + int hits = 0; + + clock_gettime(CLOCK_MONOTONIC, &start); + for (int i = 0; i < iters; i++) { + // Spread lookups across the tree extent + double lx = (double)(i * 97 % spec->max_x); + double ly = (double)(i * 53 % spec->max_y); + double nx, ny; + struct wlr_scene_node *node = + wlr_scene_node_at(&scene->tree.node, lx, ly, &nx, &ny); + if (node != NULL) { + hits++; + } + } + clock_gettime(CLOCK_MONOTONIC, &end); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = (spec->tree_count + spec->rect_count) * iters; + printf("wlr_scene_node_at: %d iters, %.3f ms, %.0f nodes/ms (hits: %d/%d)\n", + iters, elapsed, nodes / elapsed, hits, iters); +} + +static void noop_iterator(struct wlr_scene_buffer *buffer, + int sx, int sy, void *user_data) { + (void)buffer; + (void)sx; + (void)sy; + int *cnt = user_data; + (*cnt)++; +} + +static void bench_scene_node_for_each_buffer(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + int iters = 10000; + int hits = 0; + + clock_gettime(CLOCK_MONOTONIC, &start); + for (int i = 0; i < iters; i++) { + wlr_scene_node_for_each_buffer(&scene->tree.node, + noop_iterator, &hits); + } + clock_gettime(CLOCK_MONOTONIC, &end); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = (spec->tree_count + spec->rect_count) * iters; + printf("wlr_scene_node_for_each_buffer: %d iters, %.3f ms, %.0f nodes/ms (hits: %d/%d)\n", + iters, elapsed, nodes / elapsed, hits, iters); +} + +int main(void) { + struct wlr_scene *scene = wlr_scene_create(); + if (scene == NULL) { + fprintf(stderr, "wlr_scene_create failed\n"); + return 99; + } + + struct tree_spec spec = { + .depth = 5, + .branching = 5, + .rect_size = 10, + .spread = 100, + }; + if (!bench_create_tree(scene, &spec)) { + return 99; + } + bench_scene_node_at(scene, &spec); + bench_scene_node_for_each_buffer(scene, &spec); + + wlr_scene_node_destroy(&scene->tree.node); + return 0; +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 000000000..f51b2c02c --- /dev/null +++ b/test/meson.build @@ -0,0 +1,10 @@ +test( + 'box', + executable('test-box', 'test_box.c', dependencies: wlroots), +) + +benchmark( + 'scene', + executable('bench-scene', 'bench_scene.c', dependencies: wlroots), + timeout: 30, +) diff --git a/test/test_box.c b/test/test_box.c new file mode 100644 index 000000000..fcd95a8e2 --- /dev/null +++ b/test/test_box.c @@ -0,0 +1,149 @@ +#include +#include +#include + +static void test_box_empty(void) { + // NULL is empty + assert(wlr_box_empty(NULL)); + + // Zero width/height + struct wlr_box box = { .x = 0, .y = 0, .width = 0, .height = 10 }; + assert(wlr_box_empty(&box)); + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = 0 }; + assert(wlr_box_empty(&box)); + + // Negative width/height + box = (struct wlr_box){ .x = 0, .y = 0, .width = -1, .height = 10 }; + assert(wlr_box_empty(&box)); + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = -1 }; + assert(wlr_box_empty(&box)); + + // Valid box + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = 10 }; + assert(!wlr_box_empty(&box)); +} + +static void test_box_intersection(void) { + struct wlr_box dest; + + // Overlapping + struct wlr_box a = { .x = 0, .y = 0, .width = 100, .height = 100 }; + struct wlr_box b = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(wlr_box_intersection(&dest, &a, &b)); + assert(dest.x == 50 && dest.y == 50 && + dest.width == 50 && dest.height == 50); + + // Non-overlapping + b = (struct wlr_box){ .x = 200, .y = 200, .width = 50, .height = 50 }; + assert(!wlr_box_intersection(&dest, &a, &b)); + assert(dest.width == 0 && dest.height == 0); + + // Touching edges + b = (struct wlr_box){ .x = 100, .y = 0, .width = 50, .height = 50 }; + assert(!wlr_box_intersection(&dest, &a, &b)); + + // Self-intersection + assert(wlr_box_intersection(&dest, &a, &a)); + assert(dest.x == a.x && dest.y == a.y && + dest.width == a.width && dest.height == a.height); + + // Empty input + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_intersection(&dest, &a, &empty)); + + // NULL input + assert(!wlr_box_intersection(&dest, &a, NULL)); + assert(!wlr_box_intersection(&dest, NULL, &a)); +} + +static void test_box_intersects_box(void) { + // Overlapping + struct wlr_box a = { .x = 0, .y = 0, .width = 100, .height = 100 }; + struct wlr_box b = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(wlr_box_intersects(&a, &b)); + + // Non-overlapping + b = (struct wlr_box){ .x = 200, .y = 200, .width = 50, .height = 50 }; + assert(!wlr_box_intersects(&a, &b)); + + // Touching edges + b = (struct wlr_box){ .x = 100, .y = 0, .width = 50, .height = 50 }; + assert(!wlr_box_intersects(&a, &b)); + + // Self-intersection + assert(wlr_box_intersects(&a, &a)); + + // Empty input + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_intersects(&a, &empty)); + + // NULL input + assert(!wlr_box_intersects(&a, NULL)); + assert(!wlr_box_intersects(NULL, &a)); +} + +static void test_box_contains_point(void) { + struct wlr_box box = { .x = 10, .y = 20, .width = 100, .height = 50 }; + + // Interior point + assert(wlr_box_contains_point(&box, 50, 40)); + + // Inclusive lower bound + assert(wlr_box_contains_point(&box, 10, 20)); + + // Exclusive upper bound + assert(!wlr_box_contains_point(&box, 110, 70)); + assert(!wlr_box_contains_point(&box, 110, 40)); + assert(!wlr_box_contains_point(&box, 50, 70)); + + // Outside + assert(!wlr_box_contains_point(&box, 5, 40)); + assert(!wlr_box_contains_point(&box, 50, 15)); + + // Empty box + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_contains_point(&empty, 0, 0)); + + // NULL + assert(!wlr_box_contains_point(NULL, 0, 0)); +} + +static void test_box_contains_box(void) { + struct wlr_box outer = { .x = 0, .y = 0, .width = 100, .height = 100 }; + + // Fully contained + struct wlr_box inner = { .x = 10, .y = 10, .width = 50, .height = 50 }; + assert(wlr_box_contains_box(&outer, &inner)); + + // Self-containment + assert(wlr_box_contains_box(&outer, &outer)); + + // Partial overlap — not contained + struct wlr_box partial = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(!wlr_box_contains_box(&outer, &partial)); + + // Empty inner + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_contains_box(&outer, &empty)); + + // Empty outer + assert(!wlr_box_contains_box(&empty, &inner)); + + // NULL + assert(!wlr_box_contains_box(&outer, NULL)); + assert(!wlr_box_contains_box(NULL, &outer)); +} + +int main(void) { +#ifdef NDEBUG + fprintf(stderr, "NDEBUG must be disabled for tests\n"); + return 1; +#endif + + test_box_empty(); + test_box_intersection(); + test_box_intersects_box(); + test_box_contains_point(); + test_box_contains_box(); + return 0; +}