X-Git-Url: https://git.sur5r.net/?a=blobdiff_plain;f=testcases%2Flib%2Fi3test.pm;h=32a17934d8e23d6e91cfb9645dcee68faeecf432;hb=5a29e61a4605cb3a1a9333d8e85d9afdc6b51ab1;hp=f8ad4e2f886aa3b882a03aa0b773c881ca2cf319;hpb=9b8d9f730340ea9d548ab18df45d892c5a2dfdde;p=i3%2Fi3 diff --git a/testcases/lib/i3test.pm b/testcases/lib/i3test.pm index f8ad4e2f..32a17934 100644 --- a/testcases/lib/i3test.pm +++ b/testcases/lib/i3test.pm @@ -16,6 +16,10 @@ use SocketActivation; use v5.10; +# preload +use Test::More (); +use Data::Dumper (); + use Exporter (); our @EXPORT = qw( get_workspace_names @@ -29,6 +33,7 @@ our @EXPORT = qw( open_floating_window get_dock_clients cmd + cmp_float sync_with_i3 does_i3_live exit_gracefully @@ -56,11 +61,54 @@ BEGIN { } } +my $i3_pid; +my $i3_autostart; + +END { + + # testcases which start i3 manually should always call exit_gracefully + # on their own. Let’s see, whether they really did. + if (! $i3_autostart) { + return unless $i3_pid; + + $tester->ok(undef, 'testcase called exit_gracefully()'); + } + + # don't trigger SIGCHLD handler + local $SIG{CHLD}; + + # From perldoc -v '$?': + # Inside an "END" subroutine $? contains the value + # that is going to be given to "exit()". + # + # Since waitpid sets $?, we need to localize it, + # otherwise TAP would be misinterpreted our return status + local $?; + + # When measuring code coverage, try to exit i3 cleanly (otherwise, .gcda + # files are not written) + if ($ENV{COVERAGE} || $ENV{VALGRIND}) { + exit_gracefully($i3_pid, "/tmp/nested-$ENV{DISPLAY}"); + + } else { + kill(9, $i3_pid) + or $tester->BAIL_OUT("could not kill i3"); + + waitpid $i3_pid, 0; + } +} + sub import { - my $class = shift; + my ($class, %args) = @_; my $pkg = caller; - my $test_more_args = @_ ? "qw(@_)" : ""; + $i3_autostart = delete($args{i3_autostart}) // 1; + + my $cv = launch_with_config('-default', dont_block => 1) + if $i3_autostart; + + my $test_more_args = ''; + $test_more_args = join(' ', 'qw(', %args, ')') if keys %args; local $@; eval << "__"; package $pkg; @@ -69,12 +117,14 @@ use Data::Dumper; use AnyEvent::I3; use Time::HiRes qw(sleep); __ - $tester->bail_out("$@") if $@; + $tester->BAIL_OUT("$@") if $@; feature->import(":5.10"); strict->import; warnings->import; $x ||= i3test::X11->new; + $cv->recv if $i3_autostart; + @_ = ($class); goto \&Exporter::import; } @@ -144,6 +194,12 @@ sub wait_for_unmap { # # 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 ] @@ -155,6 +211,7 @@ sub open_window { my %args = @_ == 1 ? %{$_[0]} : @_; my $dont_map = delete $args{dont_map}; + my $before_map = delete $args{before_map}; $args{class} //= WINDOW_CLASS_INPUT_OUTPUT; $args{rect} //= [ 0, 0, 30, 30 ]; @@ -164,6 +221,12 @@ sub open_window { my $window = $x->root->create_child(%args); + if ($before_map) { + # TODO: investigate why _create is not needed + $window->_create; + $before_map->($window) for $window; + } + return $window if $dont_map; $window->map; @@ -185,7 +248,7 @@ sub open_empty_con { my ($i3) = @_; my $reply = $i3->command('open')->recv; - return $reply->{id}; + return $reply->[0]->{id}; } sub get_workspace_names { @@ -194,6 +257,7 @@ sub get_workspace_names { my @outputs = @{$tree->{nodes}}; my @cons; 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}}; @cons = (@cons, @{$content->{nodes}}); @@ -208,7 +272,34 @@ sub get_unused_workspace { $tmp } +=head2 fresh_workspace(...) + +Switches to an unused workspace and returns the name of that workspace. + +Optionally switches to the specified output first. + + my $ws = fresh_workspace; + + # Get a fresh workspace on the second output. + my $ws = fresh_workspace(output => 1); + +=cut sub fresh_workspace { + my %args = @_; + if (exists($args{output})) { + my $i3 = i3(get_socket_path()); + my $tree = $i3->get_tree->recv; + 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 $focused = $content->{focus}->[0]; + my $workspace = first { $_->{id} == $focused } @{$content->{nodes}}; + $workspace = $workspace->{name}; + cmd("workspace $workspace"); + } + my $unused = get_unused_workspace; cmd("workspace $unused"); $unused @@ -274,11 +365,11 @@ sub get_dock_clients { @{$output->{nodes}}); } elsif ($which eq 'top') { my $first = first { $_->{type} == 5 } @{$output->{nodes}}; - @docked = (@docked, @{$first->{nodes}}); + @docked = (@docked, @{$first->{nodes}}) if defined($first); } elsif ($which eq 'bottom') { my @matching = grep { $_->{type} == 5 } @{$output->{nodes}}; my $last = $matching[-1]; - @docked = (@docked, @{$last->{nodes}}); + @docked = (@docked, @{$last->{nodes}}) if defined($last); } } return @docked; @@ -293,17 +384,19 @@ sub workspace_exists { ($name ~~ @{get_workspace_names()}) } +=head2 focused_ws + +Returns the name of the currently focused workspace. + +=cut sub focused_ws { my $i3 = i3(get_socket_path()); my $tree = $i3->get_tree->recv; - my @outputs = @{$tree->{nodes}}; - my @cons; - for my $output (@outputs) { - # get the first CT_CON of each output - my $content = first { $_->{type} == 2 } @{$output->{nodes}}; - my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}}; - return $first->{name} - } + my $focused = $tree->{focus}->[0]; + my $output = first { $_->{id} == $focused } @{$tree->{nodes}}; + my $content = first { $_->{type} == 2 } @{$output->{nodes}}; + my $first = first { $_->{fullscreen_mode} == 1 } @{$content->{nodes}}; + return $first->{name} } # @@ -317,23 +410,22 @@ sub focused_ws { # See also docs/testsuite for a long explanation # 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)) { - $_sync_window = $x->root->create_child( - class => WINDOW_CLASS_INPUT_OUTPUT, - rect => X11::XCB::Rect->new(x => -15, y => -15, width => 10, height => 10 ), + if (!exists($args{window_id}) && + (!defined($_sync_window) || exists($args{no_cache}))) { + $_sync_window = open_window( + rect => [ -15, -15, 10, 10 ], override_redirect => 1, - background_color => '#ff0000', - event_mask => [ 'structure_notify' ], ); - - $_sync_window->map; - - wait_for_event 2, sub { $_[0]->{response_type} == MAP_NOTIFY }; } + 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; @@ -346,7 +438,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, @@ -388,12 +480,16 @@ sub exit_gracefully { }; if (!$exited) { - kill(9, $pid) or die "could not kill i3"; + kill(9, $pid) + or $tester->BAIL_OUT("could not kill i3"); } if ($socketpath =~ m,^/tmp/i3-test-socket-,) { unlink($socketpath); } + + waitpid $pid, 0; + undef $i3_pid; } # Gets the socket path from the I3_SOCKET_PATH atom stored on the X11 root window @@ -409,6 +505,9 @@ sub get_socket_path { my $cookie = $x->get_property(0, $x->get_root_window(), $atom->id, GET_PROPERTY_TYPE_ANY, 0, 256); my $reply = $x->get_property_reply($cookie->{sequence}); my $socketpath = $reply->{value}; + if ($socketpath eq "/tmp/nested-$ENV{DISPLAY}") { + $socketpath .= '-activation'; + } $_cached_socket_path = $socketpath; return $socketpath; } @@ -416,26 +515,31 @@ sub get_socket_path { # # launches a new i3 process with the given string as configuration file. # useful for tests which test specific config file directives. -# -# be sure to use !NO_I3_INSTANCE! somewhere in the file to signal -# complete-run.pl that it should not create an instance of i3 -# sub launch_with_config { - my ($config, $dont_add_socket_path) = @_; + my ($config, %args) = @_; - $dont_add_socket_path //= 0; + $tmp_socket_path = "/tmp/nested-$ENV{DISPLAY}"; - if (!defined($tmp_socket_path)) { - $tmp_socket_path = File::Temp::tempnam('/tmp', 'i3-test-socket-'); + $args{dont_create_temp_dir} //= 0; + + my ($fh, $tmpfile) = tempfile("i3-cfg-for-$ENV{TESTNAME}-XXXXX", UNLINK => 1); + + if ($config ne '-default') { + say $fh $config; + } else { + open(my $conf_fh, '<', './i3-test.config') + or $tester->BAIL_OUT("could not open default config: $!"); + local $/; + say $fh scalar <$conf_fh>; } - my ($fh, $tmpfile) = tempfile('/tmp/i3-test-config-XXXXX', UNLINK => 1); - say $fh $config; - say $fh "ipc-socket $tmp_socket_path" unless $dont_add_socket_path; + say $fh "ipc-socket $tmp_socket_path" + unless $args{dont_add_socket_path}; + close($fh); my $cv = AnyEvent->condvar; - my $pid = activate_i3( + $i3_pid = activate_i3( unix_socket_path => "$tmp_socket_path-activation", display => $ENV{DISPLAY}, configfile => $tmpfile, @@ -443,16 +547,29 @@ sub launch_with_config { testname => $ENV{TESTNAME}, valgrind => $ENV{VALGRIND}, strace => $ENV{STRACE}, + restart => $ENV{RESTART}, cv => $cv, + dont_create_temp_dir => $args{dont_create_temp_dir}, ); + # force update of the cached socket path in lib/i3test + # as soon as i3 has started + $cv->cb(sub { get_socket_path(0) }); + + return $cv if $args{dont_block}; + # blockingly wait until i3 is ready $cv->recv; - # force update of the cached socket path in lib/i3test - get_socket_path(0); + return $i3_pid; +} + +# compares two floats and return true if they differ less +# then 1e-6 +sub cmp_float { + my ($a, $b) = @_; - return $pid; + return abs($a - $b) < 1e-6; } package i3test::X11;