root/trunk/lib/Perlbal/Test.pm @ 822

Revision 822, 8.7 kB (checked in by bradfitz, 5 months ago)

make Perlbal::Test be more robust and only use free ports.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1#!/usr/bin/perl -w
2
3package Perlbal::Test;
4
5=head1 NAME
6
7Perlbal::Test - Test harness for perlbal server
8
9=head1 SYNOPSIS
10
11#  my $msock = Perlbal::Test::start_server();
12
13=head1 DESCRIPTION
14
15Perlbal::Test provides access to a perlbal server running on the
16local host, for testing purposes.
17
18The server can be an already-existing server, a child process, or
19the current process.
20
21Various functions are provided to interact with the server.
22
23=head1 FUNCTIONS
24
25=cut
26
27use strict;
28use POSIX qw( :sys_wait_h );
29use IO::Socket::INET;
30use Socket qw(MSG_NOSIGNAL IPPROTO_TCP TCP_NODELAY SOL_SOCKET);
31use HTTP::Response;
32
33require Exporter;
34use vars qw(@ISA @EXPORT);
35@ISA = qw(Exporter);
36@EXPORT = qw(ua start_server foreach_aio manage filecontent tempdir new_port
37             manage_multi
38             mgmt_port wait_on_child dump_res resp_from_sock msock);
39
40our $i_am_parent = 0;
41our $msock;  # management sock of child
42our $to_kill = 0;
43our $mgmt_port;
44
45our $free_port = 60000;
46
47=head1 I<mgmt_port()>
48
49Return the current management port number.
50
51=cut
52
53sub mgmt_port {
54        return $mgmt_port;
55}
56
57END {
58    manage("shutdown") if $i_am_parent;
59}
60
61=head1 I<dump_res($http_response)>
62
63Return a readable string formatted from an HTTP::Response object.
64Only the first 80 characters of returned content are returned.
65
66=cut
67
68sub dump_res {
69    my $res = shift;
70    my ($pkg, $filename, $line) = caller;
71    my $ret = "$filename:$line ==> ";
72    unless ($res) {
73        $ret .= "[response undefined]\n";
74        return $ret;
75    }
76    my $ct = $res->content;
77    my $len = length $ct;
78    if ($len > 80) {
79        $ct = substr($ct, 0, 80) . "...";
80    }
81    my $status = $res->status_line;
82    $status =~ s/[\r\n]//g;
83    return $ret . "status=[$status] content=$len" . "[$ct]\n";
84}
85
86=head1 I<tempdir()>
87
88Return a newly created temporary directory. The directory will be
89removed automatically upon program exit.
90
91=cut
92
93sub tempdir {
94    require File::Temp;
95    return File::Temp::tempdir( CLEANUP => 1 );
96}
97
98=head1 I<new_port()>
99
100Return the next free port number in the series. Port numbers are assigned
101starting at 60000.
102
103=cut
104
105sub new_port {
106    test_port() ? return $free_port++ : return new_port($free_port++);
107}
108
109=head1 I<test_port()>
110
111Return 1 if the port is free to use for listening on $free_port else return 0.
112
113=cut
114
115sub test_port {
116    my $sock = IO::Socket::INET->new(LocalPort => $free_port) or return 0;
117    $sock->close();
118    return 1;
119}
120
121=head1 I<filecontent($file>>
122
123Return a string containing the contents of the file $file. If $file
124cannot be opened, then return undef.
125
126=cut
127
128sub filecontent {
129    my $file = shift;
130    my $ct;
131    open (F, $file) or return undef;
132    $ct = do { local $/; <F>; };
133    close F;
134    return $ct;
135}
136
137=head1 I<foreach_aio($callback)>
138
139Set the server into each AIO mode (none, ioaio) and call the specified
140callback function with the mode name as argument.
141
142=cut
143
144sub foreach_aio (&) {
145    my $cb = shift;
146
147    foreach my $mode (qw(none ioaio)) {
148        my $line = manage("SERVER aio_mode = $mode");
149        next unless $line;
150        $cb->($mode);
151    }
152}
153
154=head1 I<manage($cmd, %opts)>
155
156Send a command $cmd to the server, and return the response line from
157the server.
158
159Optional arguments are:
160
161  quiet_failure => 1
162
163Output a warning if the response indicated an error,
164unless $opts{quiet_failure} is true, or the command
165was 'shutdown' (which doesn't return a response).
166
167=cut
168
169sub manage {
170    my $cmd = shift;
171    my %opts = @_;
172
173    print $msock "$cmd\r\n";
174    my $res = <$msock>;
175
176    if (!$res || $res =~ /^ERR/) {
177        # Make the result visible in failure cases, unless
178        # the command was 'shutdown'... cause that never
179        # returns anything.
180        warn "Manage command failed: '$cmd' '$res'\n"
181            unless($opts{quiet_failure} || $cmd eq 'shutdown');
182
183        return 0;
184    }
185    return $res;
186}
187
188=head1 I<manage_multi($cmd)>
189
190Send a command $cmd to the server, and return a multi-line
191response. Return the number zero if there was an error or
192no response.
193
194=cut
195
196sub manage_multi {
197    my $cmd = shift;
198
199    print $msock "$cmd\r\n";
200    my $res;
201    while (<$msock>) {
202        last if /^\./;
203        last if /^ERROR/;
204        $res .= $_;
205    }
206    return 0 if !$res || $res =~ /^ERR/;
207    return $res;
208}
209
210=head1 I<start_server($conf)>
211
212Optionally start a perlbal server and return a socket connected to its
213management port.
214
215The argument $conf is a string specifying initial configuration
216commands.
217
218If the environment variable TEST_PERLBAL_FOREGROUND is set to a true
219value then a server will be started in the foreground, in which case
220this function does not return. When the server function finishes,
221exit() will be called to terminate the process.
222
223If the environment variable TEST_PERLBAL_USE_EXISTING is set to a true
224value then a socket will be returned which is connected to an existing
225server's management port.
226
227Otherwise, a child process is forked and a socket is returned which is
228connected to the child's management port.
229
230The management port is assigned automatically, a new port number each
231time this function is called. The starting port number is 60000.
232
233=cut
234
235sub start_server {
236    my $conf = shift;
237    $mgmt_port = new_port();
238
239    if ($ENV{'TEST_PERLBAL_FOREGROUND'}) {
240        _start_perbal_server($conf, $mgmt_port);
241    }
242
243    if ($ENV{'TEST_PERLBAL_USE_EXISTING'}) {
244        my $msock = wait_on_child(0, $mgmt_port);
245        return $msock;
246    }
247
248    my $child = fork;
249    if ($child) {
250        $i_am_parent = 1;
251        $to_kill = $child;
252        my $msock = wait_on_child($child, $mgmt_port);
253        my $rv = waitpid($child, WNOHANG);
254        if ($rv) {
255            die "Child process (webserver) died.\n";
256        }
257        print $msock "proc\r\n";
258        my $spid = undef;
259        while (<$msock>) {
260            last if m!^\.\r?\n!;
261            next unless /^pid:\s+(\d+)/;
262            $spid = $1;
263        }
264        die "Our child was $child, but we connected and it says it's $spid."
265            unless $child == $spid;
266
267        return $msock;
268    }
269
270    # child process...
271    _start_perbal_server($conf, $mgmt_port);
272}
273
274# Start a perlbal server running and tell it to listen on the specified
275# management port number. This function does not return.
276
277sub _start_perbal_server {
278    my ($conf, $mgmt_port) = @_;
279
280    require Perlbal;
281
282    $conf .= qq{
283CREATE SERVICE mgmt
284SET mgmt.listen = 127.0.0.1:$mgmt_port
285SET mgmt.role = management
286ENABLE mgmt
287};
288
289    my $out = sub { print STDOUT "$_[0]\n"; };
290    die "Configuration error" unless Perlbal::run_manage_commands($conf, $out);
291
292    unless (Perlbal::Socket->WatchedSockets() > 0) {
293        die "Invalid configuration.  (shouldn't happen?)  Stopping (self=$$).\n";
294    }
295
296    Perlbal::run();
297    exit 0;
298}
299
300
301=head1 I<msock()>
302
303Return a reference to the socket connected to the server's management
304port.
305
306=cut
307
308sub msock {
309    return $msock;
310}
311
312
313=head1 I<ua()>
314
315Return a new instance of LWP::UserAgent.
316
317=cut
318
319sub ua {
320    require LWP;
321    require LWP::UserAgent;
322    return LWP::UserAgent->new;
323}
324
325=head1 I<wait_on_child($pid, $port)>
326
327Return a socket which is connected to a child process.
328
329$pid specifies the child process id, and $port is the port number on
330which the child is listening.
331
332Several attempts are made; if the child dies or a connection cannot
333be made within 5 seconds then this function dies with an error message.
334
335=cut
336
337sub wait_on_child {
338    my $pid = shift;
339    my $port = shift;
340
341    my $start = time;
342    while (1) {
343        $msock = IO::Socket::INET->new(PeerAddr => "127.0.0.1:$port");
344        return $msock if $msock;
345        select undef, undef, undef, 0.25;
346        if ($pid && waitpid($pid, WNOHANG) > 0) {
347            die "Child process (webserver) died.\n";
348        }
349        die "Timeout waiting for port $port to startup" if time > $start + 5;
350    }
351}
352
353=head1 I<resp_from_sock($sock)>
354
355Read an HTTP response from a socket and return it
356as an HTTP::Response object
357
358In scalar mode, return only the $http_response object.
359
360In array mode, return an array of ($http_response, $firstline) where
361$firstline is the first line read from the socket, for example:
362
363"HTTP/1.1 200 OK"
364
365=cut
366
367sub resp_from_sock {
368    my $sock = shift;
369
370    my $res = "";
371    my $firstline = undef;
372
373    while (<$sock>) {
374        $res .= $_;
375        $firstline ||= $_;
376        last if ! $_ || /^\r?\n/;
377    }
378
379    unless ($firstline) {
380        print STDERR "Didn't get a firstline in HTTP response.\n";
381        return undef;
382    }
383
384    my $resp = HTTP::Response->parse($res);
385    return undef unless $resp;
386
387    my $cl = $resp->header('Content-Length');
388    if (defined $cl && $cl > 0) {
389        my $content = '';
390        my $rv;
391        while (($rv = read($sock, $content, $cl)) &&
392               ($cl -= $rv) > 0) {
393            # don't do anything, the loop is it
394        }
395        $resp->content($content);
396    }
397
398    return wantarray ? ($resp, $firstline) : $resp;
399}
400
4011;
Note: See TracBrowser for help on using the browser.