test/unit.c 35.5 KiB raw
1
#include <assert.h>
2
#include <fcntl.h>
3
#include <limits.h>
4
#include <stdlib.h>
5
#include <string.h>
6
#include <sys/wait.h>
7
8
#define main swm_program_main
9
#include "swm.c"
10
#undef main
11
12
/* Verify that releasing a pool item clears its storage. */
13
static void test_pool(pool_t *pool) {
14
    unsigned char *item = pool_take(pool);
15
    size_t         i;
16
17
    assert(item);
18
    memset(item, 0xa5, pool->item_size);
19
    pool_release(pool, item);
20
21
    for (i = 0; i < pool->item_size; i++)
22
        assert(item[i] == 0);
23
}
24
25
/* Verify pool allocation limits and clearing for every compositor object type. */
26
static void test_pools(void) {
27
    client_t *allocated[MAX_CLIENTS];
28
    int       saved, nullfd;
29
    size_t    i;
30
31
    for (i = 0; i < MAX_CLIENTS; i++) {
32
        allocated[i] = pool_take(&client_pool);
33
        assert(allocated[i] == &client_items[i]);
34
    }
35
    saved  = dup(STDERR_FILENO);
36
    nullfd = open("/dev/null", O_WRONLY);
37
    assert(saved >= 0 && nullfd >= 0);
38
    assert(dup2(nullfd, STDERR_FILENO) >= 0);
39
    assert(pool_take(&client_pool) == nullptr);
40
    assert(dup2(saved, STDERR_FILENO) >= 0);
41
    close(nullfd);
42
    close(saved);
43
44
    for (i = 0; i < MAX_CLIENTS; i++)
45
        pool_release(&client_pool, allocated[i]);
46
47
    test_pool(&monitor_pool);
48
    test_pool(&layer_surface_pool);
49
    test_pool(&keyboard_group_pool);
50
    test_pool(&pointer_constraint_pool);
51
    test_pool(&text_input_pool);
52
    test_pool(&input_popup_pool);
53
    test_pool(&session_lock_pool);
54
    test_pool(&pending_spawn_pool);
55
    test_pool(&window_state_pool);
56
    test_pool(&static_listener_pool);
57
    test_pool(&workspace_manager_pool);
58
    test_pool(&workspace_handle_pool);
59
    test_pool(&metadata_manager_pool);
60
}
61
62
/* Require a geometry helper to return the expected rectangle. */
63
static void assert_box(struct swm_box box, int x, int y, int width, int height) {
64
    assert(box.x == x);
65
    assert(box.y == y);
66
    assert(box.width == width);
67
    assert(box.height == height);
68
}
69
70
/* Verify pure geometry helpers. */
71
static void test_geometry(void) {
72
    struct swm_box bounds = { 10, 20, 100, 80 };
73
    struct swm_box box    = { 200, 200, 0, -2 };
74
    struct swm_box client = { 100, 100, 300, 200 };
75
    struct swm_box cursor = { 20, 30, 4, 10 };
76
    struct swm_box output = { 0, 0, 500, 400 };
77
78
    swm_box_apply_bounds(&box, &bounds, 2);
79
    assert_box(box, 105, 95, 5, 5);
80
    box = (struct swm_box){ 10, 20, 100, 80 };
81
    assert_box(swm_box_resize(&box, 20, 10, 10, 1), 10, 20, 120, 90);
82
    assert_box(swm_box_resize(&box, 500, 500, 5, 2), 105, 95, 5, 5);
83
    swm_box_reconcile_commit(&box, 90, 70, 2, 5);
84
    assert_box(box, 16, 26, 94, 74);
85
    assert_box(swm_popup_position(&client, 2, &cursor, 400, 300, &output), 100, 0, 400, 300);
86
}
87
88
/* Verify pure workspace and stack-state helpers. */
89
static void test_state(void) {
90
    bool                   visible[]  = { false, false, false, false, false };
91
    bool                   occupied[] = { false, false, true, false, true };
92
    struct swm_stack_state state      = { 16, 1, 1 };
93
    int                    side;
94
95
    assert(swm_workspace_state(false, false, false) == SWM_WORKSPACE_HIDDEN);
96
    assert(swm_workspace_state(true, true, true) == (SWM_WORKSPACE_ACTIVE | SWM_WORKSPACE_URGENT));
97
    assert(swm_border_width(3, false, false, false, false, false) == 3);
98
    assert(swm_border_width(3, true, false, false, false, false) == 0);
99
    assert(swm_workspace_next(0, 5, 1, false, visible, occupied) == 2);
100
    assert(swm_workspace_next(0, 5, -1, false, visible, occupied) == 4);
101
    visible[2] = true;
102
    assert(swm_workspace_next(0, 5, 1, false, visible, occupied) == 4);
103
    assert(swm_workspace_next(0, 5, 1, true, visible, occupied) == 1);
104
    assert(swm_stack_configure(&state, SWM_MASTER_SHRINK, SWM_MASTER_LEFT, &side));
105
    assert(state.msize == 15 && side == SWM_MASTER_LEFT);
106
    assert(tiled_resize_value(500, 1000, false) == 16);
107
    assert(tiled_resize_value(250, 1000, false) == 8);
108
    assert(tiled_resize_value(250, 1000, true) == 24);
109
    assert(tiled_resize_value(-100, 1000, false) == 1);
110
    assert(tiled_resize_value(1100, 1000, false) == 31);
111
    assert(tiled_resize_value(10, 16, false) == -1);
112
113
    for (int command = SWM_MASTER_GROW; command <= SWM_FLIP_LAYOUT; command++)
114
        assert(swm_stack_configure(&state, command, SWM_MASTER_LEFT, &side));
115
    assert(!swm_stack_configure(&state, 99, SWM_MASTER_LEFT, &side));
116
}
117
118
/* Verify pure rule and persistent-state parsing helpers. */
119
static void test_parsing(void) {
120
    char           small[5], appid[MAX_WINDOW_STATE_FIELD], title[MAX_WINDOW_STATE_FIELD];
121
    struct swm_box geometry;
122
123
    assert(swm_rule_matches("*", nullptr, "anything", "title"));
124
    assert(swm_rule_matches("term", "shell", "my-terminal", "a shell"));
125
    assert(!swm_rule_matches("browser", nullptr, "terminal", "browser"));
126
    swm_sanitize_field(small, sizeof(small), "a\tb\nc", nullptr);
127
    assert(!strcmp(small, "a b "));
128
    assert(swm_parse_window_state(
129
        "org.app\tWindow\t1\t-2\t300\t200\n", appid, sizeof(appid), title, sizeof(title), &geometry
130
    ));
131
    assert(!strcmp(appid, "org.app"));
132
    assert(!strcmp(title, "Window"));
133
    assert_box(geometry, 1, -2, 300, 200);
134
    assert(
135
        !swm_parse_window_state("broken", appid, sizeof(appid), title, sizeof(title), &geometry)
136
    );
137
}
138
139
/* Verify layout cycling and complete output-area partitioning. */
140
static void test_layouts(void) {
141
    bool                   cycle[] = { true, true, false, true };
142
    struct swm_box         area    = { -3, 7, 64, 61 };
143
    struct swm_box         boxes[64];
144
    struct swm_stack_state state;
145
    size_t                 count, produced;
146
    int                    rotate, flip, masters, stacks;
147
148
    assert(swm_next_layout(0, 4, cycle) == 1);
149
    assert(swm_next_layout(1, 4, cycle) == 3);
150
    assert(swm_next_layout(3, 4, cycle) == 0);
151
152
    for (rotate = 0; rotate <= 1; rotate++)
153
        for (flip = 0; flip <= 1; flip++)
154
            for (masters = 0; masters < 5; masters++)
155
                for (stacks = 1; stacks < 5; stacks++)
156
                    for (count = 1; count < 13; count++) {
157
                        int covered = 0;
158
159
                        state    = (struct swm_stack_state){ 13, masters, stacks };
160
                        produced = swm_stack_layout(&area, &state, rotate, flip, count, boxes);
161
                        assert(produced == count);
162
163
                        for (size_t i = 0; i < count; i++)
164
                            covered += boxes[i].width * boxes[i].height;
165
                        assert(covered == area.width * area.height);
166
                    }
167
}
168
169
/* Verify environment expansion and output bounds. */
170
static void test_utilities(void) {
171
    char output[64];
172
173
    assert(setenv("SWM_TEST_ENV", "hello world", 1) == 0);
174
    assert(env_expand(output, sizeof(output), "$SWM_TEST_ENV/tail") == 17);
175
    assert(!strcmp(output, "hello world/tail"));
176
    assert(env_expand(output, 4, "$SWM_TEST_ENV") == 0);
177
    assert(env_expand(output, 0, "value") == 0);
178
    assert(env_expand(output, sizeof(output), "$1") == 3);
179
    assert(!strcmp(output, "$1"));
180
}
181
182
/* Verify free-workspace selection and published workspace states. */
183
static void test_workspace_queries(void) {
184
    client_t    *client;
185
    workspace_t *workspace;
186
    int          i;
187
188
    for (i = 0; i < WSCOUNT; i++) {
189
        workspaces[i].mon = (monitor_t *)1;
190
        wl_list_init(&workspaces[i].handles);
191
    }
192
    workspaces[4].mon = nullptr;
193
    assert(free_workspace() == &workspaces[4]);
194
    workspaces[4].mon = (monitor_t *)1;
195
    assert(free_workspace() == nullptr);
196
197
    wl_list_init(&clients);
198
    workspace      = &workspaces[2];
199
    workspace->mon = nullptr;
200
    assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN);
201
    client = pool_take(&client_pool);
202
    assert(client);
203
    client->ws        = workspace;
204
    client->is_urgent = 1;
205
    wl_list_insert(&clients, &client->link);
206
    assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_URGENT);
207
    workspace->mon = (monitor_t *)1;
208
    assert(
209
        workspace_state(workspace) ==
210
        (EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE | EXT_WORKSPACE_HANDLE_V1_STATE_URGENT)
211
    );
212
    wl_list_remove(&client->link);
213
    pool_release(&client_pool, client);
214
}
215
216
/* Verify state-file paths and client border-width policy. */
217
static void test_paths_and_border_policy(void) {
218
    const char                 *directory = getenv("SWM_TEST_DIR");
219
    client_t                    client    = {};
220
    struct wlr_scene_tree       scene     = {};
221
    struct wlr_xwayland_surface xsurface  = { .override_redirect = true };
222
    char                        path[PATH_MAX], expected[PATH_MAX];
223
224
    assert(directory);
225
    assert(snprintf(path, sizeof(path), "%s/state", directory) < (int)sizeof(path));
226
    assert(setenv("XDG_STATE_HOME", path, 1) == 0);
227
    assert(snprintf(expected, sizeof(expected), "%s/swm/windows", path) < (int)sizeof(expected));
228
    assert(!strcmp(window_state_path(), expected));
229
    unsetenv("XDG_STATE_HOME");
230
    assert(snprintf(path, sizeof(path), "%s/home", directory) < (int)sizeof(path));
231
    assert(setenv("HOME", path, 1) == 0);
232
    assert(
233
        snprintf(expected, sizeof(expected), "%s/.local/state/swm/windows", path) <
234
        (int)sizeof(expected)
235
    );
236
    assert(!strcmp(window_state_path(), expected));
237
    client_set_border_color(&client, bordercolor);
238
    client.type             = X11;
239
    client.surface.xwayland = &xsurface;
240
    client.scene            = &scene;
241
    client_set_border_color(&client, bordercolor);
242
    client    = (client_t){};
243
    client.bw = 9;
244
    assert(client_border_width(&client) == borderwidth);
245
    client.is_fullscreen = 1;
246
    assert(client_border_width(&client) == 0);
247
}
248
249
/* Initialize a minimal XDG client object for internal tests. */
250
static void init_xdg_client(
251
    client_t                *client,
252
    struct wlr_xdg_surface  *surface,
253
    struct wlr_xdg_toplevel *toplevel,
254
    const char              *appid,
255
    const char              *title
256
) {
257
    memset(client, 0, sizeof(*client));
258
    memset(surface, 0, sizeof(*surface));
259
    memset(toplevel, 0, sizeof(*toplevel));
260
    client->type        = XDG_SHELL;
261
    client->surface.xdg = surface;
262
    surface->toplevel   = toplevel;
263
    toplevel->base      = surface;
264
    toplevel->app_id    = (char *)appid;
265
    toplevel->title     = (char *)title;
266
}
267
268
/* Verify matching rules update client state and workspace assignment. */
269
static void test_rule_application(void) {
270
    client_t                client;
271
    struct wlr_xdg_surface  surface;
272
    struct wlr_xdg_toplevel toplevel;
273
274
    init_xdg_client(&client, &surface, &toplevel, "org.telegram.desktop", "Telegram");
275
    wl_list_init(&mons);
276
    selmon = nullptr;
277
    apply_rules(&client, nullptr);
278
    assert(client.is_floating == 1);
279
    assert(client.is_borderless == 1);
280
    assert(client.mon == nullptr);
281
}
282
283
/* Verify focus queries across mapped, hidden, and unmanaged clients. */
284
static void test_focus_queries(void) {
285
    client_t                first, second, hidden;
286
    struct wlr_xdg_surface  surfaces[3];
287
    struct wlr_xdg_toplevel toplevels[3];
288
    monitor_t               monitor = {};
289
    workspace_t             shown = {}, other = {};
290
291
    shown.mon  = &monitor;
292
    monitor.ws = &shown;
293
    other.mon  = nullptr;
294
    init_xdg_client(&first, &surfaces[0], &toplevels[0], "first", "First");
295
    init_xdg_client(&second, &surfaces[1], &toplevels[1], "second", "Second");
296
    init_xdg_client(&hidden, &surfaces[2], &toplevels[2], "hidden", "Hidden");
297
    first.ws = second.ws = &shown;
298
    hidden.ws            = &other;
299
    first.mon = second.mon = &monitor;
300
301
    wl_list_init(&clients);
302
    wl_list_init(&fstack);
303
    wl_list_insert(clients.prev, &first.link);
304
    wl_list_insert(clients.prev, &second.link);
305
    wl_list_insert(clients.prev, &hidden.link);
306
    wl_list_insert(&fstack, &hidden.flink);
307
    wl_list_insert(&fstack, &first.flink);
308
    wl_list_insert(&fstack, &second.flink);
309
    assert(focus_top(&monitor) == &second);
310
    assert(focus_top(nullptr) == nullptr);
311
    assert(focus_close(&second) == &first);
312
    assert(focus_close(nullptr) == nullptr);
313
    assert(focus_close(&hidden) == nullptr);
314
    assert(client_main(nullptr) == nullptr);
315
    assert(!clients_related(nullptr, &first));
316
    assert(clients_related(&first, &first));
317
}
318
319
/* Release every saved window-state entry. */
320
static void clear_window_states(void) {
321
    window_state_t *state, *tmp;
322
323
    wl_list_for_each_safe(state, tmp, &window_states, link) {
324
        wl_list_remove(&state->link);
325
        pool_release(&window_state_pool, state);
326
    }
327
}
328
329
/* Verify saving, loading, finding, and forgetting persistent window state. */
330
static void test_window_state_storage(void) {
331
    char                    statehome[PATH_MAX];
332
    char                    filepath[MAX_STATE_PATH];
333
    client_t                client;
334
    struct wlr_xdg_surface  surface;
335
    struct wlr_xdg_toplevel toplevel;
336
    window_state_t         *state;
337
    FILE                   *file;
338
339
    assert(
340
        snprintf(statehome, sizeof(statehome), "%s/window-state-XXXXXX", getenv("SWM_TEST_DIR")) <
341
        (int)sizeof(statehome)
342
    );
343
    assert(mkdtemp(statehome));
344
    setenv("XDG_STATE_HOME", statehome, 1);
345
    wl_list_init(&window_states);
346
    init_xdg_client(&client, &surface, &toplevel, "org.test\tapp", "Window\nTitle");
347
    client.persist_float = client.is_floating = 1;
348
    client.geom                               = (struct wlr_box){ 11, -12, 640, 480 };
349
    remember_client(&client);
350
    state = find_window_state(&client);
351
    assert(state);
352
    assert(!strcmp(state->appid, "org.test app"));
353
    assert(!strcmp(state->title, "Window Title"));
354
    assert(state->geom.x == 11 && state->geom.height == 480);
355
356
    snprintf(filepath, sizeof(filepath), "%s/swm/windows", statehome);
357
    file = fopen(filepath, "r");
358
    assert(file);
359
    assert(fclose(file) == 0);
360
    clear_window_states();
361
    load_window_states();
362
    assert(find_window_state(&client));
363
    forget_client(&client);
364
    assert(find_window_state(&client) == nullptr);
365
366
    file = fopen(filepath, "w");
367
    assert(file);
368
    assert(fputs("bad line\nlegacy\t1\t2\t3\t4\n", file) >= 0);
369
    assert(fclose(file) == 0);
370
    load_window_states();
371
    assert(!wl_list_empty(&window_states));
372
    clear_window_states();
373
    unlink(filepath);
374
    snprintf(filepath, sizeof(filepath), "%s/swm", statehome);
375
    rmdir(filepath);
376
    rmdir(statehome);
377
}
378
379
/* Verify SIGCHLD handling releases tracked child processes. */
380
static void test_child_reaping(void) {
381
    pending_spawn_t *spawned;
382
    struct timespec  delay = { 0, 1000000 };
383
    pid_t            pid;
384
    int              i;
385
386
    wl_list_init(&pending_spawns);
387
    pid = fork();
388
    assert(pid >= 0);
389
390
    if (pid == 0)
391
        _exit(0);
392
    spawned = pool_take(&pending_spawn_pool);
393
    assert(spawned);
394
    spawned->pid = pid;
395
    wl_list_insert(&pending_spawns, &spawned->link);
396
    child_pid = pid;
397
398
    for (i = 0; i < 100 && !wl_list_empty(&pending_spawns); i++) {
399
        handle_signal(SIGCHLD, nullptr);
400
        nanosleep(&delay, nullptr);
401
    }
402
    assert(wl_list_empty(&pending_spawns));
403
    assert(child_pid == -1);
404
    assert(handle_signal(0, nullptr) == 0);
405
}
406
407
/* Verify spawned commands are tracked and reaped. */
408
static void test_spawn_tracking(void) {
409
    const char     *command[] = { "/bin/true", nullptr };
410
    monitor_t       monitor   = {};
411
    workspace_t     workspace = {};
412
    struct timespec delay     = { 0, 1000000 };
413
    int             i;
414
415
    assert(sigprocmask(SIG_BLOCK, nullptr, &original_signal_mask) == 0);
416
    wl_list_init(&pending_spawns);
417
    monitor.ws = &workspace;
418
    selmon     = &monitor;
419
    spawn(&(arg_t){ .v = command });
420
    assert(!wl_list_empty(&pending_spawns));
421
422
    for (i = 0; i < 100 && !wl_list_empty(&pending_spawns); i++) {
423
        handle_signal(SIGCHLD, nullptr);
424
        nanosleep(&delay, nullptr);
425
    }
426
    assert(wl_list_empty(&pending_spawns));
427
    selmon = nullptr;
428
}
429
430
/* Verify a failed keybinding command produces a desktop notification. */
431
static void test_command_failure_notification(void) {
432
    const char *stub_path = getenv("SWM_TEST_STUB_PATH");
433
    const char *test_dir  = getenv("SWM_TEST_DIR");
434
    const char *old_path  = getenv("PATH");
435
    char       *saved_path;
436
    char        notification[PATH_MAX];
437
    char        contents[1024];
438
    char       *command[] = { "swm-command-that-does-not-exist", nullptr };
439
    FILE       *file;
440
    pid_t       pid;
441
    size_t      length;
442
    int         status;
443
444
    assert(stub_path && test_dir && old_path);
445
    saved_path = strdup(old_path);
446
    assert(saved_path);
447
    assert(
448
        snprintf(notification, sizeof(notification), "%s/notification", test_dir) <
449
        (int)sizeof(notification)
450
    );
451
    unlink(notification);
452
    assert(setenv("PATH", stub_path, 1) == 0);
453
    pid = fork();
454
    assert(pid >= 0);
455
456
    if (pid == 0)
457
        execute_command(command, "key binding");
458
    assert(waitpid(pid, &status, 0) == pid);
459
    assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
460
    file = fopen(notification, "r");
461
    assert(file);
462
    length = fread(contents, 1, sizeof(contents) - 1, file);
463
    assert(length > 0);
464
    contents[length] = '\0';
465
    assert(fclose(file) == 0);
466
    assert(strstr(contents, "--app-name=swm"));
467
    assert(strstr(contents, "--urgency=critical"));
468
    assert(strstr(contents, "Command failed"));
469
    assert(strstr(contents, "swm-command-that-does-not-exist"));
470
    assert(setenv("PATH", saved_path, 1) == 0);
471
    free(saved_path);
472
    unlink(notification);
473
}
474
475
/* Verify client commands remain safe when no client can receive them. */
476
static void test_commands_without_clients(void) {
477
    struct wlr_output output  = {};
478
    monitor_t         monitor = {};
479
    arg_t             argument;
480
    int               i;
481
482
    monitor.wlr_output   = &output;
483
    monitor.ws           = &workspaces[0];
484
    workspaces[0].mon    = &monitor;
485
    workspaces[0].lt     = &layouts[0];
486
    workspaces[0].prevlt = nullptr;
487
    workspaces[0].v = workspaces[0].h = (stack_state_t){ 16, 1, 1 };
488
489
    for (i = 1; i < WSCOUNT; i++)
490
        workspaces[i].mon = (monitor_t *)1;
491
    wl_list_init(&clients);
492
    wl_list_init(&fstack);
493
    wl_list_init(&mons);
494
    wl_list_init(&ws_managers);
495
    wl_list_init(&metadata_managers);
496
    selmon = &monitor;
497
    unsetenv("XDG_RUNTIME_DIR");
498
499
    cycle_layout(nullptr);
500
    assert(workspaces[0].lt == &layouts[1]);
501
    assert(workspaces[0].prevlt == &layouts[0]);
502
    toggle_max_stack(nullptr);
503
    assert(workspaces[0].lt == &layouts[4]);
504
    toggle_max_stack(nullptr);
505
    assert(workspaces[0].lt == &layouts[1]);
506
    argument.i = MASTER_GROW;
507
    stack_config(&argument);
508
    assert(workspaces[0].h.msize == 17);
509
    argument.i = LAYOUT_FLIP;
510
    stack_config(&argument);
511
    assert(workspaces[0].lt == &layouts[3]);
512
513
    assert(focus_top(&monitor) == nullptr);
514
    focus_stack(&(arg_t){ .i = 1 });
515
    focus_main(nullptr);
516
    focus_urgent(nullptr);
517
    tag(&(arg_t){ .i = 0 });
518
    tag(&(arg_t){ .i = WSCOUNT });
519
    tag_monitor(&(arg_t){ .i = WLR_DIRECTION_LEFT });
520
    toggle_floating(nullptr);
521
    toggle_fullscreen(nullptr);
522
    kill_client(nullptr);
523
    raise_client(nullptr);
524
    zoom(nullptr);
525
    argument.i = WORKSPACE_NEXT;
526
    cycle_workspace(&argument);
527
    assert(monitor.ws == &workspaces[0]);
528
    view(&(arg_t){ .i = WSCOUNT });
529
530
    selmon = nullptr;
531
    cycle_layout(nullptr);
532
    toggle_max_stack(nullptr);
533
    stack_config(&argument);
534
    cycle_workspace(&argument);
535
    view(&(arg_t){ .i = 0 });
536
}
537
538
/* Verify small geometry, focus, inhibition, and listener helpers. */
539
static void test_small_helpers(void) {
540
    client_t           client = { .geom = { -20, 100, 2, 2 }, .bw = 2 };
541
    struct wlr_box     bounds = { 0, 0, 80, 60 };
542
    static_listener_t *slot;
543
    struct wlr_seat    fake_seat = {};
544
545
    apply_bounds(&client, &bounds);
546
    assert(client.geom.x == 0 && client.geom.y == 55);
547
    assert(client.geom.width == 5 && client.geom.height == 5);
548
    seat = &fake_seat;
549
    assert(shortcuts_inhibited() == 0);
550
    assert(focus_close(nullptr) == nullptr);
551
    slot = pool_take(&static_listener_pool);
552
    assert(slot);
553
    listener_release(&slot->listener);
554
}
555
556
/* Verify XDG and XWayland client accessors without a running compositor. */
557
static void test_client_accessors(void) {
558
    client_t                    client   = {};
559
    struct wlr_xdg_surface      xdg      = { .geometry = { 3, 4, 50, 60 } };
560
    struct wlr_xdg_toplevel     toplevel = {};
561
    struct wlr_xwayland_surface xwayland = {
562
        .class             = "x11-app",
563
        .title             = "X11 title",
564
        .x                 = 7,
565
        .y                 = 8,
566
        .width             = 90,
567
        .height            = 100,
568
        .modal             = true,
569
        .override_redirect = true,
570
        .fullscreen        = true,
571
    };
572
    xcb_size_hints_t size_hints = {
573
        .min_width  = 20,
574
        .min_height = 20,
575
        .max_width  = 20,
576
        .max_height = 40,
577
    };
578
    struct wlr_box    box;
579
    char              storage[MAX_COMMAND_SIZE], *expanded[MAX_COMMAND_ARGS];
580
    const char *const command[] = { "before-$SWM_TEST_EXPAND", "$SWM_TEST_MISSING", nullptr };
581
582
    init_xdg_client(&client, &xdg, &toplevel, "xdg-app", "XDG title");
583
    xdg.geometry         = (struct wlr_box){ 3, 4, 50, 60 };
584
    client.geom          = (struct wlr_box){ 1, 2, 70, 80 };
585
    client.bw            = 2;
586
    client.is_fullscreen = true;
587
    assert(!client_allows_move_resize(&client, CURSOR_MOVE));
588
    assert(client_allows_move_resize(&client, CURSOR_RESIZE));
589
    client.is_fullscreen = false;
590
    assert(client_allows_move_resize(&client, CURSOR_MOVE));
591
    assert(!strcmp(client_get_appid(&client), "xdg-app"));
592
    assert(!strcmp(client_get_title(&client), "XDG title"));
593
    client_get_clip(&client, &box);
594
    assert(box.x == 3 && box.y == 4 && box.width == 68 && box.height == 78);
595
    client_get_geometry(&client, &box);
596
    assert(box.x == 3 && box.y == 4 && box.width == 50 && box.height == 60);
597
    assert(client_get_parent(&client) == nullptr);
598
    assert(!client_is_float_type(&client));
599
    toplevel.current = (struct wlr_xdg_toplevel_state){
600
        .min_width  = 10,
601
        .min_height = 10,
602
        .max_width  = 10,
603
        .max_height = 20,
604
    };
605
    assert(client_is_float_type(&client));
606
    assert(!client_is_unmanaged(&client));
607
    toplevel.requested.fullscreen = true;
608
    assert(client_wants_fullscreen(&client));
609
610
    client.type             = X11;
611
    client.surface.xwayland = &xwayland;
612
    assert(client_set_bounds(&client, 10, 20) == 0);
613
    assert(!strcmp(client_get_appid(&client), "x11-app"));
614
    assert(!strcmp(client_get_title(&client), "X11 title"));
615
    client_get_clip(&client, &box);
616
    assert(box.x == 0 && box.y == 0);
617
    client_get_geometry(&client, &box);
618
    assert(box.x == 7 && box.y == 8 && box.width == 90 && box.height == 100);
619
    assert(client_get_parent(&client) == nullptr);
620
    assert(client_is_float_type(&client));
621
    assert(client_is_unmanaged(&client));
622
    assert(client_wants_fullscreen(&client));
623
    xwayland.modal      = false;
624
    xwayland.size_hints = &size_hints;
625
    assert(client_is_float_type(&client));
626
    xwayland.class = xwayland.title = nullptr;
627
    assert(!strcmp(client_get_appid(&client), "broken"));
628
    assert(!strcmp(client_get_title(&client), "broken"));
629
    client_set_resizing(&client, true);
630
    client_set_suspended(&client, true);
631
632
    setenv("SWM_TEST_EXPAND", "expanded value", 1);
633
    unsetenv("SWM_TEST_MISSING");
634
    expand_argv(command, expanded, storage);
635
    assert(!strcmp(expanded[0], "before-expanded value"));
636
    assert(!strcmp(expanded[1], ""));
637
    assert(expanded[2] == nullptr);
638
    publish_windows(nullptr);
639
}
640
641
/* Verify configured autostart commands and child tracking with harmless stubs. */
642
static void test_autostart_execution(void) {
643
    const char     *stub_path = getenv("SWM_TEST_STUB_PATH");
644
    const char     *old_path  = getenv("PATH");
645
    char           *saved_path;
646
    struct timespec delay = { 0, 1000000 };
647
    size_t          i;
648
    int             pending;
649
650
    assert(stub_path && old_path);
651
    saved_path = strdup(old_path);
652
    assert(saved_path);
653
    assert(setenv("PATH", stub_path, 1) == 0);
654
    assert(sigprocmask(SIG_BLOCK, nullptr, &original_signal_mask) == 0);
655
    wl_list_init(&pending_spawns);
656
    autostart_len = 0;
657
    autostart_exec();
658
    assert(autostart_len > 0);
659
660
    for (i = 0; i < 100; i++) {
661
        pending = 0;
662
        handle_signal(SIGCHLD, nullptr);
663
664
        for (size_t j = 0; j < autostart_len; j++)
665
            pending |= autostart_pids[j] > 0;
666
667
        if (!pending)
668
            break;
669
        nanosleep(&delay, nullptr);
670
    }
671
    assert(!pending);
672
    assert(setenv("PATH", saved_path, 1) == 0);
673
    free(saved_path);
674
}
675
676
/* Verify forwarding of every pointer-gesture event family. */
677
static void test_pointer_gestures(void) {
678
    struct wlr_pointer_swipe_begin_event  swipe_begin  = { .time_msec = 1, .fingers = 3 };
679
    struct wlr_pointer_swipe_update_event swipe_update = { .time_msec = 2, .dx = 1, .dy = -1 };
680
    struct wlr_pointer_swipe_end_event    swipe_end    = { .time_msec = 3, .cancelled = false };
681
    struct wlr_pointer_pinch_begin_event  pinch_begin  = { .time_msec = 4, .fingers = 2 };
682
    struct wlr_pointer_pinch_update_event pinch_update = {
683
        .time_msec = 5,
684
        .dx        = 1,
685
        .dy        = 2,
686
        .scale     = 1.1,
687
        .rotation  = 3,
688
    };
689
    struct wlr_pointer_pinch_end_event  pinch_end  = { .time_msec = 6, .cancelled = true };
690
    struct wlr_pointer_hold_begin_event hold_begin = { .time_msec = 7, .fingers = 2 };
691
    struct wlr_pointer_hold_end_event   hold_end   = { .time_msec = 8, .cancelled = false };
692
693
    dpy = wl_display_create();
694
    assert(dpy);
695
    seat             = wlr_seat_create(dpy, "test-seat");
696
    idle_notifier    = wlr_idle_notifier_v1_create(dpy);
697
    pointer_gestures = wlr_pointer_gestures_v1_create(dpy);
698
    assert(seat && idle_notifier && pointer_gestures);
699
    gesture_swipe_begin(nullptr, &swipe_begin);
700
    gesture_swipe_update(nullptr, &swipe_update);
701
    gesture_swipe_end(nullptr, &swipe_end);
702
    gesture_pinch_begin(nullptr, &pinch_begin);
703
    gesture_pinch_update(nullptr, &pinch_update);
704
    gesture_pinch_end(nullptr, &pinch_end);
705
    gesture_hold_begin(nullptr, &hold_begin);
706
    gesture_hold_end(nullptr, &hold_end);
707
    wl_display_destroy(dpy);
708
    dpy = nullptr;
709
}
710
711
/* Verify text-input and input-method wrappers are created and released. */
712
static void test_input_method_lifecycle(void) {
713
    struct wlr_seat                  fake_seat       = {};
714
    struct wlr_text_input_manager_v3 fake_ti_manager = {};
715
    struct wlr_text_input_v3         text_input      = {};
716
    struct wlr_input_method_v2       method          = {};
717
    text_input_t                    *wrapper         = nullptr;
718
    size_t                           i;
719
720
    seat   = &fake_seat;
721
    ti_mgr = &fake_ti_manager;
722
    wl_list_init(&fake_ti_manager.text_inputs);
723
    assert(input_method_focused_text_input() == nullptr);
724
    input_method = nullptr;
725
    input_method_send_state(&text_input);
726
    input_method_commit_notify(nullptr, &method);
727
728
    wl_signal_init(&text_input.events.enable);
729
    wl_signal_init(&text_input.events.commit);
730
    wl_signal_init(&text_input.events.disable);
731
    wl_signal_init(&text_input.events.destroy);
732
    text_input_create_notify(nullptr, &text_input);
733
734
    for (i = 0; i < LENGTH(text_input_used); i++)
735
736
        if (text_input_used[i]) {
737
            wrapper = &text_input_items[i];
738
            break;
739
        }
740
    assert(wrapper && wrapper->ti == &text_input);
741
    text_input_enable_notify(&wrapper->enable, nullptr);
742
    text_input_commit_notify(&wrapper->commit, nullptr);
743
    text_input_disable_notify(&wrapper->disable, nullptr);
744
    text_input_destroy_notify(&wrapper->destroy, nullptr);
745
    assert(!text_input_used[wrapper - text_input_items]);
746
747
    wl_signal_init(&method.events.commit);
748
    wl_signal_init(&method.events.destroy);
749
    wl_signal_init(&method.events.grab_keyboard);
750
    wl_signal_init(&method.events.new_popup_surface);
751
    input_method_create_notify(nullptr, &method);
752
    assert(input_method == &method);
753
    input_method_destroy_notify(nullptr, nullptr);
754
    assert(input_method == nullptr);
755
}
756
757
/* Verify input handlers safely reject incomplete or inapplicable state. */
758
static void test_input_early_paths(void) {
759
    struct wlr_seat                                            fake_seat    = {};
760
    struct wlr_seat_pointer_request_set_cursor_event           cursor_event = {};
761
    struct wlr_cursor_shape_manager_v1_request_set_shape_event shape_event  = {};
762
    struct wlr_drag                                            drag         = {};
763
    keyboard_group_t                                           group        = {};
764
    monitor_t                                                  monitor      = {};
765
    workspace_t                                                workspace    = {};
766
    struct wlr_output                                          output       = {};
767
768
    seat        = &fake_seat;
769
    cursor_mode = CURSOR_MOVE;
770
    set_cursor(nullptr, &cursor_event);
771
    set_cursor_shape(nullptr, &shape_event);
772
    start_drag(nullptr, &drag);
773
    assert(key_repeat(&group) == 0);
774
    assert(key_binding(0, XKB_KEY_NoSymbol, 0) == 0);
775
776
    wl_list_init(&clients);
777
    wl_list_init(&fstack);
778
    monitor.wlr_output = &output;
779
    monitor.ws         = &workspace;
780
    workspace.mon      = &monitor;
781
    workspace.lt       = &layouts[0];
782
    workspace.v = workspace.h = (stack_state_t){ 16, 1, 1 };
783
    master_left(&monitor);
784
    master_right(&monitor);
785
    master_top(&monitor);
786
    master_bottom(&monitor);
787
    max_stack(&monitor);
788
    move_resize(&(arg_t){ .u = CURSOR_MOVE });
789
}
790
791
/* Assert that the program exits with the requested status. */
792
static void assert_program_exits(int argc, char **argv, int clear_runtime, int code) {
793
    int   status;
794
    pid_t pid = fork();
795
796
    assert(pid >= 0);
797
798
    if (pid == 0) {
799
        close(STDERR_FILENO);
800
801
        if (clear_runtime)
802
            unsetenv("XDG_RUNTIME_DIR");
803
        optind = 1;
804
        _exit(swm_program_main(argc, argv));
805
    }
806
    assert(waitpid(pid, &status, 0) == pid);
807
    assert(WIFEXITED(status) && WEXITSTATUS(status) == code);
808
}
809
810
/* Assert that the program exits with failure status. */
811
static void assert_program_fails(int argc, char **argv, int clear_runtime) {
812
    assert_program_exits(argc, argv, clear_runtime, 1);
813
}
814
815
/* Verify version output and invalid command-line invocations. */
816
static void test_command_line_errors(void) {
817
    char *version[]         = { "swm", "-v", nullptr };
818
    char *help[]            = { "swm", "-h", nullptr };
819
    char *extra[]           = { "swm", "extra", nullptr };
820
    char *missing_runtime[] = { "swm", nullptr };
821
822
    assert_program_exits(2, version, 0, 0); /* -v prints the version and succeeds. */
823
    assert_program_fails(2, help, 0);
824
    assert_program_fails(2, extra, 0);
825
    assert_program_fails(1, missing_runtime, 1);
826
}
827
828
/* Verify pointer-constraint and decoration lifecycle handlers. */
829
static void test_pointer_and_decoration_lifecycle(void) {
830
    struct wlr_pointer_constraint_v1      constraint = {};
831
    struct wlr_surface                    surface    = {};
832
    pointer_constraint_t                 *wrapper    = nullptr;
833
    struct wlr_xdg_surface                xdg        = {};
834
    struct wlr_xdg_toplevel               toplevel   = {};
835
    struct wlr_xdg_toplevel_decoration_v1 decoration = {};
836
    client_t                              client     = {};
837
    struct wlr_seat                       fake_seat  = {};
838
    size_t                                i;
839
840
    seat               = &fake_seat;
841
    constraint.surface = &surface;
842
    wl_signal_init(&constraint.events.destroy);
843
    create_pointer_constraint(nullptr, &constraint);
844
845
    for (i = 0; i < LENGTH(pointer_constraint_used); i++)
846
847
        if (pointer_constraint_used[i]) {
848
            wrapper = &pointer_constraint_items[i];
849
            break;
850
        }
851
    assert(wrapper && wrapper->constraint == &constraint);
852
    destroy_pointer_constraint(&wrapper->destroy, nullptr);
853
    assert(!pointer_constraint_used[wrapper - pointer_constraint_items]);
854
    cursor_constrain(nullptr);
855
    cursor_constrain(nullptr);
856
857
    client.type               = XDG_SHELL;
858
    client.surface.xdg        = &xdg;
859
    client.decoration         = &decoration;
860
    xdg.toplevel              = &toplevel;
861
    toplevel.base             = &xdg;
862
    decoration.toplevel       = &toplevel;
863
    decoration.requested_mode = WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
864
    request_decoration_mode(&client.set_decoration_mode, nullptr);
865
    assert(client.bw == 0);
866
    maximize_notify(&client.maximize, nullptr);
867
    create_decoration(nullptr, &decoration);
868
    urgent(nullptr, &(struct wlr_xdg_activation_v1_request_activate_event){});
869
}
870
871
/* Verify modifier handling, repeatable bindings, and key-repeat dispatch. */
872
static void test_keybindings_and_repeat(void) {
873
    struct wlr_seat           fake_seat      = {};
874
    struct wlr_keyboard_group keyboard_group = {};
875
    keyboard_group_t          group          = {};
876
    struct wl_event_loop     *loop;
877
    xkb_keysym_t              symbol = XKB_KEY_j;
878
879
    seat   = &fake_seat;
880
    selmon = nullptr;
881
    wl_list_init(&fstack);
882
    assert(key_binding(MOD, XKB_KEY_1, false) == 1);
883
    assert(key_binding(MOD | WLR_MODIFIER_CAPS, XKB_KEY_1, false) == 1);
884
    assert(key_binding(MOD, XKB_KEY_1, true) == 0);
885
    assert(key_binding(MOD, XKB_KEY_j, true) == 2);
886
    assert(key_binding(0, XKB_KEY_NoSymbol, 0) == 0);
887
888
    loop = wl_event_loop_create();
889
    assert(loop);
890
    group.wlr_group                          = &keyboard_group;
891
    group.nsyms                              = 1;
892
    group.keysyms                            = &symbol;
893
    group.mods                               = MOD;
894
    keyboard_group.keyboard.repeat_info.rate = 25;
895
    group.key_repeat_source                  = wl_event_loop_add_timer(loop, key_repeat, &group);
896
    assert(group.key_repeat_source);
897
    assert(key_repeat(&group) == 0);
898
    wl_event_source_remove(group.key_repeat_source);
899
    wl_event_loop_destroy(loop);
900
}
901
902
/* Name one compositor-internal unit test callable by the Python runner. */
903
typedef struct {
904
    const char *name;
905
    void (*run)(void);
906
} internal_test_t;
907
908
/* Run the requested compositor-internal unit test. */
909
int main(int argc, char **argv) {
910
    static const internal_test_t tests[] = {
911
        { "geometry", test_geometry },
912
        { "state", test_state },
913
        { "parsing", test_parsing },
914
        { "layouts", test_layouts },
915
        { "utilities", test_utilities },
916
        { "pools", test_pools },
917
        { "workspaces", test_workspace_queries },
918
        { "paths-and-borders", test_paths_and_border_policy },
919
        { "rules", test_rule_application },
920
        { "focus", test_focus_queries },
921
        { "window-state", test_window_state_storage },
922
        { "child-reaping", test_child_reaping },
923
        { "spawn-tracking", test_spawn_tracking },
924
        { "command-failure-notification", test_command_failure_notification },
925
        { "commands-without-clients", test_commands_without_clients },
926
        { "small-helpers", test_small_helpers },
927
        { "client-accessors", test_client_accessors },
928
        { "autostart", test_autostart_execution },
929
        { "pointer-gestures", test_pointer_gestures },
930
        { "input-method", test_input_method_lifecycle },
931
        { "input-early-paths", test_input_early_paths },
932
        { "command-line", test_command_line_errors },
933
        { "pointer-and-decoration", test_pointer_and_decoration_lifecycle },
934
        { "keybindings-and-repeat", test_keybindings_and_repeat },
935
    };
936
937
    if (argc == 2 && !strcmp(argv[1], "--list")) {
938
        for (size_t i = 0; i < LENGTH(tests); i++)
939
            puts(tests[i].name);
940
        return 0;
941
    }
942
    if (argc != 2) {
943
        fprintf(stderr, "usage: unit --list|TEST\n");
944
        return 2;
945
    }
946
    for (size_t i = 0; i < LENGTH(tests); i++) {
947
        if (!strcmp(argv[1], tests[i].name)) {
948
            tests[i].run();
949
            return 0;
950
        }
951
    }
952
    fprintf(stderr, "unknown internal test: %s\n", argv[1]);
953
    return 2;
954
}