#! /usr/bin/env perl
# vim: ts=2 sw=2 sts=0 noexpandtab:
##########################################################
## This script is part of the Devel::NYTProf distribution
##
## Copyright, contact and other information can be found
## at the bottom of this file, or by going to:
## http://search.cpan.org/dist/Devel-NYTProf/
##
###########################################################
## $Id: test.pl 402 2008-08-11 20:14:45Z tim.bunce $
###########################################################
use warnings;
use strict;
use Carp;
use ExtUtils::testlib;
use Getopt::Long;
use Config;
use Test::More;
use Data::Dumper;
use Devel::NYTProf::Reader;
use Devel::NYTProf::Util qw(strip_prefix_from_paths);
$|=1;
# skip these tests when the provided condition is true
my %SKIP_TESTS = (
'test06' => ($] >= 5.008) ? 0 : "needs perl >= 5.8",
'test15' => ($] < 5.008) ? 0 : "needs perl < 5.8",
'test16' => ($] >= 5.010) ? 0 : "needs perl >= 5.10",
);
my %opts = (
profperlopts => '-d:NYTProf',
html => $ENV{NYTPROF_TEST_HTML},
);
GetOptions(\%opts,
qw/p=s I=s v|verbose d|debug html open profperlopts=s leave=i use_db_sub=i/
) or exit 1;
$opts{v} ||= $opts{d};
my $opt_perl = $opts{p};
my $opt_include = $opts{I};
my $opt_leave = $opts{leave};
my $opt_use_db_sub = $opts{use_db_sub};
my $profile_datafile = 'nytprof_t.out'; # non-default to test override works
# note some env vars that might impact the tests
$ENV{$_} && warn "$_=$ENV{$_}\n"
for qw(PERL5DB PERL5OPT PERL_UNICODE PERLIO);
if ($ENV{NYTPROF}) { # avoid external interference
warn "Existing NYTPROF env var value ($ENV{NYTPROF}) ignored for tests. Use NYTPROF_TEST env var if need be.\n";
$ENV{NYTPROF} = '';
}
# options the user wants to override when running tests
my %NYTPROF_TEST = map { split /=/, $_, 2 } split /:/, $ENV{NYTPROF_TEST}||'';
# but we'll force a specific test data file
$NYTPROF_TEST{file} = $profile_datafile;
chdir( 't' ) if -d 't';
my $tests_per_extn = { p => 1, rdt => 1, x => 3 };
s:^t/:: for @ARGV; # allow args to use t/ prefix
# *.p = perl code to profile
# *.rdt = result tsv data dump to verify
# *.x = result csv dump to verify (should change to .rcv)
my @tests = @ARGV ? @ARGV : sort <*.p *.rdt *.x>; # glob-sort, for OS/2
my @test_opt_leave = (defined $opt_leave) ? ($opt_leave) : (1, 0);
my @test_opt_use_db_sub = (defined $opt_use_db_sub) ? ($opt_use_db_sub) : (0, 1);
plan tests => 1 + number_of_tests(@tests) * @test_opt_leave * @test_opt_use_db_sub;
my $path_sep = $Config{path_sep} || ':';
if( -d '../blib' ){
unshift @INC, '../blib/arch', '../blib/lib';
}
my $bindir = (grep { -d } qw(./bin ../bin))[0];
my $nytprofcsv = "$bindir/nytprofcsv";
my $nytprofhtml = "$bindir/nytprofhtml";
my $perl5lib = $opt_include || join( $path_sep, @INC );
my $perl = $opt_perl || $^X;
# turn ./perl into ../perl, because of chdir(t) above.
$perl = ".$perl" if $perl =~ m|^\./|;
if($opts{v} ){
print "tests: @tests\n";
print "perl: $perl\n";
print "perl5lib: $perl5lib\n";
print "nytprofcvs: $nytprofcsv\n";
}
ok(-x $nytprofcsv, "Where's nytprofcsv?");
# run all tests in various configurations
for my $leave (@test_opt_leave) {
for my $use_db_sub (@test_opt_use_db_sub) {
run_all_tests( {
start => 'init',
leave => $leave,
use_db_sub => $use_db_sub,
} );
}
}
sub run_all_tests {
my ($env) = @_;
print "Running tests with options: { @{[ %$env ]} }\n";
for my $test (@tests) {
run_test($test, $env)
}
}
sub run_test {
my ($test, $env) = @_;
my %env = (%$env, %NYTPROF_TEST);
local $ENV{NYTPROF} = join ":", map { "$_=$env{$_}" } sort keys %env;
#print $test . '.'x (20 - length $test);
$test =~ / (.+?) \. (?:(\d)\.)? (\w+) $/x or do {
warn "Can't parse test filename '$test'";
return;
};
my ($basename, $fork_seqn, $type) = ($1, $2||0, $3);
SKIP: {
skip "$basename: $SKIP_TESTS{$basename}", number_of_tests($test)
if $SKIP_TESTS{$basename};
my $test_datafile = (profile_datafiles($profile_datafile))[ $fork_seqn ];
if ($type eq 'p') {
unlink_old_profile_datafiles($profile_datafile);
profile($test, $profile_datafile);
}
elsif ($type eq 'rdt') {
verify_data($test, $test_datafile);
}
elsif ($type eq 'x') {
my $outdir = "$basename.outdir";
mkdir $outdir or die "mkdir($outdir): $!" unless -d $outdir;
unlink <$outdir/*>;
verify_csv_report($test, $test_datafile, $outdir);
if ($opts{html}) {
run_command("$perl $nytprofhtml --file=$profile_datafile --out=$outdir");
run_command("open $outdir/*.html")
if $opts{open}; # possibly only useful on OS X
}
}
else {
warn "Unrecognized extension '$type' on test file '$test'\n"
unless $type eq 'new' or $type eq 'outdir'; # handy for "test.pl t/test01.*"
}
}
}
exit 0;
sub run_command {
my ($cmd) = @_;
print "NYTPROF=$ENV{NYTPROF}\n" if $opts{v} && $ENV{NYTPROF};
local $ENV{PERL5LIB} = $perl5lib;
my $ok;
if ($opts{v}) {
print "$cmd\n";
$ok = (system($cmd) == 0);
}
else {
open(RV, "$cmd |") or die "Can't execute $cmd: $!\n";
my @results = <RV>;
$ok = close RV;
}
warn "Error status $? from $cmd\n" if not $ok;
return $ok;
}
sub profile {
my ($test, $profile_datafile) = @_;
my $cmd = "$perl $opts{profperlopts} $test";
ok run_command($cmd), "$test should run ok";
}
sub verify_data {
my ($test, $profile_datafile) = @_;
my $profile = eval { Devel::NYTProf::Data->new( { filename => $profile_datafile }) };
if ($@) {
diag($@);
fail($test);
return;
}
$profile->normalize_variables;
dump_profile_to_file($profile, "$test.new");
my @got = slurp_file("$test.new");
my @expected = slurp_file($test);
is_deeply(\@got, \@expected, $test)
? unlink("$test.new")
: diff_files($test, "$test.new");
}
sub dump_data_to_file {
my ($profile, $file) = @_;
open my $fh, ">", $file or croak "Can't open $file: $!";
local $Data::Dumper::Indent = 1;
local $Data::Dumper::Sortkeys = 1;
print $fh Data::Dumper->Dump([$profile],['expected']);
return;
}
sub dump_profile_to_file {
my ($profile, $file) = @_;
open my $fh, ">", $file or croak "Can't open $file: $!";
$profile->dump_profile_data( {
filehandle => $fh,
separator => "\t",
} );
return;
}
sub diff_files {
# we don't care if this fails, it's just an aid to debug test failures
my @opts = split / /, $ENV{NYTPROF_DIFF_OPTS}||''; # e.g. '-y'
@opts = ('-u') unless @opts;
system("diff", @opts, @_);
}
sub verify_csv_report {
my ($test, $profile_datafile, $outdir) = @_;
# generate and parse/check csv report
# determine the name of the generated csv file
my $csvfile = $test;
# fork tests will still report using the original script name
$csvfile =~ s/\.\d\./.0./;
# foo.p => foo.p.csv is tested by foo.x
# foo.pm => foo.pm.csv is tested by foo.pm.x
$csvfile =~ s/\.x//;
$csvfile .= ".p" unless $csvfile =~ /\.p/;
$csvfile = "$outdir/${csvfile}-line.csv";
unlink $csvfile;
my $cmd = "$perl $nytprofcsv --file=$profile_datafile --out=$outdir";
ok run_command($cmd), "generate csv ok";
my @got = slurp_file($csvfile);
my @expected = slurp_file($test);
if ($opts{d}) {
print "GOT:\n";
print @got;
print "EXPECTED:\n";
print @expected;
print "\n";
}
my $index = 0;
foreach (@expected) {
if ($expected[$index++] =~ m/^# Version/) {
splice @expected, $index-1, 1;
}
}
my @accuracy_errors;
$index = 0;
my $limit = scalar(@got)-1;
while ($index < $limit) {
$_ = shift @got;
if (m/^# Version/) {
next;
}
# Ignore version numbers
s/^([0-9.]+),([0-9.]+),([0-9.]+),(.*)$/0,$2,0,$4/o;
my $t0 = $1;
my $c0 = $2;
my $tc0 = $3;
if (defined $expected[$index]
and 0 != $expected[$index] =~ s/^~([0-9.]+)/0/
and $c0 # protect against div-by-0 in some error situations
) {
push @accuracy_errors, "$test line $index: got $t0 expected ~$1 for time"
if abs($1 - $t0) > 0.2; # Test times. expected to be within 200ms
my $tc = $t0 / $c0;
push @accuracy_errors, "$test line $index: got $tc0 expected ~$tc for time/calls"
if abs($tc - $tc0) > 0.00002; # expected to be very close (rounding errors only)
}
push @got, $_;
$index++;
}
if ($opts{d}) {
print "TRANSFORMED TO:\n";
print @got;
print "\n";
}
is_deeply(\@got, \@expected, $test) or do {
spit_file("$test.new", join("", @got));
diff_files($test, "$test.new");
};
is(join("\n",@accuracy_errors), '', $test);
}
sub pop_times {
my $hash = shift||return;
foreach my $key (keys %$hash) {
shift @{$hash->{$key}};
pop_times($hash->{$key}->[1]);
}
}
sub number_of_tests {
my $total_tests = 0;
for (@_) {
next unless m/\.(\w+)$/;
my $tests = $tests_per_extn->{$1};
warn "Unknown test type '$1' for test file '$_'\n" if not defined $tests;
$total_tests += $tests if $tests;
}
return $total_tests;
}
sub slurp_file { # individual lines in list context, entire file in scalar context
my ($file) = @_;
open my $fh, "<", $file or croak "Can't open $file: $!";
return <$fh> if wantarray;
local $/ = undef; # slurp;
return <$fh>;
}
sub spit_file {
my ($file, $content) = @_;
open my $fh, ">", $file or croak "Can't open $file: $!";
print $fh $content;
close $fh or die "Error closing $file: $!";
}
sub profile_datafiles {
my ($filename) = @_;
croak "No filename specified" unless $filename;
my @profile_datafiles = glob("$filename*");
# sort to ensure datafile without pid suffix is first
@profile_datafiles = sort @profile_datafiles;
return @profile_datafiles; # count in scalar context
}
sub unlink_old_profile_datafiles {
my ($filename) = @_;
my @profile_datafiles = profile_datafiles($filename);
warn "Unlinking old @profile_datafiles\n"
if @profile_datafiles and $opts{v};
1 while unlink @profile_datafiles;
}
# vim:ts=2:sw=2