[core] stay in CON_STATE_CLOSE until done with req
[lighttpd.git] / tests / LightyTest.pm
blobb975d6510fbb43043869b8953dbc196b0365f893
1 package LightyTest;
3 use strict;
4 use IO::Socket;
5 use Test::More;
6 use Socket;
7 use Cwd 'abs_path';
8 use POSIX qw(:sys_wait_h dup2);
9 use Errno qw(EADDRINUSE);
11 sub find_program {
12 my @DEFAULT_PATHS = ('/usr/bin/', '/usr/local/bin/');
13 my ($envname, $program) = @_;
14 my $location;
16 if (defined $ENV{$envname}) {
17 $location = $ENV{$envname};
18 } else {
19 $location = `which "$program" 2>/dev/null`;
20 chomp $location;
21 if (! -x $location) {
22 for my $path (@DEFAULT_PATHS) {
23 $location = $path . $program;
24 last if -x $location;
29 if (-x $location) {
30 $ENV{$envname} = $location;
31 return 1;
32 } else {
33 delete $ENV{$envname};
34 return 0;
38 BEGIN {
39 our $HAVE_PHP = find_program('PHP', 'php-cgi');
40 our $HAVE_PERL = find_program('PERL', 'perl');
41 if (!$HAVE_PERL) {
42 die "Couldn't find path to perl, but it obviously seems to be running";
46 sub mtime {
47 my $file = shift;
48 my @stat = stat $file;
49 return @stat ? $stat[9] : 0;
52 sub new {
53 my $class = shift;
54 my $self = {};
55 my $lpath;
57 $self->{CONFIGFILE} = 'lighttpd.conf';
59 $lpath = (defined $ENV{'top_builddir'} ? $ENV{'top_builddir'} : '..');
60 $self->{BASEDIR} = abs_path($lpath);
62 $lpath = (defined $ENV{'top_builddir'} ? $ENV{'top_builddir'}."/tests/" : '.');
63 $self->{TESTDIR} = abs_path($lpath);
65 $lpath = (defined $ENV{'srcdir'} ? $ENV{'srcdir'} : '.');
66 $self->{SRCDIR} = abs_path($lpath);
69 if (mtime($self->{BASEDIR}.'/src/lighttpd') > mtime($self->{BASEDIR}.'/build/lighttpd')) {
70 $self->{BINDIR} = $self->{BASEDIR}.'/src';
71 if (mtime($self->{BASEDIR}.'/src/.libs')) {
72 $self->{MODULES_PATH} = $self->{BASEDIR}.'/src/.libs';
73 } else {
74 $self->{MODULES_PATH} = $self->{BASEDIR}.'/src';
76 } else {
77 $self->{BINDIR} = $self->{BASEDIR}.'/build';
78 $self->{MODULES_PATH} = $self->{BASEDIR}.'/build';
80 $self->{LIGHTTPD_PATH} = $self->{BINDIR}.'/lighttpd';
81 $self->{PORT} = 2048;
83 my ($name, $aliases, $addrtype, $net) = gethostbyaddr(inet_aton("127.0.0.1"), AF_INET);
85 $self->{HOSTNAME} = $name;
87 bless($self, $class);
89 return $self;
92 sub listening_on {
93 my $self = shift;
94 my $port = shift;
96 my $remote = IO::Socket::INET->new(
97 Proto => "tcp",
98 PeerAddr => "127.0.0.1",
99 PeerPort => $port) or return 0;
101 close $remote;
103 return 1;
106 sub stop_proc {
107 my $self = shift;
109 my $pid = $self->{LIGHTTPD_PID};
110 if (defined $pid && $pid != -1) {
111 kill('TERM', $pid) or return -1;
112 return -1 if ($pid != waitpid($pid, 0));
113 } else {
114 diag("\nProcess not started, nothing to stop");
115 return -1;
118 return 0;
121 sub wait_for_port_with_proc {
122 my $self = shift;
123 my $port = shift;
124 my $child = shift;
125 my $timeout = 10*10; # 10 secs (valgrind might take a while), select waits 0.1 s
127 while (0 == $self->listening_on($port)) {
128 select(undef, undef, undef, 0.1);
129 $timeout--;
131 # the process is gone, we failed
132 if (0 != waitpid($child, WNOHANG)) {
133 return -1;
135 if (0 >= $timeout) {
136 diag("\nTimeout while trying to connect; killing child");
137 kill('TERM', $child);
138 return -1;
142 return 0;
145 sub start_proc {
146 my $self = shift;
147 # kill old proc if necessary
148 #$self->stop_proc;
150 if ($self->listening_on($self->{PORT})) {
151 diag("\nPort ".$self->{PORT}." already in use");
152 return -1;
155 # pre-process configfile if necessary
158 $ENV{'SRCDIR'} = $self->{BASEDIR}.'/tests';
159 $ENV{'PORT'} = $self->{PORT};
161 my @cmdline = ($self->{LIGHTTPD_PATH}, "-D", "-f", $self->{SRCDIR}."/".$self->{CONFIGFILE}, "-m", $self->{MODULES_PATH});
162 if (defined $ENV{"TRACEME"} && $ENV{"TRACEME"} eq 'strace') {
163 @cmdline = (qw(strace -tt -s 4096 -o strace -f -v), @cmdline);
164 } elsif (defined $ENV{"TRACEME"} && $ENV{"TRACEME"} eq 'truss') {
165 @cmdline = (qw(truss -a -l -w all -v all -o strace), @cmdline);
166 } elsif (defined $ENV{"TRACEME"} && $ENV{"TRACEME"} eq 'gdb') {
167 @cmdline = ('gdb', '--batch', '--ex', 'run', '--ex', 'bt full', '--args', @cmdline);
168 } elsif (defined $ENV{"TRACEME"} && $ENV{"TRACEME"} eq 'valgrind') {
169 @cmdline = (qw(valgrind --tool=memcheck --track-origins=yes --show-reachable=yes --leak-check=yes --log-file=valgrind.%p), @cmdline);
171 # diag("\nstarting lighttpd at :".$self->{PORT}.", cmdline: ".@cmdline );
172 my $child = fork();
173 if (not defined $child) {
174 diag("\nFork failed");
175 return -1;
177 if ($child == 0) {
178 exec @cmdline or die($?);
181 if (0 != $self->wait_for_port_with_proc($self->{PORT}, $child)) {
182 diag(sprintf('\nThe process %i is not up', $child));
183 return -1;
186 $self->{LIGHTTPD_PID} = $child;
191 sub handle_http {
192 my $self = shift;
193 my $t = shift;
194 my $EOL = "\015\012";
195 my $BLANK = $EOL x 2;
196 my $host = "127.0.0.1";
198 my @request = $t->{REQUEST};
199 my @response = $t->{RESPONSE};
200 my $slow = defined $t->{SLOWREQUEST};
201 my $is_debug = $ENV{"TRACE_HTTP"};
203 my $remote =
204 IO::Socket::INET->new(
205 Proto => "tcp",
206 PeerAddr => $host,
207 PeerPort => $self->{PORT});
209 if (not defined $remote) {
210 diag("\nconnect failed: $!");
211 return -1;
214 $remote->autoflush(1);
216 if (!$slow) {
217 diag("\nsending request header to ".$host.":".$self->{PORT}) if $is_debug;
218 foreach(@request) {
219 # pipeline requests
220 s/\r//g;
221 s/\n/$EOL/g;
223 print $remote $_.$BLANK;
224 diag("\n<< ".$_) if $is_debug;
226 } else {
227 diag("\nsending request header to ".$host.":".$self->{PORT}) if $is_debug;
228 foreach(@request) {
229 # pipeline requests
230 chomp;
231 s/\r//g;
232 s/\n/$EOL/g;
234 print $remote $_;
235 diag("<< ".$_."\n") if $is_debug;
236 select(undef, undef, undef, 0.1);
237 print $remote "\015";
238 select(undef, undef, undef, 0.1);
239 print $remote "\012";
240 select(undef, undef, undef, 0.1);
241 print $remote "\015";
242 select(undef, undef, undef, 0.1);
243 print $remote "\012";
244 select(undef, undef, undef, 0.1);
248 diag("\n... done") if $is_debug;
250 my $lines = "";
252 diag("\nreceiving response") if $is_debug;
253 # read everything
254 while(<$remote>) {
255 $lines .= $_;
256 diag(">> ".$_) if $is_debug;
258 diag("\n... done") if $is_debug;
260 close $remote;
262 my $full_response = $lines;
264 my $href;
265 foreach $href ( @{ $t->{RESPONSE} }) {
266 # first line is always response header
267 my %resp_hdr;
268 my $resp_body;
269 my $resp_line;
270 my $conditions = $_;
272 for (my $ln = 0; defined $lines; $ln++) {
273 (my $line, $lines) = split($EOL, $lines, 2);
275 # header finished
276 last if(!defined $line or length($line) == 0);
278 if ($ln == 0) {
279 # response header
280 $resp_line = $line;
281 } else {
282 # response vars
284 if ($line =~ /^([^:]+):\s*(.+)$/) {
285 (my $h = $1) =~ tr/[A-Z]/[a-z]/;
287 if (defined $resp_hdr{$h}) {
288 # diag(sprintf("\nheader '%s' is duplicated: '%s' and '%s'\n",
289 # $h, $resp_hdr{$h}, $2));
290 $resp_hdr{$h} .= ', '.$2;
291 } else {
292 $resp_hdr{$h} = $2;
294 } else {
295 diag(sprintf("\nunexpected line '%s'", $line));
296 return -1;
301 if (not defined($resp_line)) {
302 diag(sprintf("\nempty response"));
303 return -1;
306 $t->{etag} = $resp_hdr{'etag'};
307 $t->{date} = $resp_hdr{'date'};
309 # check length
310 if (defined $resp_hdr{"content-length"}) {
311 $resp_body = substr($lines, 0, $resp_hdr{"content-length"});
312 if (length($lines) < $resp_hdr{"content-length"}) {
313 $lines = "";
314 } else {
315 $lines = substr($lines, $resp_hdr{"content-length"});
317 undef $lines if (length($lines) == 0);
318 } else {
319 $resp_body = $lines;
320 undef $lines;
323 # check conditions
324 if ($resp_line =~ /^(HTTP\/1\.[01]) ([0-9]{3}) .+$/) {
325 if ($href->{'HTTP-Protocol'} ne $1) {
326 diag(sprintf("\nproto failed: expected '%s', got '%s'", $href->{'HTTP-Protocol'}, $1));
327 return -1;
329 if ($href->{'HTTP-Status'} ne $2) {
330 diag(sprintf("\nstatus failed: expected '%s', got '%s'", $href->{'HTTP-Status'}, $2));
331 return -1;
333 } else {
334 diag(sprintf("\nunexpected resp_line '%s'", $resp_line));
335 return -1;
338 if (defined $href->{'HTTP-Content'}) {
339 $resp_body = "" unless defined $resp_body;
340 if ($href->{'HTTP-Content'} ne $resp_body) {
341 diag(sprintf("\nbody failed: expected '%s', got '%s'", $href->{'HTTP-Content'}, $resp_body));
342 return -1;
346 if (defined $href->{'-HTTP-Content'}) {
347 if (defined $resp_body && $resp_body ne '') {
348 diag(sprintf("\nbody failed: expected empty body, got '%s'", $resp_body));
349 return -1;
353 foreach (keys %{ $href }) {
354 next if $_ eq 'HTTP-Protocol';
355 next if $_ eq 'HTTP-Status';
356 next if $_ eq 'HTTP-Content';
357 next if $_ eq '-HTTP-Content';
359 (my $k = $_) =~ tr/[A-Z]/[a-z]/;
361 my $verify_value = 1;
362 my $key_inverted = 0;
364 if (substr($k, 0, 1) eq '+') {
365 $k = substr($k, 1);
366 $verify_value = 0;
367 } elsif (substr($k, 0, 1) eq '-') {
368 ## the key should NOT exist
369 $k = substr($k, 1);
370 $key_inverted = 1;
371 $verify_value = 0; ## skip the value check
374 if ($key_inverted) {
375 if (defined $resp_hdr{$k}) {
376 diag(sprintf("\nheader '%s' MUST not be set", $k));
377 return -1;
379 } else {
380 if (not defined $resp_hdr{$k}) {
381 diag(sprintf("\nrequired header '%s' is missing", $k));
382 return -1;
386 if ($verify_value) {
387 if ($href->{$_} =~ /^\/(.+)\/$/) {
388 if ($resp_hdr{$k} !~ /$1/) {
389 diag(sprintf(
390 "\nresponse-header failed: expected '%s', got '%s', regex: %s",
391 $href->{$_}, $resp_hdr{$k}, $1));
392 return -1;
394 } elsif ($href->{$_} ne $resp_hdr{$k}) {
395 diag(sprintf(
396 "\nresponse-header failed: expected '%s', got '%s'",
397 $href->{$_}, $resp_hdr{$k}));
398 return -1;
404 # we should have sucked up everything
405 if (defined $lines) {
406 diag(sprintf("\nunexpected lines '%s'", $lines));
407 return -1;
410 return 0;
413 sub spawnfcgi {
414 my ($self, $binary, $port) = @_;
415 my $child = fork();
416 if (not defined $child) {
417 diag("\nCouldn't fork");
418 return -1;
420 if ($child == 0) {
421 my $iaddr = inet_aton('localhost') || die "no host: localhost";
422 my $proto = getprotobyname('tcp');
423 socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
424 setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!";
425 bind(SOCK, sockaddr_in($port, $iaddr)) || die "bind: $!";
426 listen(SOCK, 1024) || die "listen: $!";
427 dup2(fileno(SOCK), 0) || die "dup2: $!";
428 exec { $binary } ($binary) or die($?);
429 } else {
430 if (0 != $self->wait_for_port_with_proc($port, $child)) {
431 diag(sprintf("\nThe process %i is not up (port %i, %s)", $child, $port, $binary));
432 return -1;
434 return $child;
438 sub endspawnfcgi {
439 my ($self, $pid) = @_;
440 return -1 if (-1 == $pid);
441 kill(2, $pid);
442 waitpid($pid, 0);
443 return 0;