Add a test for gravity handling (#1760)

This adds a C program which tests if the window manager handles
gravities correctly. This program is loosely based on metacity's
test-gravity.c, but completely rewritten and this version does automatic
tests instead of allowing the user to perform testing by hand.

By having this as a self-contained C program, it is possible to compare
awesome's behaviour with the behaviour of other WMs.

In my testing, only metacity and awesome pass this test. This is not
that much of a big surprise since awesome was fixed in
https://github.com/awesomeWM/awesome/pull/505 to work correctly with
metacity's test-gravity.c. However, I am surprised that e.g. Fluxbox
gets this wrong.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2017-05-13 23:28:45 +02:00 committed by Daniel Hahler
parent 9d1d5d5461
commit 3ed0be6d85
4 changed files with 650 additions and 0 deletions

View File

@ -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}

View File

@ -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

581
tests/test-gravity.c Normal file
View File

@ -0,0 +1,581 @@
/*
* A test for gravity handling.
*
* Copyright © 2017 Uli Schlachter <psychon@znc.in>
*
* 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 <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xcb_icccm.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* 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

64
tests/test-gravity.lua Normal file
View File

@ -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