The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package MBTest;

use strict;

use IO::File ();
use File::Spec;
use File::Temp ();
use File::Path ();


# Setup the code to clean out %ENV
BEGIN {
    # Environment variables which might effect our testing
    my @delete_env_keys = qw(
        HOME
        DEVEL_COVER_OPTIONS
        MODULEBUILDRC
        PERL_MB_OPT
        HARNESS_TIMER
        HARNESS_OPTIONS
        HARNESS_VERBOSE
        PREFIX
        INSTALL_BASE
        INSTALLDIRS
    );

    # Remember the ENV values because on VMS %ENV is global
    # to the user, not the process.
    my %restore_env_keys;

    sub clean_env {
        for my $key (@delete_env_keys) {
            if( exists $ENV{$key} ) {
                $restore_env_keys{$key} = delete $ENV{$key};
            }
            else {
                delete $ENV{$key};
            }
        }
    }

    END {
        while( my($key, $val) = each %restore_env_keys ) {
            $ENV{$key} = $val;
        }
    }
}


BEGIN {
  clean_env();

  # In case the test wants to use our other bundled
  # modules, make sure they can be loaded.
  my $t_lib = File::Spec->catdir('t', 'bundled');
  push @INC, $t_lib; # Let user's installed version override

  # We change directories, so expand @INC and $^X to absolute paths
  # Also add .
  @INC = (map(File::Spec->rel2abs($_), @INC), ".");
  $^X = File::Spec->rel2abs($^X);
}

use Exporter;
use Test::More;
use Config;
use Cwd ();

# We pass everything through to Test::More
use vars qw($VERSION @ISA @EXPORT %EXPORT_TAGS $TODO);
$VERSION = 0.01_01;
@ISA = qw(Test::More); # Test::More isa Exporter
@EXPORT = @Test::More::EXPORT;
%EXPORT_TAGS = %Test::More::EXPORT_TAGS;

# We have a few extra exports, but Test::More has a special import()
# that won't take extra additions.
my @extra_exports = qw(
  stdout_of
  stderr_of
  stdout_stderr_of
  slurp
  find_in_path
  check_compiler
  have_module
  blib_load
  timed_out
);
push @EXPORT, @extra_exports;
__PACKAGE__->export(scalar caller, @extra_exports);
# XXX ^-- that should really happen in import()


########################################################################

# always return to the current directory
{
  my $cwd;
  # must be done in BEGIN because tmpdir uses it in BEGIN for $ENV{HOME}
  BEGIN { 
    $cwd = File::Spec->rel2abs(Cwd::cwd);
  }

  sub original_cwd { return $cwd }

  END {
    # Go back to where you came from!
    chdir $cwd or die "Couldn't chdir to $cwd";
  }
}
########################################################################

{ # backwards compatible temp filename recipe adapted from perlfaq
  my $tmp_count = 0;
  my $tmp_base_name = sprintf("MB-%d-%d", $$, time());
  sub temp_file_name {
    sprintf("%s-%04d", $tmp_base_name, ++$tmp_count)
  }
}
########################################################################

# Setup a temp directory
sub tmpdir {
  my ($self, @args) = @_;
  my $dir = $ENV{PERL_CORE} ? MBTest->original_cwd : File::Spec->tmpdir;
  return File::Temp::tempdir('MB-XXXXXXXX', CLEANUP => 1, DIR => $dir, @args);
}

BEGIN {
  $ENV{HOME} = tmpdir; # don't want .modulebuildrc or other things interfering
}

sub save_handle {
  my ($handle, $subr) = @_;
  my $outfile = File::Spec->catfile(File::Spec->tmpdir, temp_file_name());

  local *SAVEOUT;
  open SAVEOUT, ">&" . fileno($handle)
    or die "Can't save output handle: $!";
  open $handle, "> $outfile" or die "Can't create $outfile: $!";

  eval {$subr->()};
  open $handle, ">&SAVEOUT" or die "Can't restore output: $!";

  my $ret = slurp($outfile);
  1 while unlink $outfile;
  return $ret;
}

sub stdout_of { save_handle(\*STDOUT, @_) }
sub stderr_of { save_handle(\*STDERR, @_) }
sub stdout_stderr_of {
  my $subr = shift;
  my ($stdout, $stderr);
  $stdout = stdout_of ( sub {
      $stderr = stderr_of( $subr )
  });
  return wantarray ? ($stdout, $stderr) : $stdout . $stderr;
}

sub slurp {
  my $fh = IO::File->new($_[0]) or die "Can't open $_[0]: $!";
  local $/;
  return scalar <$fh>;
}

# Some extensions we should know about if we're looking for executables
sub exe_exts {

  if ($^O eq 'MSWin32') {
    return split($Config{path_sep}, $ENV{PATHEXT} || '.com;.exe;.bat');
  }
  if ($^O eq 'os2') {
    return qw(.exe .com .pl .cmd .bat .sh .ksh);
  }
  return;
}

sub find_in_path {
  my $thing = shift;

  my @exe_ext = exe_exts();
  if ( File::Spec->file_name_is_absolute( $thing ) ) {
    foreach my $ext ( '', @exe_ext ) {
      return "$thing$ext" if -e "$thing$ext";
    }
  }
  else {
    my @path = split $Config{path_sep}, $ENV{PATH};
    foreach (@path) {
      my $fullpath = File::Spec->catfile($_, $thing);
      foreach my $ext ( '', @exe_ext ) {
        return "$fullpath$ext" if -e "$fullpath$ext";
      }
    }
  }
  return;
}

sub check_compiler {
  return (1,1) if $ENV{PERL_CORE};

  local $SIG{__WARN__} = sub {};

  blib_load('Module::Build');
  my $mb = Module::Build->current;
  $mb->verbose( 0 );

  my $have_c_compiler;
  stderr_of( sub {$have_c_compiler = $mb->have_c_compiler} );
  # XXX link_executable() is not yet implemented for Windows
  # and noexec tmpdir is irrelevant on Windows
  return ($have_c_compiler, 1) if $^O eq "MSWin32";

  # check noexec tmpdir
  my $tmp_exec;
  if ( $have_c_compiler ) {
    my $dir = MBTest->tmpdir;
    my $c_file = File::Spec->catfile($dir,'test.c');
    open my $fh, ">", $c_file;
    print {$fh} "int main() { return 0; }\n";
    close $fh;
    my $exe = $mb->cbuilder->link_executable(
      objects => $mb->cbuilder->compile( source => $c_file )
    );
    $tmp_exec = 0 == system( $exe );
  }
  return ($have_c_compiler, $tmp_exec);
}

sub have_module {
  my $module = shift;
  return eval "require $module; 1";
}

sub blib_load {
  # Load the given module and ensure it came from blib/, not the larger system
  my $mod = shift;
  have_module($mod) or die "Error loading $mod\: $@\n";

  (my $path = $mod) =~ s{::}{/}g;
  $path .= ".pm";
  my ($pkg, $file, $line) = caller;
  unless($ENV{PERL_CORE}) {
    unless($INC{$path} =~ m/\bblib\b/) {
      (my $load_from = $INC{$path}) =~ s{$path$}{};
      die "$mod loaded from '$load_from'\nIt should have been loaded from blib.  \@INC contains:\n  ",
      join("\n  ", @INC) . "\nFatal error occured in blib_load() at $file, line $line.\n";
    }
  }
}

sub timed_out {
  my ($sub, $timeout) = @_;
  return unless $sub;
  $timeout ||= 60;

  my $saw_alarm = 0;
  eval {
    local $SIG{ALRM} = sub { $saw_alarm++; die "alarm\n"; }; # NB: \n required
    alarm $timeout;
    $sub->();
    alarm 0;
  };
  if ($@) {
    die unless $@ eq "alarm\n";   # propagate unexpected errors
  }
  return $saw_alarm;
}

sub check_EUI {
  my $timed_out;
  stdout_stderr_of( sub {
      $timed_out = timed_out( sub {
          ExtUtils::Installed->new(extra_libs => [@INC])
        }
      );
    }
  );
  return ! $timed_out;
}

1;
# vim:ts=2:sw=2:et:sta