Merge branch 'testsuite' into next
This commit is contained in:
commit
1688ab475a
|
@ -1,7 +1,7 @@
|
|||
i3 testsuite
|
||||
============
|
||||
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.
|
||||
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.
|
||||
|
||||
== 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 SocketActivation;
|
||||
# the following modules are not shipped with Perl
|
||||
use EV;
|
||||
use AnyEvent;
|
||||
use AnyEvent::Handle;
|
||||
use AnyEvent::I3 qw(:all);
|
||||
use IO::Scalar; # not in core :\
|
||||
use Try::Tiny; # not in core
|
||||
use X11::XCB;
|
||||
|
||||
# install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent / EV
|
||||
# XXX: we could maybe also use a different loop than the default loop in EV?
|
||||
# We actually use AnyEvent to make sure it loads an event loop implementation.
|
||||
# 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 {
|
||||
};
|
||||
|
||||
|
@ -103,8 +105,6 @@ my $harness = TAP::Harness->new({ });
|
|||
my $aggregator = TAP::Parser::Aggregator->new();
|
||||
$aggregator->start();
|
||||
|
||||
my $cv = AnyEvent->condvar;
|
||||
|
||||
# We start tests concurrently: For each display, one test gets started. Every
|
||||
# test starts another test after completing.
|
||||
take_job($_) for @wdisplays;
|
||||
|
@ -160,7 +160,7 @@ sub take_job {
|
|||
# files are not written) and fallback to killing it
|
||||
if ($coverage_testing) {
|
||||
my $exited = 0;
|
||||
try {
|
||||
eval {
|
||||
say "Exiting i3 cleanly...";
|
||||
i3("/tmp/nested-$display")->command('exit')->recv;
|
||||
$exited = 1;
|
||||
|
@ -187,9 +187,10 @@ sub take_job {
|
|||
say "[$display] Running $test with logfile $logpath";
|
||||
|
||||
my $output;
|
||||
open(my $spool, '>', \$output);
|
||||
my $parser = TAP::Parser->new({
|
||||
exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -Ilib $test| ],
|
||||
spool => IO::Scalar->new(\$output),
|
||||
spool => $spool,
|
||||
merge => 1,
|
||||
});
|
||||
|
||||
|
|
|
@ -9,11 +9,8 @@ use X11::XCB qw(:all);
|
|||
use AnyEvent::I3;
|
||||
use EV;
|
||||
use List::Util qw(first);
|
||||
use List::MoreUtils qw(lastval);
|
||||
use Time::HiRes qw(sleep);
|
||||
use Try::Tiny;
|
||||
use Cwd qw(abs_path);
|
||||
use Proc::Background;
|
||||
use SocketActivation;
|
||||
|
||||
use v5.10;
|
||||
|
@ -269,7 +266,8 @@ sub get_dock_clients {
|
|||
my $first = first { $_->{type} == 5 } @{$output->{nodes}};
|
||||
@docked = (@docked, @{$first->{nodes}});
|
||||
} 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}});
|
||||
}
|
||||
}
|
||||
|
@ -375,7 +373,7 @@ sub exit_gracefully {
|
|||
$socketpath ||= get_socket_path();
|
||||
|
||||
my $exited = 0;
|
||||
try {
|
||||
eval {
|
||||
say "Exiting i3 cleanly...";
|
||||
i3($socketpath)->command('exit')->recv;
|
||||
$exited = 1;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
# vim:ts=4:sw=4:expandtab
|
||||
|
||||
use i3test;
|
||||
use List::MoreUtils qw(all);
|
||||
|
||||
my $i3 = i3(get_socket_path());
|
||||
|
||||
|
@ -17,8 +16,8 @@ my $workspaces = $i3->get_workspaces->recv;
|
|||
|
||||
ok(@{$workspaces} > 0, "More than zero workspaces found");
|
||||
|
||||
my $name_exists = all { defined($_->{name}) } @{$workspaces};
|
||||
ok($name_exists, "All workspaces have a name");
|
||||
#my $name_exists = all { defined($_->{name}) } @{$workspaces};
|
||||
#ok($name_exists, "All workspaces have a name");
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,25 @@
|
|||
# vim:ts=4:sw=4:expandtab
|
||||
|
||||
use i3test;
|
||||
use List::MoreUtils qw(all none);
|
||||
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());
|
||||
|
||||
####################
|
||||
|
|
|
@ -5,15 +5,10 @@
|
|||
# Tests if the various ipc_socket_path options are correctly handled
|
||||
#
|
||||
use i3test;
|
||||
use Cwd qw(abs_path);
|
||||
use Proc::Background;
|
||||
use File::Temp qw(tempfile tempdir);
|
||||
use POSIX qw(getuid);
|
||||
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>
|
||||
#####################################################################
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
#
|
||||
use i3test;
|
||||
use Cwd qw(abs_path);
|
||||
use Proc::Background;
|
||||
use File::Temp qw(tempfile tempdir);
|
||||
use POSIX qw(getuid);
|
||||
use Data::Dumper;
|
||||
use v5.10;
|
||||
|
||||
# reads in a whole file
|
||||
|
|
Loading…
Reference in New Issue