Merge branch 'testsuite' into next
This commit is contained in:
commit
1688ab475a
|
@ -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 parent’s 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.
|
||||||
|
|
|
@ -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.
|
||||||
|
# AnyEvent’s 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -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>
|
||||||
#####################################################################
|
#####################################################################
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue