X-Git-Url: https://git.sur5r.net/?a=blobdiff_plain;f=testcases%2Flib%2Fi3test.pm;h=d71a6e86cb9ece155cf90a67093cfb7b2aeb6567;hb=c23db20cb56847305bd8b6b362b8623e9e81db69;hp=8204f842de85a24f154ca59bafec3fc0842e4f91;hpb=51728bab77210a1050f11df0f0e27b3b88dc6674;p=i3%2Fi3 diff --git a/testcases/lib/i3test.pm b/testcases/lib/i3test.pm index 8204f842..d71a6e86 100644 --- a/testcases/lib/i3test.pm +++ b/testcases/lib/i3test.pm @@ -34,7 +34,6 @@ our @EXPORT = qw( get_dock_clients cmd sync_with_i3 - does_i3_live exit_gracefully workspace_exists focused_ws @@ -46,6 +45,39 @@ our @EXPORT = qw( $x ); +=head1 NAME + +i3test - Testcase setup module + +=encoding utf-8 + +=head1 SYNOPSIS + + use i3test; + + my $ws = fresh_workspace; + is_num_children($ws, 0, 'no containers on this workspace yet'); + cmd 'open'; + is_num_children($ws, 1, 'one container after "open"'); + + done_testing; + +=head1 DESCRIPTION + +This module is used in every i3 testcase and takes care of automatically +starting i3 before any test instructions run. It also saves you typing of lots +of boilerplate in every test file. + + +i3test automatically "use"s C, C, C, +C’s C and C so that all of them are available +to you in your testcase. + +See also C (L) +which provides additional test instructions (like C or C). + +=cut + my $tester = Test::Builder->new(); my $_cached_socket_path = undef; my $_sync_window = undef; @@ -115,6 +147,7 @@ use Test::More $test_more_args; use Data::Dumper; use AnyEvent::I3; use Time::HiRes qw(sleep); +use i3test::Test; __ $tester->BAIL_OUT("$@") if $@; feature->import(":5.10"); @@ -122,21 +155,28 @@ __ warnings->import; $x ||= i3test::X11->new; + # set the pointer to a predictable position in case a previous test has + # disturbed it + $x->root->warp_pointer(0, 0); $cv->recv if $i3_autostart; @_ = ($class); goto \&Exporter::import; } -# -# Waits for the next event and calls the given callback for every event to -# determine if this is the event we are waiting for. -# -# Can be used to wait until a window is mapped, until a ClientMessage is -# received, etc. -# -# wait_for_event $x, 0.25, sub { $_[0]->{response_type} == MAP_NOTIFY }; -# +=head1 EXPORT + +=head2 wait_for_event($timeout, $callback) + +Waits for the next event and calls the given callback for every event to +determine if this is the event we are waiting for. + +Can be used to wait until a window is mapped, until a ClientMessage is +received, etc. + + wait_for_event 0.25, sub { $_[0]->{response_type} == MAP_NOTIFY }; + +=cut sub wait_for_event { my ($timeout, $cb) = @_; @@ -165,8 +205,24 @@ sub wait_for_event { return $result; } -# thin wrapper around wait_for_event which waits for MAP_NOTIFY -# make sure to include 'structure_notify' in the window’s event_mask attribute +=head2 wait_for_map($window) + +Thin wrapper around wait_for_event which waits for MAP_NOTIFY. +Make sure to include 'structure_notify' in the window’s event_mask attribute. + +This function is called by C, so in most cases, you don’t need to +call it on your own. If you need special setup of the window before mapping, +you might have to map it on your own and use this function: + + my $window = open_window(dont_map => 1); + # Do something special with the window first + # … + + # Now map it and wait until it’s been mapped + $window->map; + wait_for_map($window); + +=cut sub wait_for_map { my ($win) = @_; my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win; @@ -175,9 +231,20 @@ sub wait_for_map { }; } -# Wrapper around wait_for_event which waits for UNMAP_NOTIFY. Also calls -# sync_with_i3 to make sure i3 also picked up and processed the UnmapNotify -# event. +=head2 wait_for_unmap($window) + +Wrapper around C which waits for UNMAP_NOTIFY. Also calls +C to make sure i3 also picked up and processed the UnmapNotify +event. + + my $ws = fresh_workspace; + my $window = open_window; + is_num_children($ws, 1, 'one window on workspace'); + $window->unmap; + wait_for_unmap; + is_num_children($ws, 0, 'no more windows on this workspace'); + +=cut sub wait_for_unmap { my ($win) = @_; # my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win; @@ -187,25 +254,76 @@ sub wait_for_unmap { sync_with_i3(); } -# -# Opens a new window (see X11::XCB::Window), maps it, waits until it got mapped -# and synchronizes with i3. -# -# set dont_map to a true value to avoid mapping -# -# if you want to change aspects of your window before it would be mapped, -# set before_map to a coderef. $window gets passed as $_ and as first argument. -# -# if you set both dont_map and before_map, the coderef will be called nevertheless -# -# -# default values: -# class => WINDOW_CLASS_INPUT_OUTPUT -# rect => [ 0, 0, 30, 30 ] -# background_color => '#c0c0c0' -# event_mask => [ 'structure_notify' ] -# name => 'Window ' -# +=head2 open_window([ $args ]) + +Opens a new window (see C), maps it, waits until it got mapped +and synchronizes with i3. + +The following arguments can be passed: + +=over 4 + +=item class + +The X11 window class (e.g. WINDOW_CLASS_INPUT_OUTPUT), not to be confused with +the WM_CLASS! + +=item rect + +An arrayref with 4 members specifying the initial geometry (position and size) +of the window, e.g. C<< [ 0, 100, 70, 50 ] >> for a window appearing at x=0, y=100 +with width=70 and height=50. + +Note that this is entirely irrelevant for tiling windows. + +=item background_color + +The background pixel color of the window, formatted as "#rrggbb", like HTML +color codes (e.g. #c0c0c0). This is useful to tell windows apart when actually +watching the testcases. + +=item event_mask + +An arrayref containing strings which describe the X11 event mask we use for that +window. The default is C<< [ 'structure_notify' ] >>. + +=item name + +The window’s C<_NET_WM_NAME> (UTF-8 window title). By default, this is "Window +n" with n being replaced by a counter to keep windows apart. + +=item dont_map + +Set to a true value to avoid mapping the window (making it visible). + +=item before_map + +A coderef which is called before the window is mapped (unless C is +true). The freshly created C<$window> is passed as C<$_> and as the first +argument. + +=back + +The default values are equivalent to this call: + + open_window( + class => WINDOW_CLASS_INPUT_OUTPUT + rect => [ 0, 0, 30, 30 ] + background_color => '#c0c0c0' + event_mask => [ 'structure_notify' ] + name => 'Window ' + ); + +Usually, though, calls are simpler: + + my $top_window = open_window; + +To identify the resulting window object in i3 commands, use the id property: + + my $top_window = open_window; + cmd '[id="' . $top_window->id . '"] kill'; + +=cut sub open_window { my %args = @_ == 1 ? %{$_[0]} : @_; @@ -233,8 +351,14 @@ sub open_window { return $window; } -# Thin wrapper around open_window which sets window_type to -# _NET_WM_WINDOW_TYPE_UTILITY to make the window floating. +=head2 open_floating_window([ $args ]) + +Thin wrapper around open_window which sets window_type to +C<_NET_WM_WINDOW_TYPE_UTILITY> to make the window floating. + +The arguments are the same as those of C. + +=cut sub open_floating_window { my %args = @_ == 1 ? %{$_[0]} : @_; @@ -247,9 +371,18 @@ sub open_empty_con { my ($i3) = @_; my $reply = $i3->command('open')->recv; - return $reply->{id}; + return $reply->[0]->{id}; } +=head2 get_workspace_names() + +Returns an arrayref containing the name of every workspace (regardless of its +output) which currently exists. + + my $workspace_names = get_workspace_names; + is(scalar @$workspace_names, 3, 'three workspaces exist currently'); + +=cut sub get_workspace_names { my $i3 = i3(get_socket_path()); my $tree = $i3->get_tree->recv; @@ -258,20 +391,29 @@ sub get_workspace_names { for my $output (@outputs) { next if $output->{name} eq '__i3'; # get the first CT_CON of each output - my $content = first { $_->{type} == 2 } @{$output->{nodes}}; + my $content = first { $_->{type} eq 'con' } @{$output->{nodes}}; @cons = (@cons, @{$content->{nodes}}); } [ map { $_->{name} } @cons ] } +=head2 get_unused_workspace + +Returns a workspace name which has not yet been used. See also +C which directly switches to an unused workspace. + + my $ws = get_unused_workspace; + cmd "workspace $ws"; + +=cut sub get_unused_workspace { my @names = get_workspace_names(); my $tmp; - do { $tmp = tmpnam() } while ($tmp ~~ @names); + do { $tmp = tmpnam() } while ((scalar grep { $_ eq $tmp } @names) > 0); $tmp } -=head2 fresh_workspace(...) +=head2 fresh_workspace([ $args ]) Switches to an unused workspace and returns the name of that workspace. @@ -288,11 +430,11 @@ sub fresh_workspace { if (exists($args{output})) { my $i3 = i3(get_socket_path()); my $tree = $i3->get_tree->recv; - my $output = first { $_->{name} eq "xinerama-$args{output}" } + my $output = first { $_->{name} eq "fake-$args{output}" } @{$tree->{nodes}}; die "BUG: Could not find output $args{output}" unless defined($output); # Get the focused workspace on that output and switch to it. - my $content = first { $_->{type} == 2 } @{$output->{nodes}}; + my $content = first { $_->{type} eq 'con' } @{$output->{nodes}}; my $focused = $content->{focus}->[0]; my $workspace = first { $_->{id} == $focused } @{$content->{nodes}}; $workspace = $workspace->{name}; @@ -304,6 +446,30 @@ sub fresh_workspace { $unused } +=head2 get_ws($workspace) + +Returns the container (from the i3 layout tree) which represents C<$workspace>. + + my $ws = fresh_workspace; + my $ws_con = get_ws($ws); + ok(!$ws_con->{urgent}, 'fresh workspace not marked urgent'); + +Here is an example which counts the number of urgent containers recursively, +starting from the workspace container: + + sub count_urgent { + my ($con) = @_; + + my @children = (@{$con->{nodes}}, @{$con->{floating_nodes}}); + my $urgent = grep { $_->{urgent} } @children; + $urgent += count_urgent($_) for @children; + return $urgent; + } + my $urgent = count_urgent(get_ws($ws)); + is($urgent, 3, "three urgent windows on workspace $ws"); + + +=cut sub get_ws { my ($name) = @_; my $i3 = i3(get_socket_path()); @@ -313,7 +479,7 @@ sub get_ws { my @workspaces; for my $output (@outputs) { # get the first CT_CON of each output - my $content = first { $_->{type} == 2 } @{$output->{nodes}}; + my $content = first { $_->{type} eq 'con' } @{$output->{nodes}}; @workspaces = (@workspaces, @{$content->{nodes}}); } @@ -322,17 +488,70 @@ sub get_ws { return first { $_->{name} eq $name } @workspaces; } -# -# returns the content (== tree, starting from the node of a workspace) -# of a workspace. If called in array context, also includes the focus -# stack of the workspace -# +=head2 get_ws_content($workspace) + +Returns the content (== tree, starting from the node of a workspace) +of a workspace. If called in array context, also includes the focus +stack of the workspace. + + my $nodes = get_ws_content($ws); + is(scalar @$nodes, 4, 'there are four containers at workspace-level'); + +Or, in array context: + + my $window = open_window; + my ($nodes, $focus) = get_ws_content($ws); + is($focus->[0], $window->id, 'newly opened window focused'); + +Note that this function does not do recursion for you! It only returns the +containers B. If you want to work with all containers (even +nested ones) on a workspace, you have to use recursion: + + # NB: This function does not count floating windows + sub count_urgent { + my ($nodes) = @_; + + my $urgent = 0; + for my $con (@$nodes) { + $urgent++ if $con->{urgent}; + $urgent += count_urgent($con->{nodes}); + } + + return $urgent; + } + my $nodes = get_ws_content($ws); + my $urgent = count_urgent($nodes); + is($urgent, 3, "three urgent windows on workspace $ws"); + +If you also want to deal with floating windows, you have to use C +instead and access C<< ->{nodes} >> and C<< ->{floating_nodes} >> on your own. + +=cut sub get_ws_content { my ($name) = @_; my $con = get_ws($name); return wantarray ? ($con->{nodes}, $con->{focus}) : $con->{nodes}; } +=head2 get_focused($workspace) + +Returns the container ID of the currently focused container on C<$workspace>. + +Note that the container ID is B the X11 window ID, so comparing the result +of C with a window's C<< ->{id} >> property does B work. + + my $ws = fresh_workspace; + my $first_window = open_window; + my $first_id = get_focused(); + + my $second_window = open_window; + my $second_id = get_focused(); + + cmd 'focus left'; + + is(get_focused($ws), $first_id, 'second window focused'); + +=cut sub get_focused { my ($ws) = @_; my $con = get_ws($ws); @@ -350,6 +569,16 @@ sub get_focused { return $lf; } +=head2 get_dock_clients([ $dockarea ]) + +Returns an array of all dock containers in C<$dockarea> (one of "top" or +"bottom"). If C<$dockarea> is not specified, returns an array of all dock +containers in any dockarea. + + my @docked = get_dock_clients; + is(scalar @docked, 0, 'no dock clients yet'); + +=cut sub get_dock_clients { my $which = shift; @@ -360,13 +589,13 @@ sub get_dock_clients { for my $output (@outputs) { if (!defined($which)) { @docked = (@docked, map { @{$_->{nodes}} } - grep { $_->{type} == 5 } + grep { $_->{type} eq 'dockarea' } @{$output->{nodes}}); } elsif ($which eq 'top') { - my $first = first { $_->{type} == 5 } @{$output->{nodes}}; + my $first = first { $_->{type} eq 'dockarea' } @{$output->{nodes}}; @docked = (@docked, @{$first->{nodes}}) if defined($first); } elsif ($which eq 'bottom') { - my @matching = grep { $_->{type} == 5 } @{$output->{nodes}}; + my @matching = grep { $_->{type} eq 'dockarea' } @{$output->{nodes}}; my $last = $matching[-1]; @docked = (@docked, @{$last->{nodes}}) if defined($last); } @@ -374,51 +603,95 @@ sub get_dock_clients { return @docked; } +=head2 cmd($command) + +Sends the specified command to i3. + + my $ws = unused_workspace; + cmd "workspace $ws"; + cmd 'focus right'; + +=cut sub cmd { i3(get_socket_path())->command(@_)->recv } +=head2 workspace_exists($workspace) + +Returns true if C<$workspace> is the name of an existing workspace. + + my $old_ws = focused_ws; + # switch away from where we currently are + fresh_workspace; + + ok(workspace_exists($old_ws), 'old workspace still exists'); + +=cut sub workspace_exists { my ($name) = @_; - ($name ~~ @{get_workspace_names()}) + (scalar grep { $_ eq $name } @{get_workspace_names()}) > 0; } =head2 focused_ws Returns the name of the currently focused workspace. + my $ws = focused_ws; + is($ws, '1', 'i3 starts on workspace 1'); + =cut sub focused_ws { my $i3 = i3(get_socket_path()); my $tree = $i3->get_tree->recv; my $focused = $tree->{focus}->[0]; my $output = first { $_->{id} == $focused } @{$tree->{nodes}}; - my $content = first { $_->{type} == 2 } @{$output->{nodes}}; + my $content = first { $_->{type} eq 'con' } @{$output->{nodes}}; my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}}; return $first->{name} } -# -# Sends an I3_SYNC ClientMessage with a random value to the root window. -# i3 will reply with the same value, but, due to the order of events it -# processes, only after all other events are done. -# -# This can be used to ensure the results of a cmd 'focus left' are pushed to -# X11 and that $x->input_focus returns the correct value afterwards. -# -# See also docs/testsuite for a long explanation -# +=head2 sync_with_i3([ $args ]) + +Sends an I3_SYNC ClientMessage with a random value to the root window. +i3 will reply with the same value, but, due to the order of events it +processes, only after all other events are done. + +This can be used to ensure the results of a cmd 'focus left' are pushed to +X11 and that C<< $x->input_focus >> returns the correct value afterwards. + +See also L for a longer explanation. + + my $window = open_window; + $window->add_hint('urgency'); + # Ensure i3 picked up the change + sync_with_i3; + +The only time when you need to use the C argument is when you just +killed your own X11 connection: + + cmd 'kill client'; + # We need to re-establish the X11 connection which we just killed :). + $x = i3test::X11->new; + sync_with_i3(no_cache => 1); + +=cut sub sync_with_i3 { + my %args = @_ == 1 ? %{$_[0]} : @_; + # Since we need a (mapped) window for receiving a ClientMessage, we create # one on the first call of sync_with_i3. It will be re-used in all # subsequent calls. - if (!defined($_sync_window)) { + if (!exists($args{window_id}) && + (!defined($_sync_window) || exists($args{no_cache}))) { $_sync_window = open_window( rect => [ -15, -15, 10, 10 ], override_redirect => 1, ); } + my $window_id = delete $args{window_id}; + $window_id //= $_sync_window->id; + my $root = $x->get_root_window(); # Generate a random number to identify this particular ClientMessage. my $myrnd = int(rand(255)) + 1; @@ -431,7 +704,7 @@ sub sync_with_i3 { $root, # destination window $x->atom(name => 'I3_SYNC')->id, - $_sync_window->id, # data[0]: our own window id + $window_id, # data[0]: our own window id $myrnd, # data[1]: a random value to identify the request 0, 0, @@ -452,15 +725,22 @@ sub sync_with_i3 { }; } -sub does_i3_live { - my $tree = i3(get_socket_path())->get_tree->recv; - my @nodes = @{$tree->{nodes}}; - my $ok = (@nodes > 0); - $tester->ok($ok, 'i3 still lives'); - return $ok; -} +=head2 exit_gracefully($pid, [ $socketpath ]) + +Tries to exit i3 gracefully (with the 'exit' cmd) or kills the PID if that fails. -# Tries to exit i3 gracefully (with the 'exit' cmd) or kills the PID if that fails +If C<$socketpath> is not specified, C will be called. + +You only need to use this function if you have launched i3 on your own with +C. Otherwise, it will be automatically called when the +testcase ends. + + use i3test i3_autostart => 0; + my $pid = launch_with_config($config); + # … + exit_gracefully($pid); + +=cut sub exit_gracefully { my ($pid, $socketpath) = @_; $socketpath ||= get_socket_path(); @@ -485,7 +765,20 @@ sub exit_gracefully { undef $i3_pid; } -# Gets the socket path from the I3_SOCKET_PATH atom stored on the X11 root window +=head2 get_socket_path([ $cache ]) + +Gets the socket path from the C atom stored on the X11 root +window. After the first call, this function will return a cached version of the +socket path unless you specify a false value for C<$cache>. + + my $i3 = i3(get_socket_path()); + $i3->command('nop test example')->recv; + +To avoid caching: + + my $i3 = i3(get_socket_path(0)); + +=cut sub get_socket_path { my ($cache) = @_; $cache ||= 1; @@ -505,9 +798,26 @@ sub get_socket_path { return $socketpath; } -# -# launches a new i3 process with the given string as configuration file. -# useful for tests which test specific config file directives. +=head2 launch_with_config($config, [ $args ]) + +Launches a new i3 process with C<$config> as configuration file. Useful for +tests which test specific config file directives. + + use i3test i3_autostart => 0; + + my $config = < $ENV{TESTNAME}, valgrind => $ENV{VALGRIND}, strace => $ENV{STRACE}, + xtrace => $ENV{XTRACE}, restart => $ENV{RESTART}, cv => $cv, dont_create_temp_dir => $args{dont_create_temp_dir}, @@ -557,6 +868,12 @@ sub launch_with_config { return $i3_pid; } +=head1 AUTHOR + +Michael Stapelberg + +=cut + package i3test::X11; use parent 'X11::XCB::Connection';