diff --git a/CMakeLists.txt b/CMakeLists.txt index f963539a1..bcebcb8b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -375,12 +375,16 @@ endif() # }}} # {{{ Tests +add_executable(test-gravity tests/test-gravity.c) +target_link_libraries(test-gravity + ${AWESOME_COMMON_REQUIRED_LDFLAGS} ${AWESOME_REQUIRED_LDFLAGS}) add_custom_target(check-integration sh -c "CMAKE_BINARY_DIR='${CMAKE_BINARY_DIR}' ${CMAKE_SOURCE_DIR}/tests/run.sh" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Running integration tests" USES_TERMINAL VERBATIM) +add_dependencies(check-integration test-gravity) add_custom_target(check-requires lua "${CMAKE_SOURCE_DIR}/build-utils/check_for_invalid_requires.lua" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} diff --git a/tests/run.sh b/tests/run.sh index fae0fb98d..5464d80e4 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -34,6 +34,7 @@ if [ -z "$build_dir" ]; then build_dir="$source_dir" fi fi +export build_dir # Get test files: test*, or the ones provided as args (relative to tests/). if [ $# != 0 ]; then diff --git a/tests/test-gravity.c b/tests/test-gravity.c new file mode 100644 index 000000000..047b2794e --- /dev/null +++ b/tests/test-gravity.c @@ -0,0 +1,581 @@ +/* + * A test for gravity handling. + * + * Copyright © 2017 Uli Schlachter + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * + * Loosely based on test-gravity.c from metacity, which does not have a + * copyright or license header, but Metacity itself is licensed under GPLv2. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * This program tests various gravity-related things. For each possible gravity, + * this does: + * - Create a window with this gravity. + * [Wait for expose event] + * - Check that the window is mapped in the correct place. + * - Resize the window. + * [Wait for real configure notify] + * - Check that the window moves as expected by the gravity. + * - Move and resize the window (go back to the original size). + * [Wait for real configure notify] + * - Check that the window moves as expected by the gravity. + * - Move the window. + * [Wait for synthetic configure notify] + * - Check that the window moves to the expected position. + * + * This needs a WM that supports _NET_FRAME_EXTENTS. + * Also, this assumes that the WM allows and applies window moves and resizes by + * the application. + */ +enum test_state { + TEST_STATE_CREATED, + TEST_STATE_RESIZED1, + TEST_STATE_RESIZED2, + TEST_STATE_MOVED, + TEST_STATE_DONE, + TEST_STATE_DONE_GOT_CONFIGURE, +}; + +#define WIN_POS_X1 50 +#define WIN_POS_Y1 70 +#define WIN_WIDTH1 30 +#define WIN_HEIGHT1 40 + +#define WIN_POS_X2 42 +#define WIN_POS_Y2 43 +#define WIN_WIDTH2 12 +#define WIN_HEIGHT2 16 + +static int32_t state_difference[][2] = { + [TEST_STATE_CREATED] = { 0, 0 }, + [TEST_STATE_RESIZED1] = { WIN_WIDTH1 - WIN_WIDTH2, WIN_HEIGHT1 - WIN_HEIGHT2 }, + [TEST_STATE_RESIZED2] = { 0, 0 }, + [TEST_STATE_MOVED] = { 0, 0 }, + [TEST_STATE_DONE] = { 0, 0 }, + [TEST_STATE_DONE_GOT_CONFIGURE] = { 0, 0 }, +}; + +struct window_state { + xcb_window_t window; + enum test_state state; + xcb_gravity_t gravity; + bool sent_delayed_proceed; +}; + +static xcb_connection_t *c = NULL; +static xcb_screen_t *screen; +static struct window_state window_state; +static xcb_atom_t net_frame_extents; +static bool done; +static bool had_error = false; + +static void check(bool cond, const char *format, ...) + __attribute__ ((format(printf, 2, 3))); +static void do_log(const char *format, ...) + __attribute__ ((format(printf, 1, 2))); + +static void check(bool cond, const char *format, ...) +{ + va_list ap; + + if (cond) + return; + + va_start(ap, format); + fprintf(stderr, "ERROR: "); + vfprintf(stderr, format, ap); + fprintf(stderr, "\n"); + va_end(ap); + + if (c) { + xcb_no_operation(c); + free(xcb_get_input_focus_reply(c, xcb_get_input_focus(c), NULL)); + } + had_error = true; +} + +static void do_log(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + fprintf(stdout, "LOG: "); + vfprintf(stdout, format, ap); + fprintf(stdout, "\n"); + va_end(ap); +} + +static void query_frame_extents(xcb_window_t win, uint32_t *left, + uint32_t *right, uint32_t *top, uint32_t *bottom) +{ + xcb_get_property_reply_t *reply = xcb_get_property_reply(c, + xcb_get_property(c, 0, win, net_frame_extents, XCB_ATOM_CARDINAL, + 0, 4), NULL); + + *left = 0; + *right = 0; + *top = 0; + *bottom = 0; + + if (reply && reply->length == 4) + { + uint32_t *data = (uint32_t *) xcb_get_property_value(reply); + *left = data[0]; + *right = data[1]; + *top = data[2]; + *bottom = data[3]; + } + + free(reply); +} + +static void delayed_proceed_with_window(void) +{ + xcb_client_message_event_t ev; + + if (window_state.sent_delayed_proceed) + return; + + memset(&ev, 0, sizeof(ev)); + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = window_state.window; + ev.format = 32; + ev.type = XCB_ATOM_NOTICE; + ev.data.data32[0] = (int) window_state.state; + + xcb_send_event(c, 0, window_state.window, XCB_EVENT_MASK_NO_EVENT, (char *) &ev); + window_state.sent_delayed_proceed = true; +} + +static void get_geometry(xcb_window_t win, int32_t *x, int32_t *y, + uint32_t *width, uint32_t *height, uint32_t *left, uint32_t *right, + uint32_t *top, uint32_t *bottom) +{ + xcb_get_geometry_cookie_t geo_c = xcb_get_geometry_unchecked(c, win); + xcb_translate_coordinates_cookie_t coord_c = + xcb_translate_coordinates(c, win, screen->root, 0, 0); + + query_frame_extents(win, left, right, top, bottom); + + xcb_get_geometry_reply_t *geo = xcb_get_geometry_reply(c, geo_c, NULL); + xcb_translate_coordinates_reply_t *coord = + xcb_translate_coordinates_reply(c, coord_c, NULL); + + *x = coord->dst_x; + *y = coord->dst_y; + *width = geo->width; + *height = geo->height; + + free(coord); + free(geo); +} + +static const char *gravity_to_string(xcb_gravity_t gravity) { + switch (gravity) + { + case XCB_GRAVITY_NORTH_WEST: + return "NorthWest"; + case XCB_GRAVITY_NORTH: + return "North"; + case XCB_GRAVITY_NORTH_EAST: + return "NorthEast"; + case XCB_GRAVITY_WEST: + return "West"; + case XCB_GRAVITY_CENTER: + return "Center"; + case XCB_GRAVITY_EAST: + return "East"; + case XCB_GRAVITY_SOUTH_WEST: + return "SouthWest"; + case XCB_GRAVITY_SOUTH: + return "South"; + case XCB_GRAVITY_SOUTH_EAST: + return "SouthEast"; + case XCB_GRAVITY_STATIC: + return "Static"; + default: + fprintf(stderr, "Unknown gravity value %d", (int) gravity); + exit(1); + } +} + +static const char *state_to_string(enum test_state state) +{ + switch (state) + { + case TEST_STATE_CREATED: + return "CREATED"; + case TEST_STATE_RESIZED1: + return "RESIZED1"; + case TEST_STATE_RESIZED2: + return "RESIZED2"; + case TEST_STATE_MOVED: + return "MOVED"; + case TEST_STATE_DONE: + return "DONE"; + case TEST_STATE_DONE_GOT_CONFIGURE: + return "DONE+c"; + default: + return "UNKNOWN"; + } +} + +static void check_geometry(int32_t expected_x, int32_t expected_y, uint32_t expected_width, uint32_t expected_height) +{ + int32_t actual_x, actual_y; + uint32_t actual_width, actual_height, left, right, top, bottom; + get_geometry(window_state.window, &actual_x, &actual_y, &actual_width, &actual_height, + &left, &right, &top, &bottom); + + int32_t offset_x, offset_y; + int32_t diff_x = state_difference[window_state.state][0], diff_y = state_difference[window_state.state][1]; + switch (window_state.gravity) + { + case XCB_GRAVITY_NORTH_WEST: + offset_x = left; + offset_y = top; + break; + case XCB_GRAVITY_NORTH: + offset_x = (left - right + diff_x + 1) / 2; + offset_y = top; + break; + case XCB_GRAVITY_NORTH_EAST: + offset_x = -right + diff_x; + offset_y = top; + break; + case XCB_GRAVITY_WEST: + offset_x = left; + offset_y = (top - bottom + diff_y + 1) / 2; + break; + case XCB_GRAVITY_CENTER: + offset_x = (left - right + diff_x + 1) / 2; + offset_y = (top - bottom + diff_y + 1) / 2; + break; + case XCB_GRAVITY_EAST: + offset_x = -right + diff_x; + offset_y = (top - bottom + diff_y + 1) / 2; + break; + case XCB_GRAVITY_SOUTH_WEST: + offset_x = left; + offset_y = -bottom + diff_y; + break; + case XCB_GRAVITY_SOUTH: + offset_x = (left - right + diff_x + 1) / 2; + offset_y = -bottom + diff_y; + break; + case XCB_GRAVITY_SOUTH_EAST: + offset_x = -right + diff_x; + offset_y = -bottom + diff_y; + break; + case XCB_GRAVITY_STATIC: + offset_x = 0; + offset_y = 0; + break; + default: + fprintf(stderr, "Unknown gravity!?\n"); + return; + } + + do_log("Checking if position of window with gravity %s is %dx%d and size " + "is %dx%d when frame has size left=%d, right=%d, top=%d, bottom=%d", + gravity_to_string(window_state.gravity), + expected_x, expected_y, expected_width, expected_height, + left, right, top, bottom); + + check(expected_width == actual_width, + "For window with gravity %s in state %s, expected width = %d, but got %d", + gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_width, (int) actual_width); + check(expected_height == actual_height, + "For window with gravity %s in state %s, expected height = %d, but got %d", + gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_height, (int) actual_height); + check(expected_x + offset_x == actual_x, + "For window with gravity %s in state %s, expected x = %d+%d, but got %d", + gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_x, (int) offset_x, (int) actual_x); + check(expected_y + offset_y == actual_y, + "For window with gravity %s in state %s, expected y = %d+%d, but got %d", + gravity_to_string(window_state.gravity), state_to_string(window_state.state), (int) expected_y, (int) offset_y, (int) actual_y); +} + +static void init(xcb_gravity_t gravity) +{ + const char *name = gravity_to_string(gravity); + xcb_size_hints_t size_hints; + + memset(&size_hints, 0, sizeof(size_hints)); + size_hints.win_gravity = gravity; + size_hints.flags = XCB_ICCCM_SIZE_HINT_US_POSITION + | XCB_ICCCM_SIZE_HINT_P_WIN_GRAVITY; + + window_state.window = xcb_generate_id(c); + window_state.state = TEST_STATE_CREATED; + window_state.gravity = gravity; + + do_log("creating window at %dx%d with size %dx%d", WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); + xcb_create_window(c, screen->root_depth, window_state.window, screen->root, + WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, (uint32_t[]) { + screen->white_pixel, + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_EXPOSURE + }); + xcb_change_property(c, XCB_PROP_MODE_REPLACE, window_state.window, + XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, strlen(name), name); + xcb_icccm_set_wm_size_hints(c, window_state.window, XCB_ATOM_WM_NORMAL_HINTS, &size_hints); + xcb_map_window(c, window_state.window); +} + +static void handle_expose(xcb_expose_event_t *ev) +{ + if (window_state.window != ev->window || window_state.state != TEST_STATE_CREATED) + return; + + do_log("Window in state CREATED was exposed, going to next state"); + delayed_proceed_with_window(); +} + +static void handle_configure_notify(xcb_configure_notify_event_t *ev) +{ + bool synthetic = ev->response_type & 0x80; + if (window_state.window != ev->window) + return; + + switch (window_state.state) + { + case TEST_STATE_CREATED: + return; + case TEST_STATE_RESIZED1: + if (synthetic) + return; + do_log("Window in state RESIZED1 got ConfigureNotify, going to next state"); + delayed_proceed_with_window(); + return; + case TEST_STATE_RESIZED2: + if (synthetic) + return; + do_log("Window in state RESIZED2 got ConfigureNotify, going to next state"); + delayed_proceed_with_window(); + return; + case TEST_STATE_MOVED: + if (!synthetic) + return; + do_log("Window in state MOVED got ConfigureNotify, going to next state"); + delayed_proceed_with_window(); + return; + case TEST_STATE_DONE: + if (!synthetic) + return; + } + + check(false, "Unexpected configure notify for window 0x%x with gravity %d, %dx%d+%d+%d%s", + (int) window_state.window, (int) window_state.gravity, ev->width, ev->height, + ev->x, ev->y, synthetic ? " (generated)" : ""); +} + +static void wait_for_wm(void) +{ + /* Window managers can be awfully slow and ICCCM does not provide much + * possibilities to synchronise with them. We try to do this by creating a + * window and resizing it, but without mapping it. + */ + xcb_generic_event_t *event; + xcb_window_t window = xcb_generate_id(c); + + do_log("waiting for WM"); + xcb_create_window(c, screen->root_depth, window, screen->root, + 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_EVENT_MASK, (uint32_t[]) { XCB_EVENT_MASK_STRUCTURE_NOTIFY }); + xcb_configure_window(c, window, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + (uint32_t[]) { 1, 2, 3, 4 }); + xcb_flush(c); + + while (event = xcb_wait_for_event(c)) { + if ((event->response_type & 0x7f) == XCB_CONFIGURE_NOTIFY) { + free(event); + break; + } + free(event); + } + + xcb_destroy_window(c, window); +} + +static void handle_client_message(xcb_client_message_event_t *event) +{ + /* We want a full roundtrip between "we decide to proceed" and "we actually + * proceed" so that any in-flight events are handled. To do this, we send + * ourselves two client messages. (So that any events that are still in + * flight when the first message is sent are handled) + */ + if (event->type == XCB_ATOM_NOTICE) + { + wait_for_wm(); + + event->response_type = XCB_CLIENT_MESSAGE; + event->type = XCB_ATOM_WM_COMMAND; + xcb_send_event(c, 0, event->window, XCB_EVENT_MASK_NO_EVENT, (char *) event); + return; + } + if (event->type != XCB_ATOM_WM_COMMAND) + return; + + check(window_state.window == event->window, "Got weird client message?!?"); + + if (event->data.data32[0] != (int) window_state.state) + return; + + window_state.sent_delayed_proceed = false; + switch (window_state.state) + { + case TEST_STATE_CREATED: + /* Verify the position and size set in init() */ + check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); + + do_log("Resizing window to size %dx%d", WIN_WIDTH2, WIN_HEIGHT2); + xcb_configure_window(c, window_state.window, + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + (uint32_t[]) { WIN_WIDTH2, WIN_HEIGHT2 }); + window_state.state = TEST_STATE_RESIZED1; + return; + case TEST_STATE_RESIZED1: + check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH2, WIN_HEIGHT2); + + do_log("Moving+resizing window to position %dx%d and size %dx%d", + WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); + xcb_configure_window(c, window_state.window, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + (uint32_t[]) { WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1 }); + window_state.state = TEST_STATE_RESIZED2; + return; + case TEST_STATE_RESIZED2: + check_geometry(WIN_POS_X1, WIN_POS_Y1, WIN_WIDTH1, WIN_HEIGHT1); + + do_log("Moving window to position %dx%d", WIN_POS_X2, WIN_POS_Y2); + xcb_configure_window(c, window_state.window, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, + (uint32_t[]) { WIN_POS_X2, WIN_POS_Y2 }); + window_state.state = TEST_STATE_MOVED; + return; + case TEST_STATE_MOVED: + check_geometry(WIN_POS_X2, WIN_POS_Y2, WIN_WIDTH1, WIN_HEIGHT1); + + xcb_destroy_window(c, window_state.window); + window_state.window = XCB_NONE; + window_state.state = TEST_STATE_DONE; + done = true; + return; + } +} + +static void event_loop(void) +{ + xcb_generic_event_t *ev; + + done = false; + xcb_flush(c); + while (!done && (ev = xcb_wait_for_event(c))) + { + uint8_t type = ev->response_type & 0x7f; + switch (type) + { + case XCB_EXPOSE: + handle_expose((xcb_expose_event_t *) ev); + break; + case XCB_CONFIGURE_NOTIFY: + handle_configure_notify((xcb_configure_notify_event_t *) ev); + break; + case XCB_CLIENT_MESSAGE: + handle_client_message((xcb_client_message_event_t *) ev); + case XCB_REPARENT_NOTIFY: + case XCB_MAP_NOTIFY: + case XCB_UNMAP_NOTIFY: + case XCB_DESTROY_NOTIFY: + break; + default: + printf("Got unexpected event of type 0x%x\n", (int) ev->response_type); + } + free(ev); + xcb_flush(c); + } +} + +static xcb_atom_t intern_atom(const char *str) +{ + xcb_atom_t result; + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(c, + xcb_intern_atom(c, 0, strlen(str), str), NULL); + if (!reply) + return XCB_NONE; + result = reply->atom; + free(reply); + return result; +} + +static void run_test(xcb_gravity_t gravity) +{ + do_log("Doing run for gravity %s", gravity_to_string(gravity)); + init(gravity); + event_loop(); + do_log("Finished run for gravity %s", gravity_to_string(gravity)); +} + +int main() +{ + int default_screen; + + c = xcb_connect(NULL, &default_screen); + if (xcb_connection_has_error(c)) + { + fprintf(stderr, "Could not connect to X11 server: %d", + xcb_connection_has_error(c)); + return 1; + } + screen = xcb_aux_get_screen(c, default_screen); + net_frame_extents = intern_atom("_NET_FRAME_EXTENTS"); + + run_test(XCB_GRAVITY_NORTH_WEST); + run_test(XCB_GRAVITY_NORTH); + run_test(XCB_GRAVITY_NORTH_EAST); + run_test(XCB_GRAVITY_WEST); + run_test(XCB_GRAVITY_CENTER); + run_test(XCB_GRAVITY_EAST); + run_test(XCB_GRAVITY_SOUTH_WEST); + run_test(XCB_GRAVITY_SOUTH); + run_test(XCB_GRAVITY_SOUTH_EAST); + run_test(XCB_GRAVITY_STATIC); + + check(!xcb_connection_has_error(c), + "X11 connection has error: %d", xcb_connection_has_error(c)); + xcb_disconnect(c); + if (had_error) + return 1; + puts("SUCCESS"); + return 0; +} + +// vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/tests/test-gravity.lua b/tests/test-gravity.lua new file mode 100644 index 000000000..087a5abc9 --- /dev/null +++ b/tests/test-gravity.lua @@ -0,0 +1,64 @@ +-- Test if client's c:swap() corrupts the Lua stack + +local runner = require("_runner") +local spawn = require("awful.spawn") + +local todo = 2 +local had_error = false + +local function wait_a_bit(count) + if todo == 0 or count == 5 then + return true + end +end +runner.run_steps({ + function() + local err = spawn.with_line_callback( + { os.getenv("build_dir") .. "/test-gravity" }, + { + exit = function(what, code) + assert(what == "exit", what) + assert(code == 0, "Exit code was " .. code) + todo = todo - 1 + end, + stderr = function(line) + had_error = true + print("Read on stderr: " .. line) + end, + stdout = function(line) + if line == "SUCCESS" then + todo = todo - 1 + elseif line:sub(1, 5) ~= "LOG: " then + had_error = true + print("Read on stdout: " .. line) + end + end + }) + assert(type(err) ~= "string", err) + return true + end, + -- Buy the external program some time to finish + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + wait_a_bit, + function() + if todo == 0 then + assert(not had_error, "Some error occurred, see above") + return true + end + end +}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80