2 # vim:ts=4:sw=4:expandtab
3 use strict; use warnings;
5 use File::Temp qw(tmpnam tempfile tempdir);
10 use lib qw(@abs_top_srcdir@/AnyEvent-I3/blib/lib);
12 use List::Util qw(first);
13 use Time::HiRes qw(sleep);
15 use Scalar::Util qw(blessed);
17 use i3test::Util qw(slurp);
54 i3test - Testcase setup module
62 my $ws = fresh_workspace;
63 is_num_children($ws, 0, 'no containers on this workspace yet');
65 is_num_children($ws, 1, 'one container after "open"');
71 This module is used in every i3 testcase and takes care of automatically
72 starting i3 before any test instructions run. It also saves you typing of lots
73 of boilerplate in every test file.
76 i3test automatically "use"s C<Test::More>, C<Data::Dumper>, C<AnyEvent::I3>,
77 C<Time::HiRes>’s C<sleep> and C<i3test::Test> so that all of them are available
78 to you in your testcase.
80 See also C<i3test::Test> (L<http://build.i3wm.org/docs/lib-i3test-test.html>)
81 which provides additional test instructions (like C<ok> or C<is>).
85 my $tester = Test::Builder->new();
86 my $_cached_socket_path = undef;
87 my $_sync_window = undef;
88 my $tmp_socket_path = undef;
95 return $window_count++;
104 # testcases which start i3 manually should always call exit_gracefully
105 # on their own. Let’s see, whether they really did.
106 if (! $i3_autostart) {
107 return unless $i3_pid;
109 $tester->ok(undef, 'testcase called exit_gracefully()');
112 # don't trigger SIGCHLD handler
115 # From perldoc -v '$?':
116 # Inside an "END" subroutine $? contains the value
117 # that is going to be given to "exit()".
119 # Since waitpid sets $?, we need to localize it,
120 # otherwise TAP would be misinterpreted our return status
123 # When measuring code coverage, try to exit i3 cleanly (otherwise, .gcda
124 # files are not written)
125 if ($ENV{COVERAGE} || $ENV{VALGRIND}) {
126 exit_gracefully($i3_pid, "/tmp/nested-$ENV{DISPLAY}");
130 or $tester->BAIL_OUT("could not kill i3");
137 my ($class, %args) = @_;
140 $i3_autostart = delete($args{i3_autostart}) // 1;
141 my $i3_config = delete($args{i3_config}) // '-default';
143 my $cv = launch_with_config($i3_config, dont_block => 1)
146 my $test_more_args = '';
147 $test_more_args = join(' ', 'qw(', %args, ')') if keys %args;
151 use Test::More $test_more_args;
154 use Time::HiRes qw(sleep);
157 $tester->BAIL_OUT("$@") if $@;
158 feature->import(":5.10");
162 $x ||= i3test::X11->new;
163 # set the pointer to a predictable position in case a previous test has
165 $x->root->warp_pointer(0, 0);
166 $cv->recv if $i3_autostart;
169 goto \&Exporter::import;
174 =head2 wait_for_event($timeout, $callback)
176 Waits for the next event and calls the given callback for every event to
177 determine if this is the event we are waiting for.
179 Can be used to wait until a window is mapped, until a ClientMessage is
182 wait_for_event 0.25, sub { $_[0]->{response_type} == MAP_NOTIFY };
186 my ($timeout, $cb) = @_;
192 # unfortunately, there is no constant for this
195 my $guard = AE::io $x->get_file_descriptor, $ae_read, sub {
196 while (defined(my $event = $x->poll_for_event)) {
204 # Trigger timeout after $timeout seconds (can be fractional)
205 my $t = AE::timer $timeout, 0, sub { warn "timeout ($timeout secs)"; $cv->send(0) };
207 my $result = $cv->recv;
213 =head2 wait_for_map($window)
215 Thin wrapper around wait_for_event which waits for MAP_NOTIFY.
216 Make sure to include 'structure_notify' in the window’s event_mask attribute.
218 This function is called by C<open_window>, so in most cases, you don’t need to
219 call it on your own. If you need special setup of the window before mapping,
220 you might have to map it on your own and use this function:
222 my $window = open_window(dont_map => 1);
223 # Do something special with the window first
226 # Now map it and wait until it’s been mapped
228 wait_for_map($window);
233 my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win;
234 wait_for_event 4, sub {
235 $_[0]->{response_type} == MAP_NOTIFY and $_[0]->{window} == $id
239 =head2 wait_for_unmap($window)
241 Wrapper around C<wait_for_event> which waits for UNMAP_NOTIFY. Also calls
242 C<sync_with_i3> to make sure i3 also picked up and processed the UnmapNotify
245 my $ws = fresh_workspace;
246 my $window = open_window;
247 is_num_children($ws, 1, 'one window on workspace');
250 is_num_children($ws, 0, 'no more windows on this workspace');
255 # my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win;
256 wait_for_event 4, sub {
257 $_[0]->{response_type} == UNMAP_NOTIFY # and $_[0]->{window} == $id
262 =head2 open_window([ $args ])
264 Opens a new window (see C<X11::XCB::Window>), maps it, waits until it got mapped
265 and synchronizes with i3.
267 The following arguments can be passed:
273 The X11 window class (e.g. WINDOW_CLASS_INPUT_OUTPUT), not to be confused with
278 An arrayref with 4 members specifying the initial geometry (position and size)
279 of the window, e.g. C<< [ 0, 100, 70, 50 ] >> for a window appearing at x=0, y=100
280 with width=70 and height=50.
282 Note that this is entirely irrelevant for tiling windows.
284 =item background_color
286 The background pixel color of the window, formatted as "#rrggbb", like HTML
287 color codes (e.g. #c0c0c0). This is useful to tell windows apart when actually
288 watching the testcases.
292 An arrayref containing strings which describe the X11 event mask we use for that
293 window. The default is C<< [ 'structure_notify' ] >>.
297 The window’s C<_NET_WM_NAME> (UTF-8 window title). By default, this is "Window
298 n" with n being replaced by a counter to keep windows apart.
302 Set to a true value to avoid mapping the window (making it visible).
306 A coderef which is called before the window is mapped (unless C<dont_map> is
307 true). The freshly created C<$window> is passed as C<$_> and as the first
312 The default values are equivalent to this call:
315 class => WINDOW_CLASS_INPUT_OUTPUT
316 rect => [ 0, 0, 30, 30 ]
317 background_color => '#c0c0c0'
318 event_mask => [ 'structure_notify' ]
322 Usually, though, calls are simpler:
324 my $top_window = open_window;
326 To identify the resulting window object in i3 commands, use the id property:
328 my $top_window = open_window;
329 cmd '[id="' . $top_window->id . '"] kill';
333 my %args = @_ == 1 ? %{$_[0]} : @_;
335 my $dont_map = delete $args{dont_map};
336 my $before_map = delete $args{before_map};
338 $args{class} //= WINDOW_CLASS_INPUT_OUTPUT;
339 $args{rect} //= [ 0, 0, 30, 30 ];
340 $args{background_color} //= '#c0c0c0';
341 $args{event_mask} //= [ 'structure_notify' ];
342 $args{name} //= 'Window ' . counter_window();
344 my $window = $x->root->create_child(%args);
345 $window->add_hint('input');
348 # TODO: investigate why _create is not needed
350 $before_map->($window) for $window;
353 return $window if $dont_map;
356 wait_for_map($window);
360 =head2 open_floating_window([ $args ])
362 Thin wrapper around open_window which sets window_type to
363 C<_NET_WM_WINDOW_TYPE_UTILITY> to make the window floating.
365 The arguments are the same as those of C<open_window>.
368 sub open_floating_window {
369 my %args = @_ == 1 ? %{$_[0]} : @_;
371 $args{window_type} = $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY');
373 return open_window(\%args);
379 my $reply = $i3->command('open')->recv;
380 return $reply->[0]->{id};
383 =head2 get_workspace_names()
385 Returns an arrayref containing the name of every workspace (regardless of its
386 output) which currently exists.
388 my $workspace_names = get_workspace_names;
389 is(scalar @$workspace_names, 3, 'three workspaces exist currently');
392 sub get_workspace_names {
393 my $i3 = i3(get_socket_path());
394 my $tree = $i3->get_tree->recv;
395 my @outputs = @{$tree->{nodes}};
397 for my $output (@outputs) {
398 next if $output->{name} eq '__i3';
399 # get the first CT_CON of each output
400 my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
401 @cons = (@cons, @{$content->{nodes}});
403 [ map { $_->{name} } @cons ]
406 =head2 get_unused_workspace
408 Returns a workspace name which has not yet been used. See also
409 C<fresh_workspace> which directly switches to an unused workspace.
411 my $ws = get_unused_workspace;
415 sub get_unused_workspace {
416 my @names = get_workspace_names();
418 do { $tmp = tmpnam() } while ((scalar grep { $_ eq $tmp } @names) > 0);
422 =head2 fresh_workspace([ $args ])
424 Switches to an unused workspace and returns the name of that workspace.
426 Optionally switches to the specified output first.
428 my $ws = fresh_workspace;
430 # Get a fresh workspace on the second output.
431 my $ws = fresh_workspace(output => 1);
434 sub fresh_workspace {
436 if (exists($args{output})) {
437 my $i3 = i3(get_socket_path());
438 my $tree = $i3->get_tree->recv;
439 my $output = first { $_->{name} eq "fake-$args{output}" }
441 die "BUG: Could not find output $args{output}" unless defined($output);
442 # Get the focused workspace on that output and switch to it.
443 my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
444 my $focused = $content->{focus}->[0];
445 my $workspace = first { $_->{id} == $focused } @{$content->{nodes}};
446 $workspace = $workspace->{name};
447 cmd("workspace $workspace");
450 my $unused = get_unused_workspace;
451 cmd("workspace $unused");
455 =head2 get_ws($workspace)
457 Returns the container (from the i3 layout tree) which represents C<$workspace>.
459 my $ws = fresh_workspace;
460 my $ws_con = get_ws($ws);
461 ok(!$ws_con->{urgent}, 'fresh workspace not marked urgent');
463 Here is an example which counts the number of urgent containers recursively,
464 starting from the workspace container:
469 my @children = (@{$con->{nodes}}, @{$con->{floating_nodes}});
470 my $urgent = grep { $_->{urgent} } @children;
471 $urgent += count_urgent($_) for @children;
474 my $urgent = count_urgent(get_ws($ws));
475 is($urgent, 3, "three urgent windows on workspace $ws");
481 my $i3 = i3(get_socket_path());
482 my $tree = $i3->get_tree->recv;
484 my @outputs = @{$tree->{nodes}};
486 for my $output (@outputs) {
487 # get the first CT_CON of each output
488 my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
489 @workspaces = (@workspaces, @{$content->{nodes}});
492 # as there can only be one workspace with this name, we can safely
493 # return the first entry
494 return first { $_->{name} eq $name } @workspaces;
497 =head2 get_ws_content($workspace)
499 Returns the content (== tree, starting from the node of a workspace)
500 of a workspace. If called in array context, also includes the focus
501 stack of the workspace.
503 my $nodes = get_ws_content($ws);
504 is(scalar @$nodes, 4, 'there are four containers at workspace-level');
506 Or, in array context:
508 my $window = open_window;
509 my ($nodes, $focus) = get_ws_content($ws);
510 is($focus->[0], $window->id, 'newly opened window focused');
512 Note that this function does not do recursion for you! It only returns the
513 containers B<on workspace level>. If you want to work with all containers (even
514 nested ones) on a workspace, you have to use recursion:
516 # NB: This function does not count floating windows
521 for my $con (@$nodes) {
522 $urgent++ if $con->{urgent};
523 $urgent += count_urgent($con->{nodes});
528 my $nodes = get_ws_content($ws);
529 my $urgent = count_urgent($nodes);
530 is($urgent, 3, "three urgent windows on workspace $ws");
532 If you also want to deal with floating windows, you have to use C<get_ws>
533 instead and access C<< ->{nodes} >> and C<< ->{floating_nodes} >> on your own.
538 my $con = get_ws($name);
539 return wantarray ? ($con->{nodes}, $con->{focus}) : $con->{nodes};
542 =head2 get_focused($workspace)
544 Returns the container ID of the currently focused container on C<$workspace>.
546 Note that the container ID is B<not> the X11 window ID, so comparing the result
547 of C<get_focused> with a window's C<< ->{id} >> property does B<not> work.
549 my $ws = fresh_workspace;
550 my $first_window = open_window;
551 my $first_id = get_focused();
553 my $second_window = open_window;
554 my $second_id = get_focused();
558 is(get_focused($ws), $first_id, 'second window focused');
563 my $con = get_ws($ws);
565 my @focused = @{$con->{focus}};
567 while (@focused > 0) {
569 last unless defined($con->{focus});
570 @focused = @{$con->{focus}};
571 my @cons = grep { $_->{id} == $lf } (@{$con->{nodes}}, @{$con->{'floating_nodes'}});
578 =head2 get_dock_clients([ $dockarea ])
580 Returns an array of all dock containers in C<$dockarea> (one of "top" or
581 "bottom"). If C<$dockarea> is not specified, returns an array of all dock
582 containers in any dockarea.
584 my @docked = get_dock_clients;
585 is(scalar @docked, 0, 'no dock clients yet');
588 sub get_dock_clients {
591 my $tree = i3(get_socket_path())->get_tree->recv;
592 my @outputs = @{$tree->{nodes}};
593 # Children of all dockareas
595 for my $output (@outputs) {
596 if (!defined($which)) {
597 @docked = (@docked, map { @{$_->{nodes}} }
598 grep { $_->{type} eq 'dockarea' }
599 @{$output->{nodes}});
600 } elsif ($which eq 'top') {
601 my $first = first { $_->{type} eq 'dockarea' } @{$output->{nodes}};
602 @docked = (@docked, @{$first->{nodes}}) if defined($first);
603 } elsif ($which eq 'bottom') {
604 my @matching = grep { $_->{type} eq 'dockarea' } @{$output->{nodes}};
605 my $last = $matching[-1];
606 @docked = (@docked, @{$last->{nodes}}) if defined($last);
614 Sends the specified command to i3 and returns the output.
616 my $ws = unused_workspace;
622 i3(get_socket_path())->command(@_)->recv
625 =head2 workspace_exists($workspace)
627 Returns true if C<$workspace> is the name of an existing workspace.
629 my $old_ws = focused_ws;
630 # switch away from where we currently are
633 ok(workspace_exists($old_ws), 'old workspace still exists');
636 sub workspace_exists {
638 (scalar grep { $_ eq $name } @{get_workspace_names()}) > 0;
643 Returns the name of the currently focused workspace.
646 is($ws, '1', 'i3 starts on workspace 1');
650 my $i3 = i3(get_socket_path());
651 my $tree = $i3->get_tree->recv;
652 my $focused = $tree->{focus}->[0];
653 my $output = first { $_->{id} == $focused } @{$tree->{nodes}};
654 my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
655 my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}};
656 return $first->{name}
659 =head2 sync_with_i3([ $args ])
661 Sends an I3_SYNC ClientMessage with a random value to the root window.
662 i3 will reply with the same value, but, due to the order of events it
663 processes, only after all other events are done.
665 This can be used to ensure the results of a cmd 'focus left' are pushed to
666 X11 and that C<< $x->input_focus >> returns the correct value afterwards.
668 See also L<http://build.i3wm.org/docs/testsuite.html> for a longer explanation.
670 my $window = open_window;
671 $window->add_hint('urgency');
672 # Ensure i3 picked up the change
675 The only time when you need to use the C<no_cache> argument is when you just
676 killed your own X11 connection:
679 # We need to re-establish the X11 connection which we just killed :).
680 $x = i3test::X11->new;
681 sync_with_i3(no_cache => 1);
685 my %args = @_ == 1 ? %{$_[0]} : @_;
687 # Since we need a (mapped) window for receiving a ClientMessage, we create
688 # one on the first call of sync_with_i3. It will be re-used in all
690 if (!exists($args{window_id}) &&
691 (!defined($_sync_window) || exists($args{no_cache}))) {
692 $_sync_window = open_window(
693 rect => [ -15, -15, 10, 10 ],
694 override_redirect => 1,
698 my $window_id = delete $args{window_id};
699 $window_id //= $_sync_window->id;
701 my $root = $x->get_root_window();
702 # Generate a random number to identify this particular ClientMessage.
703 my $myrnd = int(rand(255)) + 1;
705 # Generate a ClientMessage, see xcb_client_message_t
706 my $msg = pack "CCSLLLLLLL",
707 CLIENT_MESSAGE, # response_type
710 $root, # destination window
711 $x->atom(name => 'I3_SYNC')->id,
713 $window_id, # data[0]: our own window id
714 $myrnd, # data[1]: a random value to identify the request
719 # Send it to the root window -- since i3 uses the SubstructureRedirect
720 # event mask, it will get the ClientMessage.
721 $x->send_event(0, $root, EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
723 return $myrnd if $args{dont_wait_for_event};
725 # now wait until the reply is here
726 return wait_for_event 4, sub {
729 return 0 unless $event->{response_type} == 161;
731 my ($win, $rnd) = unpack "LL", $event->{data};
732 return ($rnd == $myrnd);
736 =head2 exit_gracefully($pid, [ $socketpath ])
738 Tries to exit i3 gracefully (with the 'exit' cmd) or kills the PID if that fails.
740 If C<$socketpath> is not specified, C<get_socket_path()> will be called.
742 You only need to use this function if you have launched i3 on your own with
743 C<launch_with_config>. Otherwise, it will be automatically called when the
746 use i3test i3_autostart => 0;
747 my $pid = launch_with_config($config);
749 exit_gracefully($pid);
752 sub exit_gracefully {
753 my ($pid, $socketpath) = @_;
754 $socketpath ||= get_socket_path();
758 say "Exiting i3 cleanly...";
759 i3($socketpath)->command('exit')->recv;
765 or $tester->BAIL_OUT("could not kill i3");
768 if ($socketpath =~ m,^/tmp/i3-test-socket-,) {
776 =head2 get_socket_path([ $cache ])
778 Gets the socket path from the C<I3_SOCKET_PATH> atom stored on the X11 root
779 window. After the first call, this function will return a cached version of the
780 socket path unless you specify a false value for C<$cache>.
782 my $i3 = i3(get_socket_path());
783 $i3->command('nop test example')->recv;
787 my $i3 = i3(get_socket_path(0));
790 sub get_socket_path {
794 if ($cache && defined($_cached_socket_path)) {
795 return $_cached_socket_path;
797 my $socketpath = i3test::Util::get_socket_path($x);
798 $_cached_socket_path = $socketpath;
802 =head2 launch_with_config($config, [ $args ])
804 Launches a new i3 process with C<$config> as configuration file. Useful for
805 tests which test specific config file directives.
807 use i3test i3_autostart => 0;
810 # i3 config file (v4)
811 for_window [class="borderless"] border none
812 for_window [title="special borderless title"] border none
815 my $pid = launch_with_config($config);
819 exit_gracefully($pid);
822 sub launch_with_config {
823 my ($config, %args) = @_;
825 $tmp_socket_path = "/tmp/nested-$ENV{DISPLAY}";
827 $args{dont_create_temp_dir} //= 0;
828 $args{validate_config} //= 0;
830 my ($fh, $tmpfile) = tempfile("i3-cfg-for-$ENV{TESTNAME}-XXXXX", UNLINK => 1);
832 say $fh "ipc-socket $tmp_socket_path"
833 unless $args{dont_add_socket_path};
835 if ($config ne '-default') {
838 open(my $conf_fh, '<', '@abs_top_srcdir@/testcases/i3-test.config')
839 or $tester->BAIL_OUT("could not open default config: $!");
841 say $fh scalar <$conf_fh>;
846 my $cv = AnyEvent->condvar;
847 $i3_pid = activate_i3(
848 unix_socket_path => "$tmp_socket_path-activation",
849 display => $ENV{DISPLAY},
850 configfile => $tmpfile,
851 outdir => $ENV{OUTDIR},
852 testname => $ENV{TESTNAME},
853 valgrind => $ENV{VALGRIND},
854 strace => $ENV{STRACE},
855 xtrace => $ENV{XTRACE},
856 restart => $ENV{RESTART},
858 dont_create_temp_dir => $args{dont_create_temp_dir},
859 validate_config => $args{validate_config},
860 inject_randr15 => $args{inject_randr15},
861 inject_randr15_outputinfo => $args{inject_randr15_outputinfo},
864 # If we called i3 with -C, we wait for it to exit and then return as
865 # there's nothing else we need to do.
866 if ($args{validate_config}) {
870 # We need this since exit_gracefully will not be called in this case.
873 return ${^CHILD_ERROR_NATIVE};
876 # force update of the cached socket path in lib/i3test
877 # as soon as i3 has started
878 $cv->cb(sub { get_socket_path(0) });
880 return $cv if $args{dont_block};
882 # blockingly wait until i3 is ready
890 Returns the content of the log file for the current test.
894 my $logfile = "$ENV{OUTDIR}/i3-log-for-$ENV{TESTNAME}";
895 return slurp($logfile);
898 =head2 kill_all_windows
900 Kills all windows to clean up between tests.
903 sub kill_all_windows {
904 # Sync in case not all windows are managed by i3 just yet.
906 cmd '[title=".*"] kill';
911 Michael Stapelberg <michael@i3wm.org>
916 use parent 'X11::XCB::Connection';
920 i3test::sync_with_i3();
922 return $self->SUPER::input_focus(@_);