The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w
use strict;
use Scalar::Util qw{reftype};
use File::Path;
use Getopt::Long;
use GRID::Machine qw{slurp_file};
use Fcntl qw(:DEFAULT :flock);

# Package Variables. The user can modify it inside the machine.preamble.pl
# configuration file
#
our $makebuilder = '';
our $build = '';
our $makebuilder_arg = '';
our $build_test_arg = '';
our $build_arg = '';
our $localpreamble = "local.preamble.pl";
my  $report;
my $parallel;
our %preamble;

  my $m; # current machine
  my $tmpdir; # temporary directory in the remote machine

  our $clean_files = sub {
    # Clean files
    return unless UNIVERSAL::isa($m, 'GRID::Machine');
    my $r = $m->eval( q{
      our $tmpdir;
      CORE::chdir "$tmpdir/.." || die "Can't chage to $tmpdir ...";
      rmtree($tmpdir);
    });
    warn "Can't remove files in temporary directory <$tmpdir>: $r\n" unless $r->ok;
  };


  local $SIG{INT} = $SIG{PIPE} = sub { 
    $clean_files->(); 
    warn "Tests were interrupted!";
  };

sub usage {
  return <<'EOS'
Usage:\n$0 [--report reportfile ] [--parallel] distribution.tar.gz machine1 machine2 ... 

  The null string machine '' will fork (via open2) a process executing perl in the local machine 
EOS
}

GetOptions ("localpreamble=s" => \$localpreamble, 'report=s' => \$report, parallel => \$parallel);

my $dist = shift or die usage();

die "No distribution $dist found\n" unless -r $dist;

die "Distribution does not follow standard name convention\n" unless $dist =~ m{([\w.-]+)\.tar\.gz$};
my $dir = $1;

die usage() unless @ARGV;

if (-r $localpreamble) {
  local $/ = undef;

  my $code = slurp_file($localpreamble);
  eval $code;
  warn("Error in $localpreamble $@. Local preamble skipped") if  $@;
}


for my $host (@ARGV) {

  if ($parallel) {
    my $pid = fork();
    next if $pid;
  }

  my $LOG = '';
  my $prefix = sub {
    my $str = shift;

    $str =~ s/^/$host:/mg;
    $LOG .= $str;
    $str;
  };

  $m = eval { 
    GRID::Machine->new(host => $host) 
  };

  warn "Cant' create GRID::Machine connection with $host\n", next unless UNIVERSAL::isa($m, 'GRID::Machine');

  my $r = $m->eval(q{ 
      our $tmpdir = File::Temp::tempdir;
      chdir($tmpdir) or die "Can't change to dir <$tmpdir>\n"; 
      $tmpdir;
    }
  );

  warn($r),next unless $r->ok;
  $tmpdir = $r->result;

  my $c = $preamble{$host};
  if ($c) {
    $r = $m->eval($c);
    warn("Error in $localpreamble preamble for host $host: $r"),next unless $r->ok;
  }

  my $preamble = $host.".preamble.pl";
  if (-r $preamble) {
    local $/ = undef;

    my $code = slurp_file($preamble);
    $r = $m->eval($code);
    warn("Error in $host preamble: $r"),next unless $r->ok;
  }

  $m->put([$dist]) or die "Can't copy distribution in $host\n";

  $r = $m->eval(q{
      my $dist = shift;

      eval('use Archive::Tar');
      if (Archive::Tar->can('new')) {
        # Archive::Tar is installed, use it
        my $tar = Archive::Tar->new;
        $tar->read($dist,1) or die "Archive::Tar error: Can't read distribution $dist\n";
        $tar->extract() or die "Archive::Tar error: Can't extract distribution $dist\n";
      }
      else {
        CORE::system("gunzip $dist") and die "Can't gunzip $dist\n";
        my $tar = $dist;
        $tar =~ s/\.gz$//;
        CORE::system('tar', '-xf', $tar) and die "Can't untar $tar\n";
      }
    },
    $dist # arg for eval
  );

  warn($r), next unless $r->ok;

  $m->chdir($dir)->ok or do {
    warn "$host: Can't change to directory $dir\n";
    next;
  };


  unless ($makebuilder && $build) {
    ($makebuilder, $build) = $m->eval(q{
      return ('Makefile.PL', 'make') if -e 'Makefile.PL';
      return ('Build.PL', './Build') if -e 'Build.PL';
      return '';
    })->Results;
  }

  $r = $m->system("perl $makebuilder $makebuilder_arg");
  print $prefix->("$r");
  next if !$r->ok && $r->stderr;

  $r = $m->system("$build $build_arg");
  print $prefix->("$r");
  next if !$r->ok && $r->stderr;

  $r = $m->system("$build test $build_test_arg");
  print $prefix->("$r");
  warn "Errors while running tests in $host: $r\n" unless $r->ok;

  # Clean files
  $clean_files->();

  if ($report) { 
    open my $rf, '>', "$report.$host" or die "Can't open file log_$report.$host";
      print $rf $LOG;
      print $rf "\n";
    close($rf);
  }

  exit(0) if $parallel;
}

if ($parallel) {
  wait for @ARGV;
}

__END__

=head1 NAME

remotetest.pl - make tests on a remote machine


=head1 SYNOPSYS

  remotetest.pl MyInteresting-Dist-1.107.tar.gz machine1.domain machine2.domain
  remotetest.pl  -l some.preamble.pl MyInteresting-Dist-1.107.tar.gz machine1.domain ...
  remotetest.pl  -rep reportfile MyInteresting-Dist-1.107.tar.gz machine1.domain ...

=head1 MOTIVATION

Check your Perl distribution on diferent platforms avoiding the familiar excuse
I<It works on my machine>.

=head1 REQUIREMENTS

The script assumes that 
automatic authentification via SSH has been set up with each of the remote 
UNIX machines.


=head1 DESCRIPTION

The script C<remotetest.pl> copies  the specified Perl distribution 

                MyInteresting-Dist-1.107.tar.gz

(see L<ExtUtils::MakeMaker> and L<Module::Build>) to each of the listed machines 
C<machine1.domain>, C<machine2.domain>, etc. 
and proceeeds to  test the distribution (C<make test>) on these machines.
Namely, it does the following steps:

=over 2

=item * Evals a preamble Perl file named by default C<local.preamble.pl>. Use that file
to init any configuration variables and set up things in both the local and
remote machines 

=item *  For each of the machines C<machine1.domain>, C<machine2.domain>, etc.

=over 2

=item - A C<ssh> connection is open. If a file with name C<preamble.$machine.pl> exists
(being C<$machine> the name of the machine) it will be evaluated in the remote machine C<$machine>.
A temporary directory is created and the distribution is transferred
(using C<scp>) to that directory

=item - The distribution is unpacked using C<tar> and C<gunzip>. The program then changes directory
to the distribution directory

=item - If a file C<Makefile.PL> exists, the classical sequence

                      perl Makefile.PL
                      make
                      make test

takes place. Otherwise, if a file C<Build.PL> exists, the default sequence is

                      perl Build.Pl
                      ./Build
                      ./Build test

Options can be passed in each of these steps setting some variables in
C<local.preamble.pl> (See section L<SPECIAL VARIABLES>)

=item - In case of error the program will proceed with the next machine in the list
of arguments

=item - After reporting the test results, the temporary directories
and files are removed

=back

=back

=head1 THE LOCAL PREAMBLE FILE  

The local preamble file (by default C<local.premable.pl>) can be used
to initialize the state both in the local and remote machines.
If the command option C<--localpreamble filename> is 
specified C<filename> will be used instead of C<local.premable.pl>

If exists the preamble file is used to set the variables that govern the 
execution and the environment in which the tests will be performed.
The public variables are:

=head2 SPECIAL VARIABLES

The following variables have a special meaning:

=over 2

=item *  C<$makebuilder> 

the name of the Perl program that builds the builder. Defaults
to C<Makefile.PL> if the distribution has one. Otherwise it defaults to C<Build.PL>  
if the distribution has one.
If you set a value for C<$makebuilder> and C<$build> these will be used instead.

=item * C<$build> 

defaults to C<make> or C<./Build>

=item * C<$makebuilder_arg>  

arguments for C<perl Makefile.PL> or C<perl Build.PL>

=item * C<$build_arg> 

arguments for C<make> or C<./Build>

=item * C<$build_test_arg> 

arguments for C<make test> or C<./Build test>

=item * C<%preamble> 

hash indexed in the machine IPs/names. Values are
strings containing the preamble code that will be evaluated in
the corresponding machine when the SSH connection is set

=back

=head1 THE REMOTE PREAMBLE FILES 

When conecting to C<machine1.domain> the program checks if a file
with name C<machine1.domain.premable.pl> exists. If so it will be 
loaded and evaluated in such machine before running the tests.
Settings in this file take precedence over the ones in the hash C<%preamble>

=head1 LIMITATIONS

=over 2

=item *
No input from C<STDIN> is allowed either in your C<Makefile.PL> (or C<Build.PL>)
or during any of the testing phases, i.e. your programs must run in batch mode

=item * The current version does not allow to change the arguments for C<perl Makefile.PL>,
etc. on a machine basis. The sequence of commands is the the same in each machine

=back


=head1 EXAMPLE

I have a file named C<local.preamble.pl> in the distribution directory
of C<GRID::Machine>:

  pp2@nereida:~/LGRID_Machine$ cat -n local.preamble.pl
     1  # This code is executed in the local machine
     2
     3  # Redefine them if needed
     4  #our $makebuilder = 'Makefile:PL';
     5  #our $build = 'make';
     6  #our $makebuilder_arg = ''; # s.t. like INSTALL_BASE=~/personalmodules
     7  #our $build_arg = '';       # arguments for "make"/"Build"
     8
     9  our $build_test_arg = 'TEST_VERBOSE=1';
    10
    11  # This code will be executed in the remote servers
    12  our %preamble = (
    13    beowulf => q{ $ENV{GRID_REMOTE_MACHINE} = "orion"; },
    14    orion   => q{ $ENV{GRID_REMOTE_MACHINE} = "beowulf"; },
    15  );


Now when I run C<remotetest.pl> for a distribution in machine C<beowulf>
the environment variable C<GRID_REMOTE_MACHINE> will be set in C<beowulf> prior
to the execution of the tests and the tests will run with C<TEST_VERBOSE=1>:

  pp2@nereida:~/LGRID_Machine$ remotetest.pl GRID-Machine-0.091.tar.gz beowulf
  ************beowulf************
  Checking if your kit is complete...
  Looks good
  Writing Makefile for GRID::Machine
  cp lib/GRID/Machine.pm blib/lib/GRID/Machine.pm
  ...............................................................
  /usr/bin/perl "-MExtUtils::MY" -e "MY->fixin(shift)" blib/script/remotetest.pl
  Manifying blib/man1/remotetest.pl.1p
  ...............................................................
  PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" \
    "-e" "test_harness(1, 'blib/lib', 'blib/arch')" t/*.t
  t/01synopsis..............1..10
  ok 1 - use GRID::Machine;
  ok 2 - No fatals creating a GRID::Machine object
  ok 3 - installed sub on remote machine
  ok 4 - RPC didn't died
  ok 5 - nested structures
  ok 6 - Remote died gracefully
  ok 7 - Syntax error correctly catched
  ok 8 - Undefined subroutine error correctly catched
  ok 9 - Equal local references look equal on the remote side
  ok 10 - Equal remote references look equal on the local side
  ok
  t/02pod...................1..138
  ............................................................
  All tests successful.
  Files=12, Tests=213, 19 wallclock secs ( 3.12 cusr +  0.32 csys =  3.44 CPU)


To make things more comfortable, I usually set in function C<MY::postamble>
inside the C<Makefile.PL> a target C<remotetest> (see L<ExtUtils::MakeMaker>)

  sub MY::postamble {
          my @machines = qw{orion beowulf};

  remotetest:
          remotetest.pl \${DISTVNAME}.tar.gz @machines
  EOT
  }

this way I can simply run the remote test by writing :

  pp2@nereida:~/LGRID_Machine$ make remotetest
  scripts/remotetest.pl GRID-Machine-0.091.tar.gz orion beowulf
  ************orion************
  Checking if your kit is complete...
  Looks good
  ...................................


=head1 AUTHOR

Casiano Rodriguez Leon E<lt>casiano@ull.esE<gt>

=head1 COPYRIGHT

(c) Copyright 2008 Casiano Rodriguez-Leon

This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.8 or,
at your option, any later version of Perl 5 you may have available.

=cut