]> git.sur5r.net Git - i3/i3/blob - testcases/complete-run.pl
681119d3eb30adcb01c7d70b2e4f389f8a91743e
[i3/i3] / testcases / complete-run.pl
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab
3 #
4 # © 2010-2011 Michael Stapelberg and contributors
5 #
6 # syntax: ./complete-run.pl --display :1 --display :2
7 # to run the test suite on the X11 displays :1 and :2
8 # use 'Xdummy :1' and 'Xdummy :2' before to start two
9 # headless X11 servers
10 #
11
12 use strict;
13 use warnings;
14 use EV;
15 use AnyEvent;
16 use IO::Scalar; # not in core :\
17 use File::Temp qw(tempfile tempdir);
18 use v5.10;
19 use Data::Dumper;
20 use Cwd qw(abs_path);
21 use TAP::Harness;
22 use TAP::Parser;
23 use TAP::Parser::Aggregator;
24 use File::Basename qw(basename);
25 use AnyEvent::I3 qw(:all);
26 use Try::Tiny;
27 use Getopt::Long;
28 use Time::HiRes qw(sleep gettimeofday tv_interval);
29 use X11::XCB;
30 use IO::Socket::UNIX; # core
31 use POSIX; # core
32 use AnyEvent::Handle;
33
34 # install a dummy CHLD handler to overwrite the CHLD handler of AnyEvent / EV
35 # XXX: we could maybe also use a different loop than the default loop in EV?
36 $SIG{CHLD} = sub {
37 };
38
39 # reads in a whole file
40 sub slurp {
41     open my $fh, '<', shift;
42     local $/;
43     <$fh>;
44 }
45
46 my $coverage_testing = 0;
47 my @displays = ();
48
49 my $result = GetOptions(
50     "coverage-testing" => \$coverage_testing,
51     "display=s" => \@displays,
52 );
53
54 @displays = split(/,/, join(',', @displays));
55 @displays = map { s/ //g; $_ } @displays;
56
57 @displays = qw(:1) if @displays == 0;
58
59 # connect to all displays for two reasons:
60 # 1: check if the display actually works
61 # 2: keep the connection open so that i3 is not the only client. this prevents
62 #    the X server from exiting (Xdummy will restart it, but not quick enough
63 #    sometimes)
64 my @conns;
65 my @wdisplays;
66 for my $display (@displays) {
67     my $screen;
68     my $x = X11::XCB->new($display, $screen);
69     if ($x->has_error) {
70         say STDERR "WARNING: Not using X11 display $display, could not connect";
71     } else {
72         push @conns, $x;
73         push @wdisplays, $display;
74     }
75 }
76
77 my $config = slurp('i3-test.config');
78
79 # 1: get a list of all testcases
80 my @testfiles = @ARGV;
81
82 # if no files were passed on command line, run all tests from t/
83 @testfiles = <t/*.t> if @testfiles == 0;
84
85 # 2: create an output directory for this test-run
86 my $outdir = "testsuite-";
87 $outdir .= POSIX::strftime("%Y-%m-%d-%H-%M-%S-", localtime());
88 $outdir .= `git describe --tags`;
89 chomp($outdir);
90 mkdir($outdir) or die "Could not create $outdir";
91 unlink("latest") if -e "latest";
92 symlink("$outdir", "latest") or die "Could not symlink latest to $outdir";
93
94 # 3: run all tests
95 my @done;
96 my $num = @testfiles;
97 my $harness = TAP::Harness->new({ });
98
99 my $aggregator = TAP::Parser::Aggregator->new();
100 $aggregator->start();
101
102 my $cv = AnyEvent->condvar;
103
104 # We start tests concurrently: For each display, one test gets started. Every
105 # test starts another test after completing.
106 take_job($_) for @wdisplays;
107
108 #
109 # Takes a test from the beginning of @testfiles and runs it.
110 #
111 # The TAP::Parser (which reads the test output) will get called as soon as
112 # there is some activity on the stdout file descriptor of the test process
113 # (using an AnyEvent->io watcher).
114 #
115 # When a test completes and @done contains $num entries, the $cv condvar gets
116 # triggered to finish testing.
117 #
118 sub take_job {
119     my ($display) = @_;
120
121     my $test = shift @testfiles;
122     return unless $test;
123     my $dont_start = (slurp($test) =~ /# !NO_I3_INSTANCE!/);
124     my $logpath = "$outdir/i3-log-for-" . basename($test);
125
126     my ($fh, $tmpfile) = tempfile('i3-run-cfg.XXXXXX', UNLINK => 1);
127     say $fh $config;
128     say $fh "ipc-socket /tmp/nested-$display";
129     close($fh);
130
131     my $activate_cv = AnyEvent->condvar;
132     my $time_before_start = [gettimeofday];
133     my $start_i3 = sub {
134         # remove the old unix socket
135         unlink("/tmp/nested-$display-activation");
136
137         # pass all file descriptors up to three to the children.
138         # we need to set this flag before opening the socket.
139         open(my $fdtest, '<', '/dev/null');
140         $^F = fileno($fdtest);
141         close($fdtest);
142         my $socket = IO::Socket::UNIX->new(
143             Listen => 1,
144             Local => "/tmp/nested-$display-activation",
145         );
146
147         my $pid = fork;
148         if (!defined($pid)) {
149             die "could not fork()";
150         }
151         if ($pid == 0) {
152             $ENV{LISTEN_PID} = $$;
153             $ENV{LISTEN_FDS} = 1;
154             $ENV{DISPLAY} = $display;
155             $^F = 3;
156
157             POSIX::close(3);
158             POSIX::dup2(fileno($socket), 3);
159
160             # now execute i3
161             my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler";
162             my $cmd = "exec $i3cmd -c $tmpfile >$logpath 2>&1";
163             exec "/bin/sh", '-c', $cmd;
164
165             # if we are still here, i3 could not be found or exec failed. bail out.
166             exit 1;
167         }
168
169         my $child_watcher;
170         $child_watcher = AnyEvent->child(pid => $pid, cb => sub {
171             say "child died. pid = $pid";
172             undef $child_watcher;
173         });
174
175         # close the socket, the child process should be the only one which keeps a file
176         # descriptor on the listening socket.
177         $socket->close;
178
179         # We now connect (will succeed immediately) and send a request afterwards.
180         # As soon as the reply is there, i3 is considered ready.
181         my $cl = IO::Socket::UNIX->new(Peer => "/tmp/nested-$display-activation");
182         my $hdl;
183         $hdl = AnyEvent::Handle->new(fh => $cl, on_error => sub { $activate_cv->send(0) });
184
185         # send a get_tree message without payload
186         $hdl->push_write('i3-ipc' . pack("LL", 0, 4));
187
188         # wait for the reply
189         $hdl->push_read(chunk => 1, => sub {
190             my ($h, $line) = @_;
191             $activate_cv->send(1);
192             undef $hdl;
193         });
194
195         return $pid;
196     };
197
198     my $pid;
199     $pid = $start_i3->() unless $dont_start;
200
201     my $kill_i3 = sub {
202         # Don’t bother killing i3 when we haven’t started it
203         return if $dont_start;
204
205         # When measuring code coverage, try to exit i3 cleanly (otherwise, .gcda
206         # files are not written) and fallback to killing it
207         if ($coverage_testing) {
208             my $exited = 0;
209             try {
210                 say "Exiting i3 cleanly...";
211                 i3("/tmp/nested-$display")->command('exit')->recv;
212                 $exited = 1;
213             };
214             return if $exited;
215         }
216
217         say "[$display] killing i3";
218         kill(9, $pid) or die "could not kill i3";
219     };
220
221     # This will be called as soon as i3 is running and answered to our
222     # IPC request
223     $activate_cv->cb(sub {
224         my $time_activating = [gettimeofday];
225         my $start_duration = tv_interval($time_before_start, $time_activating);
226         my ($status) = $activate_cv->recv;
227         if ($dont_start) {
228             say "[$display] Not starting i3, testcase does that";
229         } else {
230             say "[$display] i3 startup: took " . sprintf("%.2f", $start_duration) . "s, status = $status";
231         }
232
233         say "[$display] Running $test with logfile $logpath";
234
235         my $output;
236         my $parser = TAP::Parser->new({
237             exec => [ 'sh', '-c', qq|DISPLAY=$display LOGPATH="$logpath" /usr/bin/perl -It/lib $test| ],
238             spool => IO::Scalar->new(\$output),
239             merge => 1,
240         });
241
242         my @watchers;
243         my ($stdout, $stderr) = $parser->get_select_handles;
244         for my $handle ($parser->get_select_handles) {
245             my $w;
246             $w = AnyEvent->io(
247                 fh => $handle,
248                 poll => 'r',
249                 cb => sub {
250                     # Ignore activity on stderr (unnecessary with merge => 1,
251                     # but let’s keep it in here if we want to use merge => 0
252                     # for some reason in the future).
253                     return if defined($stderr) and $handle == $stderr;
254
255                     my $result = $parser->next;
256                     if (defined($result)) {
257                         # TODO: check if we should bail out
258                         return;
259                     }
260
261                     # $result is not defined, we are done parsing
262                     say "[$display] $test finished";
263                     close($parser->delete_spool);
264                     $aggregator->add($test, $parser);
265                     push @done, [ $test, $output ];
266
267                     $kill_i3->();
268
269                     undef $_ for @watchers;
270                     if (@done == $num) {
271                         $cv->send;
272                     } else {
273                         take_job($display);
274                     }
275                 }
276             );
277             push @watchers, $w;
278         }
279     });
280
281     $activate_cv->send(1) if $dont_start;
282 }
283
284 $cv->recv;
285
286 $aggregator->stop();
287
288 for (@done) {
289     my ($test, $output) = @$_;
290     say "output for $test:";
291     say $output;
292 }
293
294 # 4: print summary
295 $harness->summary($aggregator);