# (X)Emacs mode: -*- cperl -*-
package test;
=head1 NAME
test - tools for helping in test suites (not including running externalprograms).
=head1 SYNOPSIS
use FindBin 1.42 qw( $Bin );
use Test 1.13 qw( ok plan );
BEGIN { unshift @INC, $Bin };
use test qw( DATA_DIR
evcheck runcheck );
BEGIN {
plan tests => 3,
todo => [],
;
}
ok evcheck(sub {
open my $fh, '>', 'foo';
print $fh "$_\n"
for 'Bulgaria', 'Cholet';
close $fh;
}, 'write foo'), 1, 'write foo';
save_output('stderr', *STDERR{IO});
warn 'Hello, Mum!';
print restore_output('stderr');
=head1 DESCRIPTION
This package provides some variables, and sets up an environment, for test
scripts, such as those used in F<t/>.
This package does not including running external programs; that is provided by
C<test2.pm>. This is so that suites not needing that can include only
test.pm, and so not require the presence of C<IPC::Run>.
Setting up the environment includes:
=over 4
=item Prepending F<blib/script> onto the path
=item Pushing the module F<lib/> dir onto the @PERL5LIB var
For executed scripts.
=item Pushing the module F<lib/> dir onto the @INC var
For internal C<use> calls.
=item Changing directory to a temporary directory
To avoid cluttering the local dir, and/or allowing the local directory
structure to affect matters.
=item Cleaning up the temporary directory afterwards
Unless TEST_DEBUG is set in the environment.
=back
=cut
# ----------------------------------------------------------------------------
# Pragmas -----------------------------
use 5.00503;
use strict;
use vars qw( @EXPORT_OK );
# Inheritance -------------------------
use base qw( Exporter );
=head2 EXPORTS
The following symbols are exported upon request:
=over 4
=item BIN_DIR
=item DATA_DIR
=item REF_DIR
=item LIB_DIR
=item PERL
=item check_req
=item compare
=item evcheck
=item only_files
=item save_output
=item restore_output
=item tmpnam
=item tempdir
=item find_exec
=item read_file
=back
=cut
@EXPORT_OK = qw( BIN_DIR DATA_DIR REF_DIR LIB_DIR PERL
check_req compare evcheck find_exec only_files read_file
save_output restore_output tempdir tmpnam );
# Utility -----------------------------
use Carp qw( carp croak );
use Cwd 2.01 qw( cwd );
use Env qw( PATH PERL5LIB );
use Fatal 1.02 qw( close open seek sysopen unlink );
use Fcntl 1.03 qw( :DEFAULT );
use File::Basename qw( basename );
use File::Compare 1.1002 qw( );
use File::Path 1.0401 qw( mkpath rmtree );
use File::Spec 0.6 qw( );
use FindBin 1.42 qw( $Bin );
use POSIX 1.02 qw( );
use Test 1.122 qw( ok skip );
# ----------------------------------------------------------------------------
sub rel2abs {
if ( File::Spec->file_name_is_absolute($_[0]) ) {
return $_[0];
} else {
return catdir(cwd, $_[0]);
}
}
sub catdir {
File::Spec->catdir(@_);
}
sub catfile {
File::Spec->catfile(@_);
}
sub updir {
File::Spec->updir(@_);
}
sub min {
croak "Can't min over 0 args!\n"
unless @_;
my $min = $_[0];
for (@_[1..$#_]) {
$min = $_
if $_ < $min;
}
return $min;
}
sub max {
croak "Can't max over 0 args!\n"
unless @_;
my $max = $_[0];
for (@_[1..$#_]) {
$max = $_
if $_ > $max;
}
return $max;
}
# -------------------------------------
# PACKAGE CONSTANTS
# -------------------------------------
use constant BIN_DIR => catdir $Bin, updir, 'bin';
use constant DATA_DIR => catdir $Bin, updir, 'data';
use constant REF_DIR => catdir $Bin, updir, 'testref';
use constant LIB_DIR => catdir $Bin, updir, 'lib';
use constant BUILD_SCRIPT_DIR => => catdir $Bin, updir, qw( blib script );
sub find_exec {
my ($exec) = @_;
for (split /:/, $PATH) {
my $try = catfile $_, $exec;
return rel2abs($try)
if -x $try;
}
return;
}
use constant PERL => (basename($^X) eq $^X ?
find_exec($^X) :
rel2abs($^X));
# -------------------------------------
# PACKAGE ACTIONS
# -------------------------------------
# @PERL5LIB not available in Env for perl 5.00503
# unshift @PERL5LIB, LIB_DIR;
$PERL5LIB = defined $PERL5LIB ? join(':', LIB_DIR, $PERL5LIB) : LIB_DIR;
unshift @INC, LIB_DIR;
$PATH = join ':', BUILD_SCRIPT_DIR, split /:/, $PATH;
$_ = rel2abs($_)
for @INC;
my $tmpdn = tempdir();
$| = 1;
mkpath $tmpdn;
die "Couldn't create temp dir: $tmpdn: $!\n"
unless -r $tmpdn and -w $tmpdn and -x $tmpdn and -o $tmpdn and -d $tmpdn;
#@INC = map rel2abs($_), @INC;
chdir $tmpdn;
# -------------------------------------
# PACKAGE FUNCTIONS
# -------------------------------------
=head2 only_files
=over 4
=item ARGUMENTS
=over 4
=item expect
Arrayref of names of files to expect to exist.
=back
=item RETURNS
=over 4
=item ok
1 if exactly expected files exist, false otherwise.
=back
=back
=cut
sub only_files {
my ($expect) = @_;
local *MYDIR;
opendir MYDIR, '.';
my %files = map { $_ => 1 } readdir MYDIR;
closedir MYDIR;
my $ok = 1;
for (@$expect, '.', '..') {
if ( exists $files{$_} ) {
delete $files{$_};
} elsif ( ! -e $_ ) { # $_ might be absolute
carp "File not found: $_\n"
if $ENV{TEST_DEBUG};
$ok = 0;
}
}
for (keys %files) {
carp "Extra file found: $_\n"
if $ENV{TEST_DEBUG};
$ok = 0;
}
if ( $ok ) {
return 1;
} else {
return;
}
}
# -------------------------------------
=head2 evcheck
Eval code, return status
=over 4
=item ARGUMENTS
=over 4
=item code
Coderef to eval
=item name
Name to use in error messages
=back
=item RETURNS
=over 4
=item okay
1 if eval was okay, 0 if not.
=back
=back
=cut
sub evcheck {
my ($code, $name) = @_;
my $ok = 0;
eval {
&$code;
$ok = 1;
}; if ( $@ ) {
carp "Code $name failed: $@\n"
if $ENV{TEST_DEBUG};
$ok = 0;
}
return $ok;
}
# -------------------------------------
=head2 save_output
Redirect a filehandle to temporary storage for later examination.
=over 4
=item ARGUMENTS
=over 4
=item name
Name to store as (used in L<restore_output>)
=item filehandle
The filehandle to save
=back
=cut
# Map from names to saved filehandles.
# Values are arrayrefs, being filehandle that was saved (to restore), the
# filehandle being printed to in the meantime, and the original filehandle.
# This may be treated as a stack; to allow multiple saves... push & pop this
# stack.
my %grabs;
sub save_output {
croak sprintf("%s takes 2 arguments\n", (caller 0)[3])
unless @_ == 2;
my ($name, $filehandle) = @_;
my $tmpfh = do { local *F; *F; };
my $savefh = do { local *F; *F; };
my $tmpnam = POSIX::tmpnam;
sysopen $tmpfh, $tmpnam, O_RDWR | O_CREAT | O_EXCL;
unlink $tmpnam;
select((select($tmpfh), $| = 1)[0]);
open $savefh, '>&' . fileno $filehandle
or die "can't dup $name: $!";
open $filehandle, '>&' . fileno $tmpfh
or die "can't open $name to tempfile: $!";
push @{$grabs{$name}}, $savefh, $tmpfh, $filehandle;
}
# -------------------------------------
=head2 restore_output
Restore a saved filehandle to its original state, return the saved output.
=over 4
=item ARGUMENTS
=over 4
=item name
Name of the filehandle to restore (as passed to L<save_output>).
=back
=item RETURNS
=over 4
=item saved_string
A single string being the output saved.
=back
=cut
sub restore_output {
my ($name) = @_;
croak "$name has not been saved\n"
unless exists $grabs{$name};
croak "All saved instances of $name have been restored\n"
unless @{$grabs{$name}};
my ($savefh, $tmpfh, $origfh) = splice @{$grabs{$name}}, -3;
close $origfh
or die "cannot close $name opened to tempfile: $!";
open $origfh, '>&' . fileno $savefh
or die "cannot dup $name back again: $!";
seek $tmpfh, 0, 0;
local $/ = undef;
my $string = <$tmpfh>;
close $tmpfh;
return $string;
}
sub _test_save_restore_output {
warn "to stderr 1\n";
save_output("stderr", *STDERR{IO});
warn "Hello, Mum!";
print 'SAVED:->:', restore_output("stderr"), ":<-\n";
warn "to stderr 2\n";
}
# -------------------------------------
=head2 tmpnam
Very much like the one in L<POSIX> or L<File::Temp>, but does not get deleted
if TEST_DEBUG has SAVE in the value.
=over 4
=item ARGUMENTS
=over 4
=item name
I<Optional>. If defined, a name by which to refer to the tmpfile in user
messages.
=back
=item RETURNS
=over 4
=item filename
Name of temporary file.
=item fh
Open filehandle to temp file, in r/w mode. Only created & returned in list
context.
=back
=back
=cut
my @tmpfns;
BEGIN {
my $savewarn = $SIG{__WARN__};
# Subvert bizarre (& incorrect) subroutine redefined errors in 5.005_03
local $SIG{__WARN__} =
sub {
$savewarn->(@_)
if defined $savewarn and
UNIVERSAL::isa($savewarn,'CODE') and
$_[0] !~ /^Subroutine tmpnam redefined/;
};
*tmpnam = sub {
my $tmpnam = POSIX::tmpnam;
if (@_) {
push @tmpfns, [ $tmpnam, $_[0] ];
} else {
push @tmpfns, $tmpnam;
}
if (wantarray) {
sysopen my $tmpfh, $tmpnam, O_RDWR | O_CREAT | O_EXCL;
return $tmpnam, $tmpfh;
} else {
return $tmpnam;
}
}
}
END {
if ( defined $ENV{TEST_DEBUG} and $ENV{TEST_DEBUG} =~ /\bSAVE\b/ ) {
for (@tmpfns) {
if ( ref $_ ) {
printf "Used temp file: %s (%s)\n", @$_;
} else {
print "Used temp file: $_\n";
}
}
} else {
unlink map((ref $_ ? $_->[0] : $_), @tmpfns)
if @tmpfns;
}
}
# -------------------------------------
=head2 tempdir
Very much like the one in L<POSIX> or L<File::Temp>, but does not get deleted
if TEST_DEBUG has SAVE in the value (does get deleted otherwise).
=over 4
=item ARGUMENTS
I<None>
=item RETURNS
=over 4
=item name
Name of temporary dir.
=back
=back
=cut
my @tmpdirs;
sub tempdir {
my $tempdir = POSIX::tmpnam;
mkdir $tempdir, 0700
or die "Failed to create temporary directory $tempdir: $!\n";
if (@_) {
push @tmpdirs, [ $tempdir, $_[0] ];
} else {
push @tmpdirs, $tempdir;
}
return $tempdir;
}
END {
for (@tmpdirs) {
if ( ref $_ ) {
if ( defined $ENV{TEST_DEBUG} and $ENV{TEST_DEBUG} =~ /\bSAVE\b/ ) {
printf "Used temp dir: %s (%s)\n", @$_;
} else {
# Solaris gets narky about removing the pwd.
chdir File::Spec->rootdir;
rmtree $_->[0];
}
} else {
if ( defined $ENV{TEST_DEBUG} and $ENV{TEST_DEBUG} =~ /\bSAVE\b/ ) {
print "Used temp dir: $_\n";
} else {
# Solaris gets narky about removing the pwd.
chdir File::Spec->rootdir;
rmtree $_;
}
}
}
}
# -------------------------------------
=head2 compare
compare(+{ fn1 => $fn1, fn2 => $fn2, gzip => 1 });
This performs one test.
=over 4
=item ARGUMENTS
A single argument is taken, considered as a hash ref, with the following keys:
In TEST_DEBUG mode, if the files do not compare equal, outputs file info on
STDERR.
=over 4
=item fn1
B<Mandatory> File to compare
=item fn2
B<Mandatory> File to compare
=item name
B<Mandatory> Test name
=item sort
B<Optional> sort files prior to comparison. Requires the C<sort> command to
be on C<$PATH> (else skips).
=item gunzip
B<Optional> gunzip files prior to comparison. Requires the C<gzip> command to
be on C<$PATH> (else skips). gzip occurs prior to any sort.
=item untar
B<Optional> untar files prior to comparison. Requires the C<tar> command to
be on C<$PATH> (else skips). any gzip occurs prior to any tar. Tar files are
considered equal if they each contain the same filenames & each file contained
is equal. If the sort flag is present, each file is sorted prior to comparison.
=back
=back
=cut
#XYZ sub _run {
#XYZ my ($cmd, $name, $in) = @_;
#XYZ
#XYZ my $infn = defined $in ? tmpnam : '/dev/null';
#XYZ my $outfn = tmpnam;
#XYZ my $errfn = tmpnam;
#XYZ
#XYZ my $pid = fork;
#XYZ croak "Couldn't fork: $!\n"
#XYZ unless defined $pid;
#XYZ
#XYZ if ( $pid == 0 ) { # Child
#XYZ open STDOUT, '>', $outfn;
#XYZ open STDERR, '>', $errfn;
#XYZ open STDIN, '<', $infn;
#XYZ
#XYZ exec @$cmd;
#XYZ }
#XYZ
#XYZ my $rv = waitpid $pid, 0;
#XYZ my $status = $?;
#XYZ
#XYZ croak "Unexpected waitpid return from child $name: $rv (expected $pid)\n"
#XYZ unless $rv == $pid;
#XYZ
#XYZ local $/ = undef;
#XYZ local (OUT, ERR);
#XYZ open *OUT, '<', $outfn;
#XYZ open *ERR, '<', $errfn;
#XYZ my $out = <OUT>;
#XYZ my $err = <ERR>;
#XYZ close *OUT;
#XYZ close *ERR;
#XYZ
#XYZ return $status >> 8, $status & 127, $status & 128 , $out, $err
#XYZ }
# return codes and old-style call semantics left for backwards compatibility
BEGIN {
my $savewarn = $SIG{__WARN__};
# Subvert bizarre (& incorrect) subroutine redefined errors in 5.005_03
local $SIG{__WARN__} =
sub {
$savewarn->(@_)
if defined $savewarn and
UNIVERSAL::isa($savewarn,'CODE') and
$_[0] !~ /^Subroutine compare redefined/;
};
*compare = sub {
my ($fn1, $fn2, $sort) = @_;
my ($gzip, $tar, $name);
my $notest = 1;
if ( @_ == 1 and UNIVERSAL::isa($_[0], 'HASH') ) {
($fn1, $fn2, $name, $sort, $gzip, $tar, $notest) =
@{$_[0]}{qw( fn1 fn2 name sort gunzip untar notest )};
my @missing = grep ! defined $_[0]->{$_}, qw( fn1 fn2 name );
carp "Missing mandatory key(s): " . join(', ', @missing) . "\n"
if @missing;
}
my ($name1, $name2) = ($fn1, $fn2);
for ( grep ! defined, $fn1, $fn2 ) {
carp 'Usage: compare({fn1 => $fn1, fn2 => $fn2, name => "some name"})' ."\n"
if $ENV{TEST_DEBUG};
ok 0, 1, $name
unless $notest;
return -8;
}
{
my $err = 0;
for (0..1) {
my $fn = ($name1, $name2)[$_];
if ( ! -e $fn ) {
carp "Does not exist: $fn\n"
if $ENV{TEST_DEBUG};
$err |= 2 ** $_;
} elsif ( ! -r $fn ) {
carp "Cannot read: $fn\n"
if $ENV{TEST_DEBUG};
$err |= 2 ** $_;
}
}
if ( $err ) {
ok 0, 1, $name
unless $notest;
return -$err;
}
}
if ( $gzip ) {
unless ( find_exec('gzip') ) {
print "ok # Skip gzip not found in path\n";
return -16;
}
my $tmp1 = tmpnam;
my $tmp2 = tmpnam;
system "gzip $fn1 -cd > $tmp1"
and croak "gzip $fn1 failed: $?\n";
system "gzip $fn2 -cd > $tmp2"
and croak "gzip $fn2 failed: $?\n";
($fn1, $fn2) = ($tmp1, $tmp2);
}
if ( $tar ) {
unless ( find_exec('tar') ) {
print "ok # Skip tar not found in path\n";
return -16;
}
local $/ = "\n";
chomp (my @list1 = sort qx( tar tf $fn1 ));
croak "tar tf $fn1 failed with wait status: $?\n"
if $?;
chomp(my @list2 = sort qx( tar tf $fn2 ));
croak "tar tf $fn2 failed with wait status: $?\n"
if $?;
if ( @list2 > @list1 ) {
carp
sprintf("More files (%d) in $name2 than $name1 (%d)\n",
scalar @list2, scalar @list1)
if $ENV{TEST_DEBUG};
ok @list1, @list2, $name
unless $notest;
return 0;
} elsif ( @list1 > @list2 ) {
carp
sprintf("More files (%d) in $name1 than $name2 (%d)\n",
scalar @list1, scalar @list2)
if $ENV{TEST_DEBUG};
ok @list1, @list2, $name
unless $notest;
return 0;
}
for (my $i = 0; $i < @list1; $i++) {
if ( $list1[$i] lt $list2[$i] ) {
carp "File $list1[$i] is present in $name1 but not $name2\n"
if $ENV{TEST_DEBUG};
ok $list1[$i], $list2[$i], $name
unless $notest;
return 0;
} elsif ( $list1[$i] gt $list2[$i] ) {
carp "File $list2[$i] is present in $name2 but not $name1\n"
if $ENV{TEST_DEBUG};
ok $list2[$i], $list1[$i], $name
unless $notest;
return 0;
}
}
for my $fn (@list1) {
my $tmp1 = tmpnam;
my $tmp2 = tmpnam;
system "tar -xf $fn1 -O $fn > $tmp1"
and croak "tar -xf $fn1 -O $fn failed: $?\n";
system "tar -xf $fn2 -O $fn > $tmp2"
and croak "tar -xf $fn2 -O $fn failed: $?\n";
my $ok = compare({ fn1 => $tmp1,
fn2 => $tmp2,
sort => $sort,
notest => 1,
name =>
qq'Subcheck file "$fn" for compare $name1, $name2',
});
unless ( $ok >= 1 ) {
carp qq'Difference found testing file "$fn" in tars $name1 ($tmp1), $name2 ($tmp2)\n'
if $ENV{TEST_DEBUG};
ok 0, 1, $name
unless $notest;
return 0;
}
}
ok 1, 1, $name
unless $notest;
return 1;
}
if ( $sort ) {
unless ( find_exec('sort') ) {
print "ok # Skip sort not found in path\n";
return -16;
}
my $tmp1 = tmpnam;
my $tmp2 = tmpnam;
system sort => $fn1, -o => $tmp1
and croak "Sort $fn1 failed: $?\n";
system sort => $fn2, -o => $tmp2
and croak "Sort $fn2 failed: $?\n";
($fn1, $fn2) = ($tmp1, $tmp2);
}
unless ( File::Compare::compare($fn1, $fn2) ) {
ok 1, 1, $name
unless $notest;
return 1;
}
if ( $ENV{TEST_DEBUG} ) {
my $pid = fork;
die "Fork failed: $!\n"
unless defined $pid;
if ( $pid ) { # Parent
my $waitpid = waitpid($pid, 0);
die "Waitpid got: $waitpid (expected $pid)\n"
unless $waitpid == $pid;
} else { # Child
open *STDOUT{IO}, ">&" . fileno STDERR;
# Uniquify file names
my @args = keys %{+{ map {;$_=>1} $name1, $name2, $fn1, $fn2 }};
exec qw(ls -l), @args;
}
my $fh1 = IO::File->new($fn1, O_RDONLY)
or die "Couldn't open $fn1: $!\n";
my $fh2 = IO::File->new($fn2, O_RDONLY)
or die "Couldn't open $fn2: $!\n";
local $/ = "\n";
my $found = 0;
while ( ! $found and my $line1 = <$fh1> ) {
my $line2 = <$fh2>;
if ( ! defined $line2 ) {
print STDERR "$fn2 ended at line: $.\n";
$found = 1;
} elsif ( $line2 ne $line1 ) {
my $maxlength = max(map length($_), $line1, $line2);
my $minlength = min(map length($_), $line1, $line2);
my @diffchars = grep(substr($line1, $_, 1) ne substr($line2, $_, 1),
0..$minlength-1);
my $diff = ' ' x $minlength;
substr($diff, $_, 1) = '|'
for @diffchars;
my @extrachars, map((length($line1) > length($line2) ? '^' : 'v'),
$minlength..$maxlength-1);
$diff = join '', $diff, @extrachars;
my $diff_count = @diffchars;
my $extra_count = @extrachars;
print STDERR <<"END";
Difference at line $. ($diff_count characters differ) (top line is $extra_count chars longer):
$name1:
-->$line1<--
$diff
-->$line2<--
$name2:
Differing characters at positions @{[join ',',@diffchars]} (zero-based)
END
$found = 1;
}
}
if ( ! $found ) {
my $line2 = <$fh2>;
if ( defined $line2 ) {
print STDERR "$name1 ended before line: $.\n";
} else {
print STDERR "Difference between $name1, $name2 not found!\n";
}
}
close $fh1;
close $fh2;
}
ok 0, 1, $name
unless $notest;
return 0;
}
}
# -------------------------------------
=head2 check_req
Perform a requisite check on a given executable. This will skip if the
required modules are not present.
4+(n+m)*2 tests are performed, where n is the number of prerequisites
expected, and m is the number of outputs expected.
=over 4
=item SYNOPSIS
check_req('ccu-touch',
['/etc/passwd'],
[[REQ_FILE, '/etc/passwd']],
[[REQ_FILE, 'passwd.foo']],
'requisites 1');
=item ARGUMENTS
=over 4
=item cmd_name
The name of the command to run. It is assumed that this command is in
blib/script; hence it should be an executable in this package, and C<make>
shuold have been run recently.
=item args
The arguments to pass to the cmd_name, as an arrayref.
=item epres
The expected prerequisites, as an arrayref, wherein every member is a
two-element arrayref, the members being the requisite type, and the requisite
value.
=item eouts
The expected outputs, in the same format as the L<epres|"epres">.
=item testname
The name to use in error messages.
=back
=back
=cut
sub check_req {
my ($cmd_name, $args, $epres, $eouts, $testname) = @_;
eval "use Pipeline::DataFlow 1.03 qw( :req_types );";
my $skip;
if ( $@ ) {
print STDERR "$@\n"
if $ENV{TEST_DEBUG};
$skip = 'Skipped: Pipeline::DataFlow 1.03 not found';
} else {
$skip = 0;
}
my $count = 1;
my $test = sub {
my ($code, $expect) = @_;
my $name = sprintf "%s (%2d)", $testname, $count++;
my $value = UNIVERSAL::isa($code, 'CODE') ? $code->($name) : $code;
skip $skip, $value, $expect, $name;
};
# Initialize nicely to cope when read_reqs fails
my ($pres, $outs) = ([], []);
$test->(sub {
evcheck(sub {
($pres, $outs) = Pipeline::DataFlow->read_reqs
([catfile($Bin, updir, 'blib', 'script', $cmd_name),
@$args]);
}, $_[0]),},
1);
$test->(scalar @$pres, scalar @$epres);
my (@epres, @pres);
@epres = sort { $a->[1] cmp $b->[1] } @$epres;
@pres = sort { $a->[1] cmp $b->[1] } @$pres;
for (my $i = 0; $i < @epres; $i++) {
my ($type, $value) = @{$epres[$i]};
$test->($type, @pres > $i ? $pres[$i]->[0] : undef);
$test->($value, @pres > $i ? $pres[$i]->[1] : undef);
}
$test->(scalar @$outs, scalar @$eouts);
my (@eouts, @outs);
@eouts = sort { $a->[1] cmp $b->[1] } @$eouts;
@outs = sort { $a->[1] cmp $b->[1] } @$outs;
for (my $i = 0; $i < @eouts; $i++) {
my ($type, $value) = @{$eouts[$i]};
$test->($type, @outs > $i ? $outs[$i]->[0] : undef);
$test->($value, @outs > $i ? $outs[$i]->[1] : undef);
}
$test->(only_files([]), 1);
}
# -------------------------------------
=head2 find_exec
=over 4
=item ARGUMENTS
=over 4
=item proggie
The name of the program
=back
=item RETURNS
=over 4
=item path
The path to the first executable file with the given name on C<$PATH>. Or
nothing, if no such file exists.
=back
=back
=cut
# defined further up to use in constants
# -------------------------------------
=head2 read_file
=over 4
=item ARGUMENTS
=over 4
=item filename
B<Mandatory>
=item line-terminator
B<Optional>. Value of C<$/>. Defaults to C<"\n">.
=back
=item RETURNS
=over 4
=item lines
A list of lines in the file (lines determined by the value of
line-terminator), as an arrayref.
=back
=back
=cut
sub read_file {
my ($fn, $term) = @_;
$term = "\n"
unless defined $term;
my $fh = do { local *F; *F };
sysopen $fh, $fn, O_RDONLY;
local $/ = $term;
my @lines = <$fh>;
close $fh;
return \@lines;
}
# ----------------------------------------------------------------------------
=head1 EXAMPLES
Z<>
=head1 BUGS
Z<>
=head1 REPORTING BUGS
Email the author.
=head1 AUTHOR
Martyn J. Pearce C<fluffy@cpan.org>
=head1 COPYRIGHT
Copyright (c) 2001, 2002 Martyn J. Pearce. This program is free software; you
can redistribute it and/or modify it under the same terms as Perl itself.
=head1 SEE ALSO
Z<>
=cut
1; # keep require happy.
__END__