Merge branch 'testsuite' into next

This commit is contained in:
Michael Stapelberg 2011-10-06 00:08:06 +01:00
commit 1688ab475a
7 changed files with 127 additions and 27 deletions

View File

@ -1,7 +1,7 @@
i3 testsuite i3 testsuite
============ ============
Michael Stapelberg <michael+i3@stapelberg.de> Michael Stapelberg <michael+i3@stapelberg.de>
September 2011 October 2011
This document explains how the i3 testsuite works, how to use it and extend it. This document explains how the i3 testsuite works, how to use it and extend it.
It is targeted at developers who not necessarily have been doing testing before It is targeted at developers who not necessarily have been doing testing before
@ -449,3 +449,97 @@ request. You should use a random value in +data[1]+ and check that you received
the same one when getting the reply. the same one when getting the reply.
== Appendix B: Socket activation == Appendix B: Socket activation
Socket activation is a mechanism which was made popular by systemd, an init
replacement. It basically describes creating a listening socket before starting
a program. systemd will invoke the program only when an actual connection to
the socket is made, hence the term socket activation.
The interesting part of this (in the i3 context) is that you can very precisely
detect when the program is ready (finished its initialization).
=== Preparing the listening socket
+complete-run.pl+ will create a listening UNIX socket which it will then pass
to i3. This socket will be used by i3 as an additional IPC socket, just like
the one it will create on its own. Passing the socket happens implicitly
because children will inherit the parents sockets when fork()ing and sockets
will continue to exist after an exec() call (unless CLOEXEC is set of course).
The only explicit things +complete-run.pl+ has to do is setting the +LISTEN_FDS+
environment variable to the number of sockets which exist (1 in our case) and
setting the +LISTEN_PID+ environment variable to the current process ID. Both
variables are necessary so that the program (i3) knows how many sockets it
should use and if the environment variable is actually intended for it. i3 will
then start looking for sockets at file descriptor 3 (since 0, 1 and 2 are used
for stdin, stdout and stderr, respectively).
The actual Perl code which sets up the socket, fork()s, makes sure the socket
has file descriptor 3 and sets up the environment variables follows (shortened
a bit):
.Setup socket and environment
-----------------------------
my $socket = IO::Socket::UNIX->new(
Listen => 1,
Local => $args{unix_socket_path},
);
my $pid = fork;
if ($pid == 0) {
$ENV{LISTEN_PID} = $$;
$ENV{LISTEN_FDS} = 1;
# Only pass file descriptors 0 (stdin), 1 (stdout),
# 2 (stderr) and 3 (socket) to the child.
$^F = 3;
# If the socket does not use file descriptor 3 by chance
# already, we close fd 3 and dup2() the socket to 3.
if (fileno($socket) != 3) {
POSIX::close(3);
POSIX::dup2(fileno($socket), 3);
}
exec "/usr/bin/i3";
}
-----------------------------
=== Waiting for a reply
In the parent process, we want to know when i3 is ready to answer our IPC
requests and handle our windows. Therefore, after forking, we immediately close
the listening socket (i3 will handle this side of the socket) and connect to it
(remember, we are talking about a named UNIX socket) as a client. This connect
call will immediately succeed because the kernel buffers it. Then, we send a
request (of type GET_TREE, but that is not really relevant). Writing data to
the socket will also succeed immediately because, again, the kernel buffers it
(only up to a certain amount of data of course).
Afterwards, we just blockingly wait until we get an answer. In the child
process, i3 will setup the listening socket in its event loop. Immediately
after actually starting the event loop, it will notice a new client connecting
(the parent process) and handle its request. Since all initialization has been
completed successfully by the time the event loop is entered, we can now assume
that i3 is ready.
=== Timing and conclusion
A beautiful feature of this mechanism is that it does not depend on timing. It
does not matter when the child process gets CPU time or when the parent process
gets CPU time. On heavily loaded machines (or machines with multiple CPUs,
cores or unreliable schedulers), this makes waiting for i3 much more robust.
Before using socket activation, we typically used a +sleep(1)+ and hoped that
i3 was initialized by that time. Of course, this breaks on some (slow)
computers and wastes a lot of time on faster computers. By using socket
activation, we decreased the total amount of time necessary to run all tests
(72 files at the time of writing) from > 100 seconds to 16 seconds. This makes
it significantly more attractive to run the test suite more often (or at all)
during development.
An alternative approach to using socket activation is polling for the existance
of the IPC socket and connecting to it. While this might be slightly easier to
implement, it wastes CPU time and is considerably more ugly than this solution
:). After all, +lib/SocketActivation.pm+ contains only 54 SLOC.

View File

@ -27,16 +27,18 @@ use TAP::Parser::Aggregator;
use lib qw(lib); use lib qw(lib);
use SocketActivation; use SocketActivation;
# the following modules are not shipped with Perl # the following modules are not shipped with Perl
use EV;
use AnyEvent; use AnyEvent;
use AnyEvent::Handle; use AnyEvent::Handle;
use AnyEvent::I3 qw(:all); use AnyEvent::I3 qw(:all);
use IO::Scalar; # not in core :\
use Try::Tiny; # not in core
use X11::XCB; use X11::XCB;
# install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent / EV # We actually use AnyEvent to make sure it loads an event loop implementation.
# XXX: we could maybe also use a different loop than the default loop in EV? # Afterwards, we overwrite SIGCHLD:
my $cv = AnyEvent->condvar;
# Install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent.
# AnyEvents handler wait()s for every child which conflicts with TAP (TAP
# needs to get the exit status to determine if a test is successful).
$SIG{CHLD} = sub { $SIG{CHLD} = sub {
}; };
@ -103,8 +105,6 @@ my $harness = TAP::Harness->new({ });
my $aggregator = TAP::Parser::Aggregator->new(); my $aggregator = TAP::Parser::Aggregator->new();
$aggregator->start(); $aggregator->start();
my $cv = AnyEvent->condvar;
# We start tests concurrently: For each display, one test gets started. Every # We start tests concurrently: For each display, one test gets started. Every
# test starts another test after completing. # test starts another test after completing.
take_job($_) for @wdisplays; take_job($_) for @wdisplays;
@ -160,7 +160,7 @@ sub take_job {
# files are not written) and fallback to killing it # files are not written) and fallback to killing it
if ($coverage_testing) { if ($coverage_testing) {
my $exited = 0; my $exited = 0;
try { eval {
say "Exiting i3 cleanly..."; say "Exiting i3 cleanly...";
i3("/tmp/nested-$display")->command('exit')->recv; i3("/tmp/nested-$display")->command('exit')->recv;
$exited = 1; $exited = 1;
@ -187,9 +187,10 @@ sub take_job {
say "[$display] Running $test with logfile $logpath"; say "[$display] Running $test with logfile $logpath";
my $output; my $output;
open(my $spool, '>', \$output);
my $parser = TAP::Parser->new({ my $parser = TAP::Parser->new({
exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -Ilib $test| ], exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -Ilib $test| ],
spool => IO::Scalar->new(\$output), spool => $spool,
merge => 1, merge => 1,
}); });

View File

@ -9,11 +9,8 @@ use X11::XCB qw(:all);
use AnyEvent::I3; use AnyEvent::I3;
use EV; use EV;
use List::Util qw(first); use List::Util qw(first);
use List::MoreUtils qw(lastval);
use Time::HiRes qw(sleep); use Time::HiRes qw(sleep);
use Try::Tiny;
use Cwd qw(abs_path); use Cwd qw(abs_path);
use Proc::Background;
use SocketActivation; use SocketActivation;
use v5.10; use v5.10;
@ -269,7 +266,8 @@ sub get_dock_clients {
my $first = first { $_->{type} == 5 } @{$output->{nodes}}; my $first = first { $_->{type} == 5 } @{$output->{nodes}};
@docked = (@docked, @{$first->{nodes}}); @docked = (@docked, @{$first->{nodes}});
} elsif ($which eq 'bottom') { } elsif ($which eq 'bottom') {
my $last = lastval { $_->{type} == 5 } @{$output->{nodes}}; my @matching = grep { $_->{type} == 5 } @{$output->{nodes}};
my $last = $matching[-1];
@docked = (@docked, @{$last->{nodes}}); @docked = (@docked, @{$last->{nodes}});
} }
} }
@ -375,7 +373,7 @@ sub exit_gracefully {
$socketpath ||= get_socket_path(); $socketpath ||= get_socket_path();
my $exited = 0; my $exited = 0;
try { eval {
say "Exiting i3 cleanly..."; say "Exiting i3 cleanly...";
i3($socketpath)->command('exit')->recv; i3($socketpath)->command('exit')->recv;
$exited = 1; $exited = 1;

View File

@ -2,7 +2,6 @@
# vim:ts=4:sw=4:expandtab # vim:ts=4:sw=4:expandtab
use i3test; use i3test;
use List::MoreUtils qw(all);
my $i3 = i3(get_socket_path()); my $i3 = i3(get_socket_path());
@ -17,8 +16,8 @@ my $workspaces = $i3->get_workspaces->recv;
ok(@{$workspaces} > 0, "More than zero workspaces found"); ok(@{$workspaces} > 0, "More than zero workspaces found");
my $name_exists = all { defined($_->{name}) } @{$workspaces}; #my $name_exists = all { defined($_->{name}) } @{$workspaces};
ok($name_exists, "All workspaces have a name"); #ok($name_exists, "All workspaces have a name");
} }

View File

@ -2,9 +2,25 @@
# vim:ts=4:sw=4:expandtab # vim:ts=4:sw=4:expandtab
use i3test; use i3test;
use List::MoreUtils qw(all none);
use List::Util qw(first); use List::Util qw(first);
# to not depend on List::MoreUtils
sub all (&@) {
my $cb = shift;
for (@_) {
return 0 unless $cb->();
}
return 1;
}
sub none (&@) {
my $cb = shift;
for (@_) {
return 0 if $cb->();
}
return 1;
}
my $i3 = i3(get_socket_path()); my $i3 = i3(get_socket_path());
#################### ####################

View File

@ -5,15 +5,10 @@
# Tests if the various ipc_socket_path options are correctly handled # Tests if the various ipc_socket_path options are correctly handled
# #
use i3test; use i3test;
use Cwd qw(abs_path);
use Proc::Background;
use File::Temp qw(tempfile tempdir); use File::Temp qw(tempfile tempdir);
use POSIX qw(getuid); use POSIX qw(getuid);
use v5.10; use v5.10;
# assuming we are run by complete-run.pl
my $i3_path = abs_path("../i3");
##################################################################### #####################################################################
# default case: socket will be created in /tmp/i3-<username>/ipc-socket.<pid> # default case: socket will be created in /tmp/i3-<username>/ipc-socket.<pid>
##################################################################### #####################################################################

View File

@ -7,10 +7,7 @@
# #
use i3test; use i3test;
use Cwd qw(abs_path); use Cwd qw(abs_path);
use Proc::Background;
use File::Temp qw(tempfile tempdir); use File::Temp qw(tempfile tempdir);
use POSIX qw(getuid);
use Data::Dumper;
use v5.10; use v5.10;
# reads in a whole file # reads in a whole file