]> git.sur5r.net Git - i3/i3/blob - testcases/lib/i3test.pm.in
Introduce cmp_tree test function
[i3/i3] / testcases / lib / i3test.pm.in
1 package i3test;
2 # vim:ts=4:sw=4:expandtab
3 use strict; use warnings;
4
5 use File::Temp qw(tmpnam tempfile tempdir);
6 use Test::Builder;
7 use X11::XCB::Rect;
8 use X11::XCB::Window;
9 use X11::XCB qw(:all);
10 use lib qw(@abs_top_srcdir@/AnyEvent-I3/blib/lib);
11 use AnyEvent::I3;
12 use List::Util qw(first);
13 use Time::HiRes qw(sleep);
14 use Cwd qw(abs_path);
15 use POSIX ':sys_wait_h';
16 use Scalar::Util qw(blessed);
17 use SocketActivation;
18 use i3test::Util qw(slurp);
19
20 use v5.10;
21
22 # preload
23 use Test::More ();
24 use Data::Dumper ();
25
26 use Exporter ();
27 our @EXPORT = qw(
28     get_workspace_names
29     get_output_for_workspace
30     get_unused_workspace
31     fresh_workspace
32     get_ws_content
33     get_ws
34     get_focused
35     open_empty_con
36     open_window
37     open_floating_window
38     get_dock_clients
39     cmd
40     sync_with_i3
41     exit_gracefully
42     exit_forcefully
43     workspace_exists
44     focused_ws
45     get_socket_path
46     launch_with_config
47     get_i3_log
48     wait_for_event
49     wait_for_map
50     wait_for_unmap
51     $x
52     kill_all_windows
53     events_for
54     listen_for_binding
55     is_net_wm_state_focused
56     cmp_tree
57 );
58
59 =head1 NAME
60
61 i3test - Testcase setup module
62
63 =encoding utf-8
64
65 =head1 SYNOPSIS
66
67   use i3test;
68
69   my $ws = fresh_workspace;
70   is_num_children($ws, 0, 'no containers on this workspace yet');
71   cmd 'open';
72   is_num_children($ws, 1, 'one container after "open"');
73
74   done_testing;
75
76 =head1 DESCRIPTION
77
78 This module is used in every i3 testcase and takes care of automatically
79 starting i3 before any test instructions run. It also saves you typing of lots
80 of boilerplate in every test file.
81
82
83 i3test automatically "use"s C<Test::More>, C<Data::Dumper>, C<AnyEvent::I3>,
84 C<Time::HiRes>’s C<sleep> and C<i3test::Test> so that all of them are available
85 to you in your testcase.
86
87 See also C<i3test::Test> (L<https://build.i3wm.org/docs/lib-i3test-test.html>)
88 which provides additional test instructions (like C<ok> or C<is>).
89
90 =cut
91
92 my $tester = Test::Builder->new();
93 my $_cached_socket_path = undef;
94 my $_sync_window = undef;
95 my $tmp_socket_path = undef;
96
97 our $x;
98
99 BEGIN {
100     my $window_count = 0;
101     sub counter_window {
102         return $window_count++;
103     }
104 }
105
106 my $i3_pid;
107 my $i3_autostart;
108
109 END {
110     # Skip the remaining cleanup for testcases which set i3_autostart => 0:
111     return if !defined($i3_pid) && !$i3_autostart;
112
113     # don't trigger SIGCHLD handler
114     local $SIG{CHLD};
115
116     # From perldoc -v '$?':
117     # Inside an "END" subroutine $? contains the value
118     # that is going to be given to "exit()".
119     #
120     # Since waitpid sets $?, we need to localize it,
121     # otherwise TAP would be misinterpreted our return status
122     local $?;
123
124     # When measuring code coverage, try to exit i3 cleanly (otherwise, .gcda
125     # files are not written)
126     if ($ENV{COVERAGE} || $ENV{VALGRIND}) {
127         exit_gracefully($i3_pid, "/tmp/nested-$ENV{DISPLAY}");
128
129     } else {
130         kill(-9, $i3_pid)
131             or $tester->BAIL_OUT("could not kill i3: $!");
132
133         waitpid $i3_pid, 0;
134     }
135 }
136
137 sub import {
138     my ($class, %args) = @_;
139     my $pkg = caller;
140
141     $x ||= i3test::X11->new;
142     # set the pointer to a predictable position in case a previous test has
143     # disturbed it
144     $x->warp_pointer(
145         0, # src_window (None)
146         $x->get_root_window(), # dst_window (None)
147         0, # src_x
148         0, # src_y
149         0, # src_width
150         0, # src_height
151         0, # dst_x
152         0); # dst_y
153     # Synchronize with X11 to ensure the pointer has been warped before i3
154     # starts up.
155     $x->get_input_focus_reply($x->get_input_focus()->{sequence});
156
157     $i3_autostart = delete($args{i3_autostart}) // 1;
158     my $i3_config = delete($args{i3_config}) // '-default';
159
160     my $cv = launch_with_config($i3_config, dont_block => 1)
161         if $i3_autostart;
162
163     my $test_more_args = '';
164     $test_more_args = join(' ', 'qw(', %args, ')') if keys %args;
165     local $@;
166     eval << "__";
167 package $pkg;
168 use Test::More $test_more_args;
169 use Data::Dumper;
170 use AnyEvent::I3;
171 use Time::HiRes qw(sleep);
172 use i3test::Test;
173 __
174     $tester->BAIL_OUT("$@") if $@;
175     feature->import(":5.10");
176     strict->import;
177     warnings->import;
178
179     $cv->recv if $i3_autostart;
180
181     @_ = ($class);
182     goto \&Exporter::import;
183 }
184
185 =head1 EXPORT
186
187 =head2 wait_for_event($timeout, $callback)
188
189 Waits for the next event and calls the given callback for every event to
190 determine if this is the event we are waiting for.
191
192 Can be used to wait until a window is mapped, until a ClientMessage is
193 received, etc.
194
195   wait_for_event 0.25, sub { $_[0]->{response_type} == MAP_NOTIFY };
196
197 =cut
198 sub wait_for_event {
199     my ($timeout, $cb) = @_;
200
201     $x->flush;
202
203     while (defined(my $event = $x->wait_for_event)) {
204         return 1 if $cb->($event);
205     }
206 }
207
208 =head2 wait_for_map($window)
209
210 Thin wrapper around wait_for_event which waits for MAP_NOTIFY.
211 Make sure to include 'structure_notify' in the window’s event_mask attribute.
212
213 This function is called by C<open_window>, so in most cases, you don’t need to
214 call it on your own. If you need special setup of the window before mapping,
215 you might have to map it on your own and use this function:
216
217   my $window = open_window(dont_map => 1);
218   # Do something special with the window first
219   # …
220
221   # Now map it and wait until it’s been mapped
222   $window->map;
223   wait_for_map($window);
224
225 =cut
226 sub wait_for_map {
227     my ($win) = @_;
228     my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win;
229     wait_for_event 4, sub {
230         $_[0]->{response_type} == MAP_NOTIFY and $_[0]->{window} == $id
231     };
232 }
233
234 =head2 wait_for_unmap($window)
235
236 Wrapper around C<wait_for_event> which waits for UNMAP_NOTIFY. Also calls
237 C<sync_with_i3> to make sure i3 also picked up and processed the UnmapNotify
238 event.
239
240   my $ws = fresh_workspace;
241   my $window = open_window;
242   is_num_children($ws, 1, 'one window on workspace');
243   $window->unmap;
244   wait_for_unmap;
245   is_num_children($ws, 0, 'no more windows on this workspace');
246
247 =cut
248 sub wait_for_unmap {
249     my ($win) = @_;
250     # my $id = (blessed($win) && $win->isa('X11::XCB::Window')) ? $win->id : $win;
251     wait_for_event 4, sub {
252         $_[0]->{response_type} == UNMAP_NOTIFY # and $_[0]->{window} == $id
253     };
254     sync_with_i3();
255 }
256
257 =head2 open_window([ $args ])
258
259 Opens a new window (see C<X11::XCB::Window>), maps it, waits until it got mapped
260 and synchronizes with i3.
261
262 The following arguments can be passed:
263
264 =over 4
265
266 =item class
267
268 The X11 window class (e.g. WINDOW_CLASS_INPUT_OUTPUT), not to be confused with
269 the WM_CLASS!
270
271 =item rect
272
273 An arrayref with 4 members specifying the initial geometry (position and size)
274 of the window, e.g. C<< [ 0, 100, 70, 50 ] >> for a window appearing at x=0, y=100
275 with width=70 and height=50.
276
277 Note that this is entirely irrelevant for tiling windows.
278
279 =item background_color
280
281 The background pixel color of the window, formatted as "#rrggbb", like HTML
282 color codes (e.g. #c0c0c0). This is useful to tell windows apart when actually
283 watching the testcases.
284
285 =item event_mask
286
287 An arrayref containing strings which describe the X11 event mask we use for that
288 window. The default is C<< [ 'structure_notify' ] >>.
289
290 =item name
291
292 The window’s C<_NET_WM_NAME> (UTF-8 window title). By default, this is "Window
293 n" with n being replaced by a counter to keep windows apart.
294
295 =item dont_map
296
297 Set to a true value to avoid mapping the window (making it visible).
298
299 =item before_map
300
301 A coderef which is called before the window is mapped (unless C<dont_map> is
302 true). The freshly created C<$window> is passed as C<$_> and as the first
303 argument.
304
305 =back
306
307 The default values are equivalent to this call:
308
309   open_window(
310     class => WINDOW_CLASS_INPUT_OUTPUT
311     rect => [ 0, 0, 30, 30 ]
312     background_color => '#c0c0c0'
313     event_mask => [ 'structure_notify' ]
314     name => 'Window <n>'
315   );
316
317 Usually, though, calls are simpler:
318
319   my $top_window = open_window;
320
321 To identify the resulting window object in i3 commands, use the id property:
322
323   my $top_window = open_window;
324   cmd '[id="' . $top_window->id . '"] kill';
325
326 =cut
327 sub open_window {
328     my %args = @_ == 1 ? %{$_[0]} : @_;
329
330     my $dont_map = delete $args{dont_map};
331     my $before_map = delete $args{before_map};
332
333     $args{class} //= WINDOW_CLASS_INPUT_OUTPUT;
334     $args{rect} //= [ 0, 0, 30, 30 ];
335     $args{background_color} //= '#c0c0c0';
336     $args{event_mask} //= [ 'structure_notify' ];
337     $args{name} //= 'Window ' . counter_window();
338
339     my $window = $x->root->create_child(%args);
340     $window->add_hint('input');
341
342     if ($before_map) {
343         # TODO: investigate why _create is not needed
344         $window->_create;
345         $before_map->($window) for $window;
346     }
347
348     return $window if $dont_map;
349
350     $window->map;
351     wait_for_map($window);
352
353     # MapWindow is sent before i3 even starts rendering: the window is placed at
354     # temporary off-screen coordinates first, and x_push_changes() sends further
355     # X11 requests to set focus etc. Hence, we sync with i3 before continuing.
356     sync_with_i3();
357
358     return $window;
359 }
360
361 =head2 open_floating_window([ $args ])
362
363 Thin wrapper around open_window which sets window_type to
364 C<_NET_WM_WINDOW_TYPE_UTILITY> to make the window floating.
365
366 The arguments are the same as those of C<open_window>.
367
368 =cut
369 sub open_floating_window {
370     my %args = @_ == 1 ? %{$_[0]} : @_;
371
372     $args{window_type} = $x->atom(name => '_NET_WM_WINDOW_TYPE_UTILITY');
373
374     return open_window(\%args);
375 }
376
377 sub open_empty_con {
378     my ($i3) = @_;
379
380     my $reply = $i3->command('open')->recv;
381     return $reply->[0]->{id};
382 }
383
384 =head2 get_workspace_names()
385
386 Returns an arrayref containing the name of every workspace (regardless of its
387 output) which currently exists.
388
389   my $workspace_names = get_workspace_names;
390   is(scalar @$workspace_names, 3, 'three workspaces exist currently');
391
392 =cut
393 sub get_workspace_names {
394     my $i3 = i3(get_socket_path());
395     my $tree = $i3->get_tree->recv;
396     my @outputs = @{$tree->{nodes}};
397     my @cons;
398     for my $output (@outputs) {
399         next if $output->{name} eq '__i3';
400         # get the first CT_CON of each output
401         my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
402         @cons = (@cons, @{$content->{nodes}});
403     }
404     [ map { $_->{name} } @cons ]
405 }
406
407 =head2 get_output_for_workspace()
408
409 Returns the name of the output on which this workspace resides
410
411   cmd 'focus output fake-1';
412   cmd 'workspace 1';
413   is(get_output_for_workspace('1', 'fake-0', 'Workspace 1 in output fake-0');
414
415 =cut
416 sub get_output_for_workspace {
417     my $ws_name = shift @_;
418     my $i3 = i3(get_socket_path());
419     my $tree = $i3->get_tree->recv;
420     my @outputs = @{$tree->{nodes}};
421
422     foreach (grep { not $_->{name} =~ /^__/ } @outputs) {
423         my $output = $_->{name};
424         foreach (grep { $_->{name} =~ "content" } @{$_->{nodes}}) {
425             return $output if $_->{nodes}[0]->{name} =~ $ws_name;
426         }
427     }
428 }
429
430 =head2 get_unused_workspace
431
432 Returns a workspace name which has not yet been used. See also
433 C<fresh_workspace> which directly switches to an unused workspace.
434
435   my $ws = get_unused_workspace;
436   cmd "workspace $ws";
437
438 =cut
439 sub get_unused_workspace {
440     my @names = get_workspace_names();
441     my $tmp;
442     do { $tmp = tmpnam() } while ((scalar grep { $_ eq $tmp } @names) > 0);
443     $tmp
444 }
445
446 =head2 fresh_workspace([ $args ])
447
448 Switches to an unused workspace and returns the name of that workspace.
449
450 Optionally switches to the specified output first.
451
452     my $ws = fresh_workspace;
453
454     # Get a fresh workspace on the second output.
455     my $ws = fresh_workspace(output => 1);
456
457 =cut
458 sub fresh_workspace {
459     my %args = @_;
460     if (exists($args{output})) {
461         my $i3 = i3(get_socket_path());
462         my $tree = $i3->get_tree->recv;
463         my $output = first { $_->{name} eq "fake-$args{output}" }
464                         @{$tree->{nodes}};
465         die "BUG: Could not find output $args{output}" unless defined($output);
466         # Get the focused workspace on that output and switch to it.
467         my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
468         my $focused = $content->{focus}->[0];
469         my $workspace = first { $_->{id} == $focused } @{$content->{nodes}};
470         $workspace = $workspace->{name};
471         cmd("workspace $workspace");
472     }
473
474     my $unused = get_unused_workspace;
475     cmd("workspace $unused");
476     $unused
477 }
478
479 =head2 get_ws($workspace)
480
481 Returns the container (from the i3 layout tree) which represents C<$workspace>.
482
483   my $ws = fresh_workspace;
484   my $ws_con = get_ws($ws);
485   ok(!$ws_con->{urgent}, 'fresh workspace not marked urgent');
486
487 Here is an example which counts the number of urgent containers recursively,
488 starting from the workspace container:
489
490   sub count_urgent {
491       my ($con) = @_;
492
493       my @children = (@{$con->{nodes}}, @{$con->{floating_nodes}});
494       my $urgent = grep { $_->{urgent} } @children;
495       $urgent += count_urgent($_) for @children;
496       return $urgent;
497   }
498   my $urgent = count_urgent(get_ws($ws));
499   is($urgent, 3, "three urgent windows on workspace $ws");
500
501
502 =cut
503 sub get_ws {
504     my ($name) = @_;
505     my $i3 = i3(get_socket_path());
506     my $tree = $i3->get_tree->recv;
507
508     my @outputs = @{$tree->{nodes}};
509     my @workspaces;
510     for my $output (@outputs) {
511         # get the first CT_CON of each output
512         my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
513         @workspaces = (@workspaces, @{$content->{nodes}});
514     }
515
516     # as there can only be one workspace with this name, we can safely
517     # return the first entry
518     return first { $_->{name} eq $name } @workspaces;
519 }
520
521 =head2 get_ws_content($workspace)
522
523 Returns the content (== tree, starting from the node of a workspace)
524 of a workspace. If called in array context, also includes the focus
525 stack of the workspace.
526
527   my $nodes = get_ws_content($ws);
528   is(scalar @$nodes, 4, 'there are four containers at workspace-level');
529
530 Or, in array context:
531
532   my $window = open_window;
533   my ($nodes, $focus) = get_ws_content($ws);
534   is($focus->[0], $window->id, 'newly opened window focused');
535
536 Note that this function does not do recursion for you! It only returns the
537 containers B<on workspace level>. If you want to work with all containers (even
538 nested ones) on a workspace, you have to use recursion:
539
540   # NB: This function does not count floating windows
541   sub count_urgent {
542       my ($nodes) = @_;
543
544       my $urgent = 0;
545       for my $con (@$nodes) {
546           $urgent++ if $con->{urgent};
547           $urgent += count_urgent($con->{nodes});
548       }
549
550       return $urgent;
551   }
552   my $nodes = get_ws_content($ws);
553   my $urgent = count_urgent($nodes);
554   is($urgent, 3, "three urgent windows on workspace $ws");
555
556 If you also want to deal with floating windows, you have to use C<get_ws>
557 instead and access C<< ->{nodes} >> and C<< ->{floating_nodes} >> on your own.
558
559 =cut
560 sub get_ws_content {
561     my ($name) = @_;
562     my $con = get_ws($name);
563     return wantarray ? ($con->{nodes}, $con->{focus}) : $con->{nodes};
564 }
565
566 =head2 get_focused($workspace)
567
568 Returns the container ID of the currently focused container on C<$workspace>.
569
570 Note that the container ID is B<not> the X11 window ID, so comparing the result
571 of C<get_focused> with a window's C<< ->{id} >> property does B<not> work.
572
573   my $ws = fresh_workspace;
574   my $first_window = open_window;
575   my $first_id = get_focused();
576
577   my $second_window = open_window;
578   my $second_id = get_focused();
579
580   cmd 'focus left';
581
582   is(get_focused($ws), $first_id, 'second window focused');
583
584 =cut
585 sub get_focused {
586     my ($ws) = @_;
587     my $con = get_ws($ws);
588
589     my @focused = @{$con->{focus}};
590     my $lf;
591     while (@focused > 0) {
592         $lf = $focused[0];
593         last unless defined($con->{focus});
594         @focused = @{$con->{focus}};
595         my @cons = grep { $_->{id} == $lf } (@{$con->{nodes}}, @{$con->{'floating_nodes'}});
596         $con = $cons[0];
597     }
598
599     return $lf;
600 }
601
602 =head2 get_dock_clients([ $dockarea ])
603
604 Returns an array of all dock containers in C<$dockarea> (one of "top" or
605 "bottom"). If C<$dockarea> is not specified, returns an array of all dock
606 containers in any dockarea.
607
608   my @docked = get_dock_clients;
609   is(scalar @docked, 0, 'no dock clients yet');
610
611 =cut
612 sub get_dock_clients {
613     my $which = shift;
614
615     my $tree = i3(get_socket_path())->get_tree->recv;
616     my @outputs = @{$tree->{nodes}};
617     # Children of all dockareas
618     my @docked;
619     for my $output (@outputs) {
620         if (!defined($which)) {
621             @docked = (@docked, map { @{$_->{nodes}} }
622                                 grep { $_->{type} eq 'dockarea' }
623                                 @{$output->{nodes}});
624         } elsif ($which eq 'top') {
625             my $first = first { $_->{type} eq 'dockarea' } @{$output->{nodes}};
626             @docked = (@docked, @{$first->{nodes}}) if defined($first);
627         } elsif ($which eq 'bottom') {
628             my @matching = grep { $_->{type} eq 'dockarea' } @{$output->{nodes}};
629             my $last = $matching[-1];
630             @docked = (@docked, @{$last->{nodes}}) if defined($last);
631         }
632     }
633     return @docked;
634 }
635
636 =head2 cmd($command)
637
638 Sends the specified command to i3 and returns the output.
639
640   my $ws = unused_workspace;
641   cmd "workspace $ws";
642   cmd 'focus right';
643
644 =cut
645 sub cmd {
646     i3(get_socket_path())->command(@_)->recv
647 }
648
649 =head2 workspace_exists($workspace)
650
651 Returns true if C<$workspace> is the name of an existing workspace.
652
653   my $old_ws = focused_ws;
654   # switch away from where we currently are
655   fresh_workspace;
656
657   ok(workspace_exists($old_ws), 'old workspace still exists');
658
659 =cut
660 sub workspace_exists {
661     my ($name) = @_;
662     (scalar grep { $_ eq $name } @{get_workspace_names()}) > 0;
663 }
664
665 =head2 focused_ws
666
667 Returns the name of the currently focused workspace.
668
669   my $ws = focused_ws;
670   is($ws, '1', 'i3 starts on workspace 1');
671
672 =cut
673 sub focused_ws {
674     my $i3 = i3(get_socket_path());
675     my $tree = $i3->get_tree->recv;
676     my $focused = $tree->{focus}->[0];
677     my $output = first { $_->{id} == $focused } @{$tree->{nodes}};
678     my $content = first { $_->{type} eq 'con' } @{$output->{nodes}};
679     my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}};
680     return $first->{name}
681 }
682
683 =head2 sync_with_i3([ $args ])
684
685 Sends an I3_SYNC ClientMessage with a random value to the root window.
686 i3 will reply with the same value, but, due to the order of events it
687 processes, only after all other events are done.
688
689 This can be used to ensure the results of a cmd 'focus left' are pushed to
690 X11 and that C<< $x->input_focus >> returns the correct value afterwards.
691
692 See also L<https://build.i3wm.org/docs/testsuite.html> for a longer explanation.
693
694   my $window = open_window;
695   $window->add_hint('urgency');
696   # Ensure i3 picked up the change
697   sync_with_i3;
698
699 The only time when you need to use the C<no_cache> argument is when you just
700 killed your own X11 connection:
701
702   cmd 'kill client';
703   # We need to re-establish the X11 connection which we just killed :).
704   $x = i3test::X11->new;
705   sync_with_i3(no_cache => 1);
706
707 =cut
708 sub sync_with_i3 {
709     my %args = @_ == 1 ? %{$_[0]} : @_;
710
711     # Since we need a (mapped) window for receiving a ClientMessage, we create
712     # one on the first call of sync_with_i3. It will be re-used in all
713     # subsequent calls.
714     if (!exists($args{window_id}) &&
715         (!defined($_sync_window) || exists($args{no_cache}))) {
716         $_sync_window = open_window(
717             rect => [ -15, -15, 10, 10 ],
718             override_redirect => 1,
719             dont_map => 1,
720         );
721     }
722
723     my $window_id = delete $args{window_id};
724     $window_id //= $_sync_window->id;
725
726     my $root = $x->get_root_window();
727     # Generate a random number to identify this particular ClientMessage.
728     my $myrnd = int(rand(255)) + 1;
729
730     # Generate a ClientMessage, see xcb_client_message_t
731     my $msg = pack "CCSLLLLLLL",
732          CLIENT_MESSAGE, # response_type
733          32,     # format
734          0,      # sequence
735          $root,  # destination window
736          $x->atom(name => 'I3_SYNC')->id,
737
738          $window_id,    # data[0]: our own window id
739          $myrnd, # data[1]: a random value to identify the request
740          0,
741          0,
742          0;
743
744     # Send it to the root window -- since i3 uses the SubstructureRedirect
745     # event mask, it will get the ClientMessage.
746     $x->send_event(0, $root, EVENT_MASK_SUBSTRUCTURE_REDIRECT, $msg);
747
748     return $myrnd if $args{dont_wait_for_event};
749
750     # now wait until the reply is here
751     return wait_for_event 4, sub {
752         my ($event) = @_;
753         # TODO: const
754         return 0 unless $event->{response_type} == 161;
755
756         my ($win, $rnd) = unpack "LL", $event->{data};
757         return ($rnd == $myrnd);
758     };
759 }
760
761 =head2 exit_gracefully($pid, [ $socketpath ])
762
763 Tries to exit i3 gracefully (with the 'exit' cmd) or kills the PID if that fails.
764
765 If C<$socketpath> is not specified, C<get_socket_path()> will be called.
766
767 You only need to use this function if you have launched i3 on your own with
768 C<launch_with_config>. Otherwise, it will be automatically called when the
769 testcase ends.
770
771   use i3test i3_autostart => 0;
772   my $pid = launch_with_config($config);
773   # …
774   exit_gracefully($pid);
775
776 =cut
777 sub exit_gracefully {
778     my ($pid, $socketpath) = @_;
779     $socketpath ||= get_socket_path();
780
781     my $exited = 0;
782     eval {
783         say "Exiting i3 cleanly...";
784         i3($socketpath)->command('exit')->recv;
785         $exited = 1;
786     };
787
788     if (!$exited) {
789         kill(9, $pid)
790             or $tester->BAIL_OUT("could not kill i3: $!");
791     }
792
793     if ($socketpath =~ m,^/tmp/i3-test-socket-,) {
794         unlink($socketpath);
795     }
796
797     waitpid $pid, 0;
798     undef $i3_pid;
799 }
800
801 =head2 exit_forcefully($pid, [ $signal ])
802
803 Tries to exit i3 forcefully by sending a signal (defaults to SIGTERM).
804
805 You only need to use this function if you want to test signal handling
806 (in which case you must have launched i3 on your own with
807 C<launch_with_config>).
808
809   use i3test i3_autostart => 0;
810   my $pid = launch_with_config($config);
811   # …
812   exit_forcefully($pid);
813
814 =cut
815 sub exit_forcefully {
816     my ($pid, $signal) = @_;
817     $signal ||= 'TERM';
818
819     # Send the given signal to the i3 instance and wait for up to 10s
820     # for it to terminate.
821     kill($signal, $pid)
822         or $tester->BAIL_OUT("could not kill i3: $!");
823     my $status;
824     my $timeout = 10;
825     do {
826         $status = waitpid $pid, WNOHANG;
827
828         if ($status <= 0) {
829             sleep(1);
830             $timeout--;
831         }
832     } while ($status <= 0 && $timeout > 0);
833
834     if ($status <= 0) {
835         kill('KILL', $pid)
836             or $tester->BAIL_OUT("could not kill i3: $!");
837         waitpid $pid, 0;
838     }
839     undef $i3_pid;
840 }
841
842 =head2 get_socket_path([ $cache ])
843
844 Gets the socket path from the C<I3_SOCKET_PATH> atom stored on the X11 root
845 window. After the first call, this function will return a cached version of the
846 socket path unless you specify a false value for C<$cache>.
847
848   my $i3 = i3(get_socket_path());
849   $i3->command('nop test example')->recv;
850
851 To avoid caching:
852
853   my $i3 = i3(get_socket_path(0));
854
855 =cut
856 sub get_socket_path {
857     my ($cache) = @_;
858     $cache //= 1;
859
860     if ($cache && defined($_cached_socket_path)) {
861         return $_cached_socket_path;
862     }
863     my $socketpath = i3test::Util::get_socket_path($x);
864     $_cached_socket_path = $socketpath;
865     return $socketpath;
866 }
867
868 =head2 launch_with_config($config, [ $args ])
869
870 Launches a new i3 process with C<$config> as configuration file. Useful for
871 tests which test specific config file directives.
872
873   use i3test i3_autostart => 0;
874
875   my $config = <<EOT;
876   # i3 config file (v4)
877   for_window [class="borderless"] border none
878   for_window [title="special borderless title"] border none
879   EOT
880
881   my $pid = launch_with_config($config);
882
883   # …
884
885   exit_gracefully($pid);
886
887 =cut
888 sub launch_with_config {
889     my ($config, %args) = @_;
890
891     $tmp_socket_path = "/tmp/nested-$ENV{DISPLAY}";
892
893     $args{dont_create_temp_dir} //= 0;
894     $args{validate_config} //= 0;
895
896     my ($fh, $tmpfile) = tempfile("i3-cfg-for-$ENV{TESTNAME}-XXXXX", UNLINK => 1);
897
898     say $fh "ipc-socket $tmp_socket_path"
899         unless $args{dont_add_socket_path};
900
901     if ($config ne '-default') {
902         print $fh $config;
903     } else {
904         open(my $conf_fh, '<', '@abs_top_srcdir@/testcases/i3-test.config')
905             or $tester->BAIL_OUT("could not open default config: $!");
906         local $/;
907         say $fh scalar <$conf_fh>;
908     }
909
910     close($fh);
911
912     my $cv = AnyEvent->condvar;
913     $i3_pid = activate_i3(
914         unix_socket_path => "$tmp_socket_path-activation",
915         display => $ENV{DISPLAY},
916         configfile => $tmpfile,
917         outdir => $ENV{OUTDIR},
918         testname => $ENV{TESTNAME},
919         valgrind => $ENV{VALGRIND},
920         strace => $ENV{STRACE},
921         xtrace => $ENV{XTRACE},
922         restart => $ENV{RESTART},
923         cv => $cv,
924         dont_create_temp_dir => $args{dont_create_temp_dir},
925         validate_config => $args{validate_config},
926         inject_randr15 => $args{inject_randr15},
927         inject_randr15_outputinfo => $args{inject_randr15_outputinfo},
928     );
929
930     # If we called i3 with -C, we wait for it to exit and then return as
931     # there's nothing else we need to do.
932     if ($args{validate_config}) {
933         $cv->recv;
934         waitpid $i3_pid, 0;
935
936         # We need this since exit_gracefully will not be called in this case.
937         undef $i3_pid;
938
939         return ${^CHILD_ERROR_NATIVE};
940     }
941
942     # force update of the cached socket path in lib/i3test
943     # as soon as i3 has started
944     $cv->cb(sub { get_socket_path(0) });
945
946     return $cv if $args{dont_block};
947
948     # blockingly wait until i3 is ready
949     $cv->recv;
950
951     return $i3_pid;
952 }
953
954 =head2 get_i3_log
955
956 Returns the content of the log file for the current test.
957
958 =cut
959 sub get_i3_log {
960     my $logfile = "$ENV{OUTDIR}/i3-log-for-$ENV{TESTNAME}";
961     return slurp($logfile);
962 }
963
964 =head2 kill_all_windows
965
966 Kills all windows to clean up between tests.
967
968 =cut
969 sub kill_all_windows {
970     # Sync in case not all windows are managed by i3 just yet.
971     sync_with_i3;
972     cmd '[title=".*"] kill';
973 }
974
975 =head2 events_for($subscribecb, [ $rettype ], [ $eventcbs ])
976
977 Helper function which returns an array containing all events of type $rettype
978 which were generated by i3 while $subscribecb was running.
979
980 Set $eventcbs to subscribe to multiple event types and/or perform your own event
981 aggregation.
982
983 =cut
984 sub events_for {
985     my ($subscribecb, $rettype, $eventcbs) = @_;
986
987     my @events;
988     $eventcbs //= {};
989     if (defined($rettype)) {
990         $eventcbs->{$rettype} = sub { push @events, shift };
991     }
992     my $subscribed = AnyEvent->condvar;
993     my $flushed = AnyEvent->condvar;
994     $eventcbs->{tick} = sub {
995         my ($event) = @_;
996         if ($event->{first}) {
997             $subscribed->send($event);
998         } else {
999             $flushed->send($event);
1000         }
1001     };
1002     my $i3 = i3(get_socket_path(0));
1003     $i3->connect->recv;
1004     $i3->subscribe($eventcbs)->recv;
1005     $subscribed->recv;
1006     # Subscription established, run the callback.
1007     $subscribecb->();
1008     # Now generate a tick event, which we know we’ll receive (and at which point
1009     # all other events have been received).
1010     my $nonce = int(rand(255)) + 1;
1011     $i3->send_tick($nonce);
1012
1013     my $tick = $flushed->recv;
1014     $tester->is_eq($tick->{payload}, $nonce, 'tick nonce received');
1015     return @events;
1016 }
1017
1018 =head2 listen_for_binding($cb)
1019
1020 Helper function to evaluate whether sending KeyPress/KeyRelease events via XTEST
1021 triggers an i3 key binding or not. Expects key bindings to be configured in the
1022 form “bindsym <binding> nop <binding>”, e.g.  “bindsym Mod4+Return nop
1023 Mod4+Return”.
1024
1025   is(listen_for_binding(
1026       sub {
1027           xtest_key_press(133); # Super_L
1028           xtest_key_press(36); # Return
1029           xtest_key_release(36); # Return
1030           xtest_key_release(133); # Super_L
1031           xtest_sync_with_i3;
1032       },
1033       ),
1034      'Mod4+Return',
1035      'triggered the "Mod4+Return" keybinding');
1036
1037 =cut
1038
1039 sub listen_for_binding {
1040     my ($cb) = @_;
1041     my $triggered = AnyEvent->condvar;
1042     my @events = events_for(
1043         $cb,
1044         'binding');
1045
1046     $tester->is_eq(scalar @events, 1, 'Received precisely one event');
1047     $tester->is_eq($events[0]->{change}, 'run', 'change is "run"');
1048     # We look at the command (which is “nop <binding>”) because that is easier
1049     # than re-assembling the string representation of $event->{binding}.
1050     my $command = $events[0]->{binding}->{command};
1051     $command =~ s/^nop //g;
1052     return $command;
1053 }
1054
1055 =head2 is_net_wm_state_focused
1056
1057 Returns true if the given window has the _NET_WM_STATE_FOCUSED atom.
1058
1059     ok(is_net_wm_state_focused($window), '_NET_WM_STATE_FOCUSED set');
1060
1061 =cut
1062 sub is_net_wm_state_focused {
1063     my ($window) = @_;
1064
1065     sync_with_i3;
1066     my $atom = $x->atom(name => '_NET_WM_STATE_FOCUSED');
1067     my $cookie = $x->get_property(
1068         0,
1069         $window->{id},
1070         $x->atom(name => '_NET_WM_STATE')->id,
1071         GET_PROPERTY_TYPE_ANY,
1072         0,
1073         4096
1074     );
1075
1076     my $reply = $x->get_property_reply($cookie->{sequence});
1077     my $len = $reply->{length};
1078     return 0 if $len == 0;
1079
1080     my @atoms = unpack("L$len", $reply->{value});
1081     for (my $i = 0; $i < $len; $i++) {
1082         return 1 if $atoms[$i] == $atom->id;
1083     }
1084
1085     return 0;
1086 }
1087
1088 =head2 cmp_tree([ $args ])
1089
1090 Compares the tree layout before and after an operation inside a subtest.
1091
1092 The following arguments can be passed:
1093
1094 =over 4
1095
1096 =item layout_before
1097
1098 Required argument. The initial layout to be created. For example,
1099 'H[ V[ a* S[ b c ] d ] e ]' or 'V[a b] T[c d*]'.
1100 The layout will be converted to a JSON file which will be passed to i3's
1101 append_layout command.
1102
1103 The syntax's rules, assertions and limitations are:
1104
1105 =over 8
1106
1107 =item 1.
1108
1109 Upper case letters H, V, S, T mean horizontal, vertical, stacked and tabbed
1110 layout respectively. They must be followed by an opening square bracket and must
1111 be closed with a closing square bracket.
1112 Each of the non-leaf containers is marked with their corresponding letter
1113 followed by a number indicating the position of the container relative to other
1114 containers of the same type. For example, 'H[V[xxx] V[xxx] H[xxx]]' will mark
1115 the non-leaf containers as H1, V1, V2, H2.
1116
1117 =item 2.
1118
1119 Spaces are ignored.
1120
1121 =item 3.
1122
1123 Other alphanumeric characters mean a new window which uses the provided
1124 character for its class and name. Eg 'H[a b]' will open windows with classes 'a'
1125 and 'b' inside a horizontal split. Windows use a single character for their
1126 class, eg 'H[xxx]' will open 3 windows with class 'x'.
1127
1128 =item 4.
1129
1130 Asterisks after a window mean that the window must be focused after the layout
1131 is loaded. Currently, focusing non-leaf containers must be done manually, in the
1132 callback (C<cb>) function.
1133
1134 =back
1135
1136 =item cb
1137
1138 Subroutine to be called after the layout provided by C<layout_before> is created
1139 but before the resulting layout (C<layout_after>) is checked.
1140
1141 =item layout_after
1142
1143 Required argument. The final layout in which the tree is expected to be after
1144 the callback is called. Uses the same syntax with C<layout_before>.
1145 For non-leaf containers, their layout (horizontal, vertical, stacked, tabbed)
1146 is compared with the corresponding letter (H, V, S, T).
1147 For leaf containers, their name is compared with the provided alphanumeric.
1148
1149 =item ws
1150
1151 The workspace in which the layout will be created. Will switch focus to it. If
1152 not provided, a new one is created.
1153
1154 =item msg
1155
1156 Message to prepend to the subtest's name. If not empty, it will be followed by ': '.
1157
1158 =item dont_kill
1159
1160 By default, all windows are killed before the C<layout_before> layout is loaded.
1161 Set to 1 to avoid this.
1162
1163 =back
1164
1165 =cut
1166 sub cmp_tree {
1167     local $Test::Builder::Level = $Test::Builder::Level + 1;
1168
1169     my %args = @_;
1170     my $ws = $args{ws};
1171     if (defined($ws)) {
1172         cmd "workspace $ws";
1173     } else {
1174         $ws = fresh_workspace;
1175     }
1176     my $msg = '';
1177     if ($args{msg}) {
1178         $msg = $args{msg} . ': ';
1179     }
1180     die unless $args{layout_before};
1181     die unless $args{layout_after};
1182
1183     kill_all_windows unless $args{dont_kill};
1184     my @windows = create_layout($args{layout_before});
1185     Test::More::subtest $msg . $args{layout_before} . ' -> ' . $args{layout_after} => sub {
1186         $args{cb}->(\@windows) if $args{cb};
1187         verify_layout($args{layout_after}, $ws);
1188     };
1189
1190     return @windows;
1191 }
1192
1193 sub create_layout {
1194     my $layout = shift;
1195
1196     my $focus;
1197     my @windows = ();
1198     my $r = '';
1199     my $depth = 0;
1200     my %layout_counts = (H => 0, V => 0, S => 0, T => 0);
1201
1202     foreach my $char (split('', $layout)) {
1203         if ($char eq 'H') {
1204             $r = $r . '{"layout": "splith",';
1205             $r = $r . '"marks": ["H' . ++$layout_counts{H} . '"],';
1206         } elsif ($char eq 'V') {
1207             $r = $r . '{"layout": "splitv",';
1208             $r = $r . '"marks": ["V' . ++$layout_counts{V} . '"],';
1209         } elsif ($char eq 'S') {
1210             $r = $r . '{"layout": "stacked",';
1211             $r = $r . '"marks": ["S' . ++$layout_counts{S} . '"],';
1212         } elsif ($char eq 'T') {
1213             $r = $r . '{"layout": "tabbed",';
1214             $r = $r . '"marks": ["T' . ++$layout_counts{T} . '"],';
1215         } elsif ($char eq '[') {
1216             $depth++;
1217             $r = $r . '"nodes": [';
1218         } elsif ($char eq ']') {
1219             # End of nodes array: delete trailing comma.
1220             chop $r;
1221             # When we are at depth 0 we need to split using newlines, making
1222             # multiple "JSON texts".
1223             $depth--;
1224             $r = $r . ']}' . ($depth == 0 ? "\n" : ',');
1225         } elsif ($char eq ' ') {
1226         } elsif ($char eq '*') {
1227             $focus = $windows[$#windows];
1228         } elsif ($char =~ /[[:alnum:]]/) {
1229             push @windows, $char;
1230
1231             $r = $r . '{"swallows": [{';
1232             $r = $r . '"class": "^' . "$char" . '$"';
1233             $r = $r . '}]},';
1234         } else {
1235             die "Could not understand $char";
1236         }
1237     }
1238
1239     die "Invalid layout, depth is $depth > 0" unless $depth == 0;
1240
1241     Test::More::diag($r);
1242     my ($fh, $tmpfile) = tempfile("layout-XXXXXX", UNLINK => 1);
1243     print $fh "$r\n";
1244     close($fh);
1245
1246     my $return = cmd "append_layout $tmpfile";
1247     die 'Could not parse layout json file' unless $return->[0]->{success};
1248
1249     my @result_windows;
1250     push @result_windows, open_window(wm_class => "$_", name => "$_") foreach @windows;
1251     cmd '[class=' . $focus . '] focus' if $focus;
1252
1253     return @result_windows;
1254 }
1255
1256 sub verify_layout {
1257     my ($layout, $ws) = @_;
1258
1259     my $nodes = get_ws_content($ws);
1260     my %counters;
1261     my $depth = 0;
1262     my $node;
1263
1264     foreach my $char (split('', $layout)) {
1265         my $node_name;
1266         my $node_layout;
1267         if ($char eq 'H') {
1268             $node_layout = 'splith';
1269         } elsif ($char eq 'V') {
1270             $node_layout = 'splitv';
1271         } elsif ($char eq 'S') {
1272             $node_layout = 'stacked';
1273         } elsif ($char eq 'T') {
1274             $node_layout = 'tabbed';
1275         } elsif ($char eq '[') {
1276             $depth++;
1277             delete $counters{$depth};
1278         } elsif ($char eq ']') {
1279             $depth--;
1280         } elsif ($char eq ' ') {
1281         } elsif ($char eq '*') {
1282             $tester->is_eq($node->{focused}, 1, 'Correct node focused');
1283         } elsif ($char =~ /[[:alnum:]]/) {
1284             $node_name = $char;
1285         } else {
1286             die "Could not understand $char";
1287         }
1288
1289         if ($node_layout || $node_name) {
1290             if (exists($counters{$depth})) {
1291                 $counters{$depth} = $counters{$depth} + 1;
1292             } else {
1293                 $counters{$depth} = 0;
1294             }
1295
1296             $node = $nodes->[$counters{0}];
1297             for my $i (1 .. $depth) {
1298                 $node = $node->{nodes}->[$counters{$i}];
1299             }
1300
1301             if ($node_layout) {
1302                 $tester->is_eq($node->{layout}, $node_layout, "Layouts match in depth $depth, node number " . $counters{$depth});
1303             } else {
1304                 $tester->is_eq($node->{name}, $node_name, "Names match in depth $depth, node number " . $counters{$depth});
1305             }
1306         }
1307     }
1308 }
1309
1310
1311
1312 =head1 AUTHOR
1313
1314 Michael Stapelberg <michael@i3wm.org>
1315
1316 =cut
1317
1318 package i3test::X11;
1319 use parent 'X11::XCB::Connection';
1320
1321 sub input_focus {
1322     my $self = shift;
1323     i3test::sync_with_i3();
1324
1325     return $self->SUPER::input_focus(@_);
1326 }
1327
1328 1