#!/usr/bin/env perl

use Getopt::Long;
#use Data::Dumper;
use File::Basename;
use File::Spec;
use Cwd qw(abs_path);
use strict;

########################################
# Globals and Command Line options

my %opts = ('groups' => 'default',
            'master-directory' => 'fulltests',
	    'srcdir' => dirname("$0") . "/..",
            'builddir' => '..',
	    'failed-file' => 'failed_tests',
	   );

Getopt::Long::Configure(qw(no_ignore_case));
GetOptions(\%opts,
           "verbose",
           "help|?",
           "quiet|q",
           "groups|g=s",
           "r=s",
           "debug",
           "srcdir|D=s",
           "builddir|d=s",
	   "f",
	   "F",
	   "failed-file=s",
	   "master-directory=s",
	  ) || ++$opts{'help'};

# Change srcdir and builddir to absolute paths
$opts{'srcdir'} = abs_path($opts{'srcdir'});
$opts{'builddir'} = abs_path($opts{'builddir'});
# Set exeext.
$opts{'exeext'} = join(readpipe($opts{'builddir'} . '/net-snmp-config --exeext'));

usage() if ($opts{'help'});

# Build the harness object
my %args = (
	    verbosity => ($opts{'verbose'} ? 1 : ($opts{'quiet'} ? -1 : 0)),
	    exec => \&decide_exec,
	    # this option is *really* weird in how it works
	    failures => ($opts{'quiet'} ? 0 : ($opts{'verbose'} ? 0 : 1)),
	    errors => ($opts{'quiet'} ? 0 : 1),
	   );

# list of support infrastructure components
my %support;
my %sources;

# if the -d option was specified, pass on the source root directory to all apps
if (exists($opts{'master-directory'})) {
    $ENV{'NETSNMPSRCDIR'} = $opts{'master-directory'};
} else {
    $ENV{'NETSNMPSRCDIR'} = '.';
}

# pass srcdir and builddir to all apps
$ENV{'srcdir'} = $opts{'srcdir'};
$ENV{'builddir'} = $opts{'builddir'};

# set up MIBDIRS to refer to the src directory
if (!$ENV{'MIBDIRS'}) {
    $ENV{'MIBDIRS'} = "$opts{srcdir}/mibs";
}

my $cfp_path = File::Spec->catfile(dirname(abs_path($0)), "check_for_pskill");
my $rc = `$cfp_path`;
if ($rc != 0) {
    die "$cfp_path failed with error code $rc\n";
}

########################################
# Protection measures
$ENV{'SNMPCONFPATH'} = "/dev/null";

# create the testing harness infrastructure

my $harness;
if (eval { require TAP::Harness; } ) {
    import TAP::Harness;
    $harness = TAP::Harness->new(\%args);
} else {
    require Test::Harness;
    import Test::Harness;
    if ($opts{'groups'} ne 'default') {
	print STDERR "
ERROR: I can not find the perl TAP::Harness module.  We support the
more common Test::Harness module but only for the default test group.

Either:
  1) run only the default tests (i.e., just \"make test\")
  2) install the TAP::Harness perl module

";
	exit 1;
    }
    DIE("Test::Harness must be of version 1.21 or newer\n" .
	"Install the TAP::Harness module or use the RUNTESTS script\n")
      if $Test::Harness::VERSION < 1.21;
}


########################################
# gather the tests
my @tests;

DEBUG("Gathering and building tests:\n");
find_support();
if ($opts{'f'}) {
    DIE("The -f and -g options can not be both specified\n")
      if ($opts{'groups'} ne 'default');
    DIE("The -f and -r options can not be both specified\n") if ($opts{'r'});
    DIE("No $opts{'failed-file'} file was found to read failed state from\n")
      if (! -f $opts{'failed-file'});
    open(F, $opts{'failed-file'});
    while (<F>) {
	chomp;
	push @tests, build_test($_);
    }
} else {
    @tests = gather_tests($opts{'groups'}, $opts{'r'});
}

########################################
# rename them to remove parent dirs
@tests = rename_tests(@tests);

########################################
# run the tests

DEBUG("Running tests:\n"); 
DEBUG("-" x 78, "\n");

my $results;
if ($harness) {
    $results = $harness->runtests(@tests);
} else {
    # minimal backwards compat with Test::Harness
    run_older_perl_tests(@tests);
}

my @failed = $results->failed();
if (!$opts{'q'} && $#failed > -1) {
    print "\nWe failed these ", (1 + $#failed), " tests:\n";
    my @lines = @failed;
    map { if (exists($sources{$_})) { $_ = "$_ ( $sources{$_} )"} } @lines;
    print "  ", join("\n  ",@lines), "\n";
}

if (!$opts{'F'}) {
    open(F,">$opts{'failed-file'}");
    if ($#failed > -1) {
	print F join("\n", get_sources(@failed)) . "\n";
    }
    close(F);
}

exit($results->all_passed() ? 0 : 1);

######################################################################
# Infrastructure
#

########################################
# decides how we should execute a test
#
sub decide_exec {
    my ( $harness, $testfile ) = @_;

    # 1) Parse the $testfile argument.
    my ($dirname, $groupname, $basename, $app_extension, $file_extension) = 
      ($testfile =~ /([^\/]+)\/([^\/]+)\/([^\/]+)_([^\/_]+)\.*([^\/\.]*)$/);
    $app_extension =~ s/$opts{'exeext'}\$//;

    # 2) we have a RUN_TYPE file in the same directory
    if (exists($support{'run'}{$app_extension}{$groupname})) {
	return [$support{'run'}{$app_extension}{$groupname}, $testfile];
    }

    # 3) return a generic run script
    if (exists($support{'run'}{$app_extension}{'generic'})) {
	return [$support{'run'}{$app_extension}{'generic'}, $testfile];
    }

    # 4) give up and let the test harness decide itself
    return undef;
}

sub gather_tests {
    my ( $groups, $regexp ) = @_;
    my @groups;

    # figure out the list of groups we need to search through
    if ($groups eq 'all') {
	# find every group we can
	# we exclude:
	#  - things not a directory
	#  - anything with "template" in the name
	@groups =
	  grep { !/(template|support)/ &&
		   -d $_ && s/$opts{'srcdir'}\/testing\/$opts{'master-directory'}.// } glob("$opts{'srcdir'}/testing/$opts{'master-directory'}/*");
    } else {
	# they specified a comma separated list
	@groups = split(/,\s*/, $groups);
    }
    DEBUG("Checking groups: ", join(", ", @groups), "\n");

    my @tests;
    foreach my $group (@groups) {
	my @files;

	DEBUG("checking group $group\n");

	if (! -d "$opts{'srcdir'}/testing/$opts{'master-directory'}/$group") {
	    ERROR("group '$group' is not a directory under '$opts{'srcdir'}/testing/$opts{'master-directory'}'; ignoring\n");
	    next;
	}

	# push on all files that start with T[NUM]*
	push_or_skip(\@tests, $regexp, glob("$opts{'srcdir'}/testing/$opts{'master-directory'}/$group/T[0-9]*"));
    }

    return @tests;
}

sub push_or_skip {
    my ($array, $regexp, @files) = @_;
    foreach my $file (@files) {
	next if ($file =~ /.(bak|old|orig|rej)$/);
	next if ($file =~ /~$/);
	next if (defined($regexp) && $file !~ /$regexp/i);
	DEBUG("  Adding file $file\n");
	push @$array, build_test($file);
    }
}

# rename all the tests to remove the top subdir to help readability
sub rename_tests {
    my (@tests) = @_;
    my @newtests;

    # yes, I could have used map.  But I didn't.
    foreach my $file (@tests) {
	my $title = "$file";
	my $foundheader = 0;
	$title = $sources{$file} if (exists($sources{$file}));
	
	open(SRC, $title);
	while (<SRC>) {
	    if (/(HEADER|TITLE)\s+['"]*(.*)/) {
		$title = $2;
		$title =~ s/\s*\*\/.*//;
		$title =~ s/['"]$//;
		$foundheader = 1;
		last;
	    }
	}
	close(SRC);

	if (! $foundheader) {
	    $title =~ s/^$opts{'srcdir'}\/testing\///;
	    $title =~ s/$opts{'master-directory'}.//;
	}
	$sources{$title} = $sources{$file} || $file;
	push @newtests, [$file, $title];
    }
    return @newtests;
}

# called to possibly manipulate the list of tests to run by building some
sub build_tests {
    my (@tests) = @_;
    my @newtests;

    foreach my $test (@tests) {
	my $title;
	
	my $built = build_test($test);
	if (ref($built) eq 'ARRAY') {
	    push @newtests, @$built;
	} elsif ($built ne "") {
	    push @newtests, $built;
	}
    }
    return @newtests;
}

#
# Finds scripts that are used to build and run actual commands
#
sub find_builders {
    $support{'build'} = {};
    find_scripts('build', $support{'build'});
}

sub find_runners {
    $support{'run'} = {};
    find_scripts('run', $support{'run'});
}

sub find_support {
    find_builders();
    find_runners();
}

sub find_scripts {
    my ($buildname, $hashref) = @_;
    my $count;
    DEBUG("looking for $buildname scripts\n");
    foreach my $builder (glob("$opts{'srcdir'}/testing/$opts{'master-directory'}/*/*_${buildname}")) {
	next if ($builder =~ /~$/);
	next if ($builder =~ /.(bak|orig|rej|old)$/);

	my ($group, $type) = ($builder =~ /([^\/]+)\/([^\/]*)_${buildname}/);
	# save this as a certain group builder
	$hashref->{$type}{$group} = $builder;

	# save this as a generic builder if there isn't a better
	# generic one, such as one that exists in the support
	# directory.
	if (!exists($hashref->{$type}{'generic'}) || $group eq 'support') {
	    $hashref->{$type}{'generic'} = $builder;
	}
	$count++;
    }
    DEBUG("  found $count\n");
}

# called to build a test from a registerd builder
sub build_test {
    my ($testfile) = @_;

    my ($dirname, $groupname, $basename, $app_extension, $file_extension) = 
      ($testfile =~ /([^\/]+)\/([^\/]+)\/([^\/]+)_([^\/_]+)\.([^\/\.]+)$/);

    # is this even a buildable type recipe?
    if (!$dirname || !$basename || !$app_extension || !$file_extension) {
	return $testfile;
    }

    DEBUG("found: $testfile => $dirname, $basename, $app_extension, $file_extension\n");

    # choices:
    # 1) we have a registered subroutine to build an extension from
    # XXX

    # 2) we have a BUILD_TYPE file in the same directory
    if (exists($support{'build'}{$app_extension}{$dirname})) {
	return
	  call_build_script($support{'build'}{$app_extension}{$dirname}, $testfile);
    }

    # 3) return a generic build script
    if (exists($support{'build'}{$app_extension}{'generic'})) {
	return
	  call_build_script($support{'build'}{$app_extension}{'generic'}, $testfile);
    }

    # 4) we assume it's fine as is
    return $testfile;
}

sub call_build_script {
    my ($scriptname, $filename) = @_;

    my $maybenewfile = $filename;
    $maybenewfile =~ s/.[^\.]+$/$opts{'exeext'}/;
    $maybenewfile =~ s/T([^\/]+)$/B$1/;  # change prefix to B for 'Built'
    $maybenewfile =~ s/^$opts{'srcdir'}\///;

    my $newpath = $maybenewfile;
    $newpath =~ s/\/[^\/]*$//;

    if (! -d $newpath) {
	DEBUG("making directory $newpath\n");
	system("$opts{'srcdir'}/mkinstalldirs $newpath");
    }

    my $lastline;
    DEBUG("BUILDING: $scriptname $filename $maybenewfile\n");
    open(B,"$scriptname $filename $maybenewfile|");
    while (<B>) {
	$lastline = $_;
    }
    chomp($lastline);

    DEBUG("  result: $lastline\n");
    return undef if ($lastline eq 'fail');
    return undef if ($lastline eq 'skip');
    return $filename if ($lastline eq '');
    $sources{$lastline} = $filename;        # remember where we came from
    return $lastline;
}

sub get_sources {
    my (@names) = @_;
    map { if (exists($sources{$_})) { $_ = $sources{$_} } } @names;
    return @names;
}

sub run_older_perl_tests {
    #
    # Older versions of perl used a different test suite called Test::Harness
    # It is much more limited than TAP::Harness;
    #
    # Here we massage our older tests into something that will work under
    # Test::Harness too.
    #
    my @tests = @_;

    # create the temporary files
    my @tempfiles;
    if (! -d "$opts{'master-directory'}") {
	mkdir("$opts{'master-directory'}", 0777);
    }
    if (! -d "$opts{'master-directory'}/temptests") {
	mkdir("$opts{'master-directory'}/temptests", 0777);
    }
    foreach my $test (@tests) {
	my $tempfile = "$test->[0].t";
	$tempfile =~ s#^$opts{'srcdir'}#$opts{'builddir'}#;
	$tempfile =~ s#$opts{'master-directory'}/default/#$opts{'master-directory'}/temptests/#;
	open(T, ">$tempfile") || die("$tempfile: $!");
	print T "# functionally perl\n\nsystem(\"$opts{'srcdir'}/testing/fulltests/support/simple_run $test->[0]\");\n";
	close(T);
	chmod(0755, $tempfile);
	push @tempfiles, $tempfile;
    }

    $results = runtests(@tempfiles);

    unlink(@tempfiles) || die("$@ $!");
    exit;
}

# usage output
sub usage {
    print "$0 [OPTIONS]\n";

    print "\nOPTIONS:\n";
    print "  -v\t\t\tRuns in verbose mode; dumping all test output\n";
    print "    --verbose\n";
    print "  -q\t\t\tRuns in quieter mode; dumps less test output\n";
    print "    --quiet\n";
    print "  -g GROUP\t\tRuns the group of specified tests (default = 'default')\n";
    print "    --group GROUP\n";
    print "\t\t\t(use 'all' to run all tests)\n";
    print "  -r REGEXP\t\tOnly run test files matching this regexp\n";
    print "  -f\t\t\tRun only the failed tests from the last run\n";
    print "  -F\t\t\tDon't create the failed_tests file\n";
    print "  --failed-file FILE\tThe location of the failed state file\n";
    print "  -D PATH\t\tSource directory\n";
    print "    --srcdir PATH\n";
    print "    (currently '$opts{'srcdir'}')\n";
    print "  -d PATH\t\tBuild directory to be tested\n";
    print "    --builddir PATH\n";
    print "    (currently '$opts{'builddir'}')\n";
    print "  --master-directory DIRNAME\n";
    print "    (default = 'fulltests')\n";
    print "  -h\t\t\tThis help output\n";
    print "    --help\n";
    print "  --debug\t\tDebugging output\n\n";
    exit;
}

sub DEBUG {
    return if (!$opts{'debug'});
    print @_;
}

sub ERROR {
    print STDERR "Error:", @_;
}

sub DIE {
    ERROR(@_);
    exit 1;
}

=pod

=head1 NAME

runfulltests - the Net-SNMP test suite coordinator

=head1 SYNOPSIS

runfulltests [OPTIONS]

# ./RUNFULLTESTS

# ./RUNFULLTESTS -g all

=head1 DESCRIPTION

B<RUNFULLTESTS> is a TAP (see below) output aggregator and test suite
management program.  It runs groups of tests that it can find in the
I<fulltests> sub-directory.  It defaults to running a basic set of
high-level Net-SNMP tests found in the fulltests/default directory.
To run a different set of tests see the -g flag.

It is able to keep state and remember which tests failed so that
during development you can simply re-run only the "last failed tests"
using the -f switch.

=head2 Perl Requirements

Ideally it should be run under a fairly modern version of perl (eg,
5.10) but minmial support is provided for older versions of perl as
well.

If no perl is available on the system, there is also a fall-back
"RUNTESTS" suite which merely executes the default scripts for testing
compliance of the high-level applications.

=head2 Important Notes About Writing New Tests

When designing new tests it is strongly encouraged that some
conventions are followed:

  - Design the test files so they can be build/run without them
    needing to be build/run within a testing harness like this one
    (B<RUNFULLTESTS>).  IE, you should be able to run them directly by
    hand for debugging purposes without requiring them to be invoked
    within this or any other testing harness.
  - Name them beginning with TNNN where NNN is a 3 digit number

The rational behind these rules follows in the sections below

=head1 OPTIONS

=over

=item -g GROUP

=item --group GROUP

By default the "default" group of tests is run.  Which is really just
everything that B<RUNFULLTESTS> can find from the I<fulltests/default>
sub-directory.  The -g switch can be used to specify other
sub-directories of tests to run.  The argument is a comma-separated
list of subdirectories to use.

The special keyword I<all> can be used to run every test in every
subdirectory of the I<fulltests> directory.

=item -r REGEXP

Only run test files that match the I<REGEXP> regular expression.

To run only tests of a certain file name you might combine this with
'-g all'.  E.G., -g all -r snmpv3 will run all snmpv3 (named) tests
that B<RUNFULLTESTS> can find.

=item -f

Only run the tests that failed from the last run.

=item --failed-file FILE

Where to store the state of which tests have failed.

=item -F

Don't save state to the failed-file.

=item -D PATH

=item --srcdir PATH

If RUNFULLTESTS is being executed from a build directory other than
where the source files are located, this flag can be used to specify
where the Net-SNMP root source directory is found.

=item -d PATH

=item --builddir PATH

Specifies the root of the build directory.

=item --master-directory DIRNAME

Specifies an alternate master directory.  The default is "fulltests"

=item -v

=item --verbose

Turns on verbose output mode.

=item -q

=item --quiet

Turns on quiet output mode.

=item --debug

Turns on debugging output (which primarily shows how B<RUNFULLTESTS>
is collecting tests to run, etc).

=item -h

=item --help

Command line usage help output.

=back

=head1 TEST ARCHITECTURE

=head2 TAP output

TAP stands for "Test Anything Protocol".  TAP was originally
perl-specific but has been turning out to be a generic protocol for
testing just about anything.  It's heavily documented at:

  http://testanything.org/wiki/index.php/Main_Page

We're using TAP because it's highly flexible and separates the
invidual tests from the "collect and report" aspect.  RUNFULLTESTS is
simply a perl-based implementation for collecting and summarizing the
test results.  Other collection agents could be used instead of this
one, and any sort of test or language could be used for the individual
tests as well (in fact the default suite has some that are SH-based,
C-based, ...).

It may be that eventually the TAP protocol actually makes it into the
IETF (http://testanything.org/wiki/index.php/TAP_at_IETF:_Draft_Standard).

The syntax of TAP is very simple.  See the above web page for a
complete description, but this will provide you a minimal "getting
started" example and shows the output of 5 sub-tests run from a single
test application.

  1..5
  ok 1 - Yay
  ok 2 - Second part succeeded
  not ok 3 - Oh no...  A problem occurred.
  not ok 4 - The computer thought 2+2 was 5
  ok 5 - All is well that ends well

That's it.  Output from a test tool like that is auto-summarized by
this application in success/fail reports.

=head2 Testing Phases

The testing process goes through the following phases:

 - Test and Infrastructure Collection
 - Test Execution
   - Build if needed
   - Run

=head2 Test Collection

B<RUNFULLTESTS> will search all the requested groups for files that
begin with the letter 'T'.  They are executed in alphabetical order
within their group.  Convention is to number test files with a three
digit (zero-padded) number after the 'T' letter to ensure ordering is
as expected.

Files to be collected by B<RUNFULLTESTS> are made up of a number of
components to support a flexible build and execution system (discussed
in detail in the following sections).  They are structured as follows:

  T<NNN><NAME>_<TYPE>.<SUFFIX>

Where:

  NNN:      Is the 3 digit number mentioned above.
  NAME:     The filename of the test describing what it's about
  TYPE:     The internal "type" of the test, which is used later in building
            and execution (see below).
  .SUFFIX:  An optional file-type suffix

Examples:

  fulltests/default/T001snmpv1get_simple
  fulltests/snmpv3/T040keymanagetest_capp.c

Any other files are ignored in terms of tests and may be supplimental
to the above build systems.  (Supporting files, by convention, begin
with a capital 'S').

=head3 Full Title

Within the file there may be a line containing "HEADER ..." that will
be examined for a better title of the test.  Anything before "HEADER"
will be ignored, and the special "*/" will be replaced as well.  For
example, these are valid header source lines:

  # HEADER A cool test
  /* HEADER A cool test from my C-based source file */

=head2 Infrastructure Collection

In addition to test files, I<infrastructure> files are searched for
and remembered for later use (again, see below).  These files will be
of the form:

  <TYPE>_<USEAGE>

Where:

  TYPE:      The type name matching the file to support.

  USAGE:     How this file should be used.  Currently should be either
            I<build> or I<run> as described below.

Example files

  fulltests/support/clib_build
  fulltests/support/simple_run

Infrastructure files may exist in the source directory of where they're
expected to be run (ie, parallel to the test files) or they may exist
in the special "support" directory if they're expected to be
generically used across multilpe test group types.

=head2 Test Execution

Tests are run in two phases using the following pseudo-algorithm:

  + for each test file
    + if an appropriate TYPE_build file exists for a test {
      + run "TYPE_build TESTFILE"
      + record the last line as the new TESTFILE to run
    }

    + if an apporpriate TYPE_run file exists for a test {
      + run "TYPE_run TESTFILE"
      + collect it's output as the TAP output
    } else {
      + run "TESTFILE"
      + collect it's output as the TAP output
    }

For example, if the following files existed:

  fulltests/examplres/T001testing_examp
  fulltests/examplres/examp_build
  fulltests/examplres/examp_run

Then the following would be the rough execution:

  newfile = `fulltests/examplres/examp_build \
             fulltests/examplres/T001testing_examp | tail -1`
  fulltests/examplres/examp_run $newfile

=head1 TEST TYPES

Net-SNMP testing comes with a number of test suite "builders" and
"runners" that are useful for developing new tests.  These are
documented here:

=over

=item simple

I<simple> test files are simple sh-shell-script files used to test
high-level functionality of Net-SNMP tools.  They're easy to write and
should generally contain the following sort of structure:

  . ../support/simple_eval_tools.sh
  HEADER my name
  STARTAGENT
  CAPTURE "snmpget..."
  CHECK "for this string"
  STOPAGENT
  FINISHED

Example file: fulltests/default/T001snmpv1get_simple

=item capp

I<capp> files are fundamentally full C-source-code applications that
are built and linked against the libnetsnmp library.  Thus, a file
named I<T001mytest_capp.c> is compiled using the same compiler used to
compile Net-SNMP and linked against the required libraries for a basic
Net-SNMP application.  It should, of course, produce TAP output after
it's compiled and run.

Example file: fulltests/snmpv3/T010scapitest_capp.c

=item clib

I<clib> files are simple C-source-code files that are wrapped into a
main() application with appropriate #include files, etc.  I<clib>
files are designed primarly to write quick unit-tests for the Net-SNMP
core library.

Example file: fulltests/unit-tests/T001defaultstore_clib.c

=item Write your own!

This test system is designed to be flexible and expandable if the
basic architecture is followed.  The goal is to make it easy to create
very simple test suites or complex unit-tests or anything in between.

=back

=head1 DEBUGGING BROKEN TESTS

If the individual tests are designed well, you should be able to
re-run individual tests outside of the B<RUNFULLTESTS> aggregation
environment using the appropriate _build and _run scripts as needed.
Test writers are encouraged to output comments in their TAP output to
help users debug the results.

=head1 Author

Original architecture: Wes Hardaker <hardaker@users.sourceforge.net>

=cut

