File Coverage

blib/lib/TAP/Harness/Remote.pm
Criterion Covered Total %
statement 24 145 16.5
branch 0 42 0.0
condition 0 14 0.0
subroutine 8 23 34.7
pod 10 10 100.0
total 42 234 17.9


line stmt bran cond sub pod time code
1             package TAP::Harness::Remote;
2              
3             our $VERSION = '1.10';
4              
5 1     1   50267 use warnings;
  1         3  
  1         44  
6 1     1   7 use strict;
  1         2  
  1         42  
7 1     1   5 use Carp;
  1         6  
  1         113  
8              
9 1     1   8 use base 'TAP::Harness';
  1         2  
  1         16714  
10 1     1   40108 use constant config_path => "$ENV{HOME}/.remote_test";
  1         3  
  1         213  
11 1     1   7 use File::Spec;
  1         2  
  1         143  
12 1     1   7 use Cwd;
  1         2  
  1         84  
13 1     1   974 use YAML;
  1         24096  
  1         2292  
14              
15             =head1 NAME
16              
17             TAP::Harness::Remote - Run tests on a remote server farm
18              
19             =head1 SYNOPSIS
20              
21             prove -l --state=save,slow --harness TAP::Harness::Remote t/*.t
22              
23             =head1 DESCRIPTION
24              
25             Sometimes you want to run tests on a remote testing machine, rather
26             than your local development box. C allows you
27             so reproduce entire directory trees on a remote server via C,
28             and spawn the tests remotely. It also supports round-robin
29             distribution of tests across multiple remote testing machines.
30              
31             =head1 USAGE
32              
33             C synchronizes local directories to the remote
34             testing server. All tests that you wish to run remotely must be
35             somewhere within these "local testing directories." You should
36             configure this set by creating or editing your F<~/.remote_test> file:
37              
38             ---
39             ssh: /usr/bin/ssh
40             local:
41             - /path/to/local/testing/root/
42             - /path/to/another/testing/root/
43             user: username
44             host: remote.testing.host.example.com
45             root: /where/to/place/local/root/on/remote/
46             perl: /usr/bin/perl
47             master: 1
48             ssh_args:
49             - -x
50             - -S
51             - '~/.ssh/master-%r@%h:%p'
52             rsync_args:
53             - -C
54             - --exclude
55             - blib
56             env:
57             FOO: bar
58              
59             See L for more details on the
60             individual configuration options.
61              
62             Once your F<~/.remote_test> is configured, you can run your tests
63             remotely, using:
64              
65             prove -l --harness TAP::Harness::Remote t/*.t
66              
67             Any paths in C<@INC> which point inside your local testing roots are
68             rewritten to point to the equivilent path on the remote host. This is
69             especially useful if you are testing a number of inter-related
70             modules; by placing all of them all as local testing roots, and adding
71             all of their C paths to your C, you can ensure that
72             the remote machine always tests your combination of the modules, not
73             whichever versions are installed on the remote host.
74              
75             If you have a farm of remote hosts, you may change the C
76             configuration variable to be an array reference of hostnames. Tests
77             will be distributed in a round-robin manner across the hosts. Each
78             host will run as many tests in parallel as you specified with C<-j>.
79              
80             Especially when running tests in parallel, it is highly suggested that
81             you use the standard L C<--state=save,slow> option, as
82             this ensures that the slowest tests will run first, reducing your
83             overall test run time.
84              
85             =head1 METHODS
86              
87             =head2 new
88              
89             Overrides L to load the local configuration, and add
90             the necessary hooks for when tests are actually run.
91              
92             =cut
93              
94             sub new {
95 0     0 1   my $class = shift;
96 0           my $self = $class->SUPER::new(@_);
97              
98 0           $self->load_remote_config;
99 0           for ( @{$self->remote_config("local")} ) {
  0            
100 0 0         die
101             "Local testing root ($_) doesn't exist\n"
102             unless -d $_;
103             }
104              
105             # Find which testing root we're under
106              
107             die
108 0 0         "Current path isn't inside of local testing roots (@{$self->remote_config('local')})\n"
  0            
109             unless defined $self->rewrite_path( Cwd::cwd );
110              
111 0 0         die "Testing host not defined\n"
112 0           unless grep { defined and not /\.example\.com$/ }
113 0 0         @{ $self->remote_config("host") };
114              
115 0 0 0       die
116 0           "Can't find or execute ssh command: @{[$self->remote_config('ssh')]}\n"
117             unless -e $self->remote_config("ssh")
118             and -x $self->remote_config("ssh");
119              
120 0           $ENV{HARNESS_PERL} = $self->remote_config("ssh");
121              
122 0           $self->jobs( $self->jobs * @{ $self->remote_config("host") } );
  0            
123              
124 0     0     $self->callback( before_runtests => sub { $self->setup(@_) } );
  0            
125 0     0     $self->callback( parser_args => sub { $self->change_switches(@_) } );
  0            
126 0           return $self;
127             }
128              
129             =head2 config_path
130              
131             Returns the path to the configuration file; this is usually
132             C<$ENV{HOME}/.remote_test>.
133              
134             =head2 default_config
135              
136             Returns, as a hashref, the default configuration. See
137             L.
138              
139             =cut
140              
141             sub default_config {
142             return {
143 0     0 1   user => "smoker",
144             host => "smoke-server.example.com",
145             root => "/home/smoker/remote-test/$ENV{USER}/",
146             perl => "/home/smoker/bin/perl",
147             local => [ "$ENV{HOME}/remote-test/" ],
148             ssh => "/usr/bin/ssh",
149             ssh_args => [ "-x", "-S", "~/.ssh/master-%r@%h:%p" ],
150             rsync_args => [ "-C" ],
151             master => 1,
152             env => {},
153             };
154             }
155              
156             =head2 load_remote_config
157              
158             Loads and canonicalizes the configuration. Writes and uses the
159             default configuration (L) if the file does not exist.
160              
161             =cut
162              
163             sub load_remote_config {
164 0     0 1   my $self = shift;
165 0 0 0       unless ( -e $self->config_path and -r $self->config_path ) {
166 0           YAML::DumpFile( $self->config_path, $self->default_config );
167             }
168 0           $self->{remote_config} = YAML::LoadFile( $self->config_path );
169              
170             # Make local path into an arrayref
171 0 0         $self->{remote_config}{local} = [ $self->{remote_config}{local} ]
172             unless ref $self->{remote_config}{local};
173              
174             # Strip trailing slashes in local dirs, for rsync
175 0           $self->{remote_config}{local} = [map {s|/$||; $_} @{$self->{remote_config}{local}}];
  0            
  0            
  0            
176              
177             # Host should be an arrayref
178 0 0         $self->{remote_config}{host} = [ $self->{remote_config}{host} ]
179             unless ref $self->{remote_config}{host};
180              
181             # Ditto ssh_args
182 0 0 0       $self->{remote_config}{ssh_args}
183             = [ split ' ', ( $self->{remote_config}{ssh_args} || "") ]
184             unless ref $self->{remote_config}{ssh_args};
185              
186             # Also, rsync_args
187 0 0 0       $self->{remote_config}{rsync_args}
188             = [ split ' ', ($self->{remote_config}{rsync_args} || "") ]
189             unless ref $self->{remote_config}{rsync_args};
190              
191             # Defaults for env
192 0   0       $self->{env} ||= {};
193             }
194              
195             =head2 remote_config KEY
196              
197             Returns the configuration value set fo the given C.
198              
199             =cut
200              
201             sub remote_config {
202 0     0 1   my $self = shift;
203 0 0         $self->load_remote_config unless $self->{remote_config};
204 0           return $self->{remote_config}->{ shift @_ };
205             }
206              
207             =head2 userhost [HOST]
208              
209             Returns a valid C string; host is taken to be the first
210             known host, unless provided.
211              
212             =cut
213              
214             sub userhost {
215 0     0 1   my $self = shift;
216 0 0         my $userhost = @_ ? shift : $self->remote_config("host")->[0];
217 0 0         $userhost = $self->remote_config("user") . "\@" . $userhost
218             if $self->remote_config("user");
219 0           return $userhost;
220             }
221              
222             =head2 start_masters
223              
224             Starts the ssh master connections, if support for them is enabled.
225             Otherwise, does nothing. See the man page for C for more
226             information about master connections.
227              
228             =cut
229              
230             sub start_masters {
231 0     0 1   my $self = shift;
232 0 0         return unless $self->remote_config("master");
233              
234             local $SIG{USR1} = sub {
235 0     0     die "Failed to set up SSH master connections\n";
236 0           };
237              
238 0           my $parent = $$;
239 0           for my $host ( @{ $self->remote_config("host") } ) {
  0            
240 0           my $userhost = $self->userhost($host);
241 0           my $pid = fork;
242 0 0         die "Fork failed: $!" unless $pid >= 0;
243 0 0         if ( not $pid ) {
244             # Make sure we clean out this list, so we don't run
245             # anything on _our_ DESTROY
246 0           $self->{ssh_master} = {};
247              
248             # Start the master
249 0           system($self->remote_config("ssh"),
250 0           @{ $self->remote_config("ssh_args") }, "-M", "-N", $userhost);
251              
252             # Signal the parent when we're done; we're still within 2
253             # seconds of starting, we'll catch this and abort.
254 0           kill 'USR1', $parent;
255 0           exit;
256             }
257 0           $self->{ssh_master}{$userhost} = $pid;
258             }
259              
260             # During this sleep, we're waiting for our kids to tell us that
261             # they died.
262 0           sleep 5;
263             }
264              
265             =head2 setup
266              
267             Starts the openssh master connections if need be (see
268             L), then L's over the local roots.
269             Additionally, stores a rewritten PERL5LIB path such that any
270             directories which point into the local root are included in the remote
271             PERL5LIB as well.
272              
273             =cut
274              
275             sub setup {
276 0     0 1   my $self = shift;
277 0     0     $SIG{USR1} = sub {};
  0            
278 0           $self->start_masters;
279 0           $self->rsync;
280              
281             # Set up our perl5lib
282 0   0       $self->{perl5lib} = join( ":", grep {defined} map {$self->rewrite_path($_)} split( /:/, $ENV{PERL5LIB} || "" ) );
  0            
  0            
283 0           $self->{perl5lib} =~ s/^(lib:){1,}/lib:/;
284              
285             # Also, any other env vars
286 0           $self->{env} = [];
287 0           for my $k (keys %{$self->remote_config("env")}) {
  0            
288 0           my $val = $self->remote_config("env")->{$k};
289 0           $val =~ s/'/'"'"'/g;
290 0           push @{$self->{env}}, "$k='$val'";
  0            
291             }
292             }
293              
294             =head2 rsync
295              
296             Sends all local roots to the remote hosts, one at a time, using C.
297              
298             =cut
299              
300             sub rsync {
301 0     0 1   my $self = shift;
302              
303 0           for my $host ( @{ $self->remote_config("host") } ) {
  0            
304 0           my $userhost = $self->userhost($host);
305 0           my $return = system(
306             qw!rsync -avz --delete!,
307 0           @{$self->remote_config('rsync_args')},
308             qq!--rsh!,
309             $self->remote_config("ssh")
310 0           . " @{$self->remote_config('ssh_args')}",
311 0           @{$self->remote_config("local")},
312             "$userhost:" . $self->remote_config("root")
313             );
314 0 0         die "rsync to $userhost failed" if $return;
315             }
316             }
317              
318             =head2 rewrite_path PATH
319              
320             Rewrites the given local C into the remote path on the testing
321             server. Returns undef if the C isn't inside any of the
322             configured local paths.
323              
324             =cut
325              
326             sub rewrite_path {
327 0     0 1   my $self = shift;
328 0           my $path = shift;
329 0           my $remote = $self->remote_config("root");
330 0           for my $local ( @{$self->remote_config("local")} ) {
  0            
331 0 0         if ($path =~ /^$local/) {
332 0           $path =~ s{^$local}{$remote . "/" . (File::Spec->splitpath($local))[-1]}e;
  0            
333 0           return $path;
334             }
335             }
336 0           return undef;
337             }
338              
339             =head2 DESTROY
340              
341             Tears down the ssh master connections, if they were started.
342              
343             =cut
344              
345             sub DESTROY {
346 0     0     my $self = shift;
347 0 0         return unless $self->remote_config("master");
348 0 0         for my $userhost ( keys %{ $self->{ssh_master} || {} } ) {
  0            
349 0 0         next unless kill 0, $self->{ssh_master}{$userhost};
350 0           system $self->remote_config("ssh"), @{ $self->remote_config("ssh_args") }, "-O",
  0            
351             "exit", $userhost;
352             }
353             }
354              
355             =head2 change_switches
356              
357             Changes the switches around, such that the remote perl is called, via
358             ssh. This code is called once per test file.
359              
360             =cut
361              
362             sub change_switches {
363 0     0 1   my ( $self, $args, $test ) = @_;
364              
365 0           my $remote = $self->remote_config("root");
366              
367 0           my @other = grep { not /^-I/ } @{ $args->{switches} };
  0            
  0            
368 0           my @inc = map {"-I$_"} grep {defined $_} map { s/^-I//; $self->rewrite_path($_) }
  0            
  0            
  0            
  0            
369 0           grep {/^-I/} @{ $args->{switches} };
  0            
370              
371 0           my $host = $self->remote_config("host")
372 0           ->[ $self->{hostno}++ % @{ $self->remote_config("host") } ];
373 0           my $userhost = $self->userhost($host);
374 0           $args->{switches} = [
375 0           @{ $self->remote_config("ssh_args") }, $userhost,
376             "cd", $self->rewrite_path( Cwd::cwd ),
377 0           "&&", "PERL5LIB='@{[$self->{perl5lib}]}'",
378 0           @{$self->{env}},
379             $self->remote_config("perl"), @other,
380             @inc
381             ];
382             }
383              
384             =head1 CONFIGURATION AND ENVIRONMENT
385              
386             Configuration is done via the file C<~/.remote_test>, which is a YAML
387             file. Valid keys are:
388              
389             =over
390              
391             =item user
392              
393             The username to use on the remote connection.
394              
395             =item host
396              
397             The host to connect to. If this is an array reference, tests will be
398             distributed, round-robin fashion, across all of the hosts. This does
399             also incur the overhead of rsync'ing to each host.
400              
401             =item root
402              
403             The remote testing root. This is the place where the local roots will
404             be C'd to.
405              
406             =item local
407              
408             The local testing roots. This can be either an array reference of
409             multiple roots, or a single string. Files under each of these
410             directories will be C'd to the remote server. All tests to be
411             run remotely must be within these roots.
412              
413             =item perl
414              
415             The path to the C binary on the remote host.
416              
417             =item ssh
418              
419             The path to the local C binary.
420              
421             =item ssh_args
422              
423             Either a string or an array reference of arguments to pass to ssh.
424             Suggested defaults include C<-x> and C<-S ~/.ssh/master-%r@%h:%p>
425              
426             =item master
427              
428             If a true value is given for this, will attempt to use OpenSSH master
429             connections to reduce the overhead of making repeated connections to
430             the remote host.
431              
432             =item rsync_args
433              
434             Either a string or an array reference of arguments to pass to rsync.
435             You can use this, for say C<--exclude blib>. The arguments C<-avz
436             --delete> are fixed, and then any C are appended. C<-C>
437             is generally a useful and correct option, and is the default when
438             creating new F<.remote_test> files. See L for more details.
439              
440             =item env
441              
442             A hash reference of environment variable names and values, to be
443             used on the remote host.
444              
445             =back
446              
447             =head1 DEPENDENCIES
448              
449             A recent enough TAP::Harness build; 3.03 or later should suffice.
450             Working copies of OpenSSH and rsync.
451              
452             =head1 BUGS AND LIMITATIONS
453              
454             Aborting tests using C<^C> may leave dangling processes on the remote
455             host.
456              
457             Please report any bugs or feature requests to
458             C, or through the web interface at
459             L.
460              
461             =head1 AUTHOR
462              
463             Alex Vandiver C<< >>
464              
465             =head1 LICENCE AND COPYRIGHT
466              
467             Copyright (c) 2007-2008, Best Practical Solutions, LLC. All rights
468             reserved.
469              
470             This module is free software; you can redistribute it and/or
471             modify it under the same terms as Perl itself. See L.
472              
473             =head1 DISCLAIMER OF WARRANTY
474              
475             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
476             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
477             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
478             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
479             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
480             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
481             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
482             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
483             NECESSARY SERVICING, REPAIR, OR CORRECTION.
484              
485             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
486             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
487             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
488             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
489             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
490             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
491             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
492             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
493             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
494             SUCH DAMAGES.
495              
496             =cut
497              
498             1;