#!/usr/bin/perl -w
# Copyright (c) 1996-2015 Sullivan Beck. All rights reserved.
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.

# SB_TEST.PL
###############################################################################
# HISTORY
#
# 1996-??-??  Wrote initial version for Date::Manip module
#
# 1996-2001   Numerous changes
#
# 2001-03-29  Rewrote to make it easier to drop in for any module.
#
# 2001-06-19  Modifications to make space delimited stuff work better.
#
# 2001-08-23  Added support for undef args.
#
# 2007-08-14  Better support for undef/blank args.
#
# 2008-01-02  Better handling of $runtests.
#
# 2008-01-24  Better handling of undef/blank args when arguements are
#             entered as lists instead of strings.
#
# 2008-01-25  Created a global $testnum variable to store the test number
#             in.
#
# 2008-11-05  Slightly better handling of blank/undef in returned values.
#
# 2009-09-01  Added "-l" value to $runtests.
#
# 2009-09-30  Much better support for references.
#
# 2010-02-05  Fixed bug in passing tests as lists
#
# 2010-04-05  Renamed to testfunc.pl to avoid being called in a core module

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

use Storable qw(dclone);

# Usage: test_Func($funcref,$tests,$runtests,@extra)=@_;
#
# This takes a series of tests, runs them, compares the output of the tests
# with expected output, and reports any differences.  Each test consists of
# several parts:
#    a function passed in as a reference ($funcref)
#    a series of arguments to be passed to the function
#    the expected output from the function call
#
# Tests may be passed in in two methods: as a string, or as a reference.
#
# Using the string case, $tests is a newline delimited string.  Each test
# takes one or more lines of the string.  Tests are separated from each
# other by a blank line.
#
# Arguments and return value(s) may be written as a single line:
#    ARG1 ARG2 ... ARGn ~ VAL1 VAL2 ... VALm
# or as multiple lines:
#    ARG1
#    ARG2
#    ...
#    ARGn
#    ~
#    VAL1
#    VAL2
#    ...
#    VALm
#
# If any of the arguments OR values have spaces in them, only the multiline
# form may be used.
#
# If there is exactly one return value, the separating tilde is
# optional:
#    ARG1 ARG2 ... ARGn VAL1
# or:
#    ARG1
#    ARG2
#    ...
#    ARGn
#    VAL
#
# It is valid to have a function with no arguments or with no return
# value (or both).  The "~" must be used:
#
#    ARG1 ARG2 ... ARGn ~
#
#    ~ VAL1 VAL2 ... VALm
#
#    ~
#
# Leading and trailing space is ignored in the multi-line format.
#
# If desired, any of the ARGs or VALs may be the word "_undef_" which
# will be strictly interpreted as the perl undef value. The word "_blank_"
# may also be used to designate a defined but empty string.
#
# They may also be (in the multiline format) of the form:
#
#   \ STRING           : a string reference
#
#   [] LIST            : a list reference (where LIST is a
#                        comma separated list)
#
#   [SEP] LIST         : a list reference (where SEP is a
#                        single character separator)
#
#   {} HASH            : a hash reference (where HASH is
#                        a comma separated list)
#
#   {SEP} HASH         : a hash reference (where SEP is a
#                        single character separator)
#
# Alternately, the tests can be passed in as a list reference:
#    $tests = [
#               [
#                 [ @ARGS1 ],
#                 [ @VALS1 ]
#               ],
#               [
#                 [ @ARGS2 ],
#                 [ @VALS2 ]
#               ], ...
#             ]
#
# @extra are extra arguments which are added to the function call.
#
# There are several ways to run the tests, depending on the value of
# $runtests.
#
# If $runtests is 0, the tests are run in a non-interactive way suitable
# for running as part of a "make test".
#
# If $runtests is a positive number, it runs runs all tests starting at
# that value in a way suitable for running interactively.
#
# If $runtests is a negative number, it runs all tests starting at that
# value, but providing feedback at each test.
#
# If $runtests is a string "=N" (where N is a number), it runs only
# that test.
#
# If $runtests is the string "-l", it lists the tests and the expected
# output without running any.

sub test_Func {
   my($funcref,$tests,$runtests,@extra)=@_;
   my(@tests);

   $runtests     = 0  if (! $runtests);
   my($starttest,$feedback,$endtest,$runtest);
   if      ($runtests eq "0"  or  $runtests eq "-0") {
      $starttest = 1;
      $feedback  = 1;
      $endtest   = 0;
      $runtest   = 1;
   } elsif ($runtests =~ /^\d+$/){
      $starttest = $runtests;
      $feedback  = 0;
      $endtest   = 0;
      $runtest   = 1;
   } elsif ($runtests =~ /^-(\d+)$/) {
      $starttest = $1;
      $feedback  = 1;
      $endtest   = 0;
      $runtest   = 1;
   } elsif ($runtests =~ /^=(\d+)$/) {
      $starttest = $1;
      $feedback  = 1;
      $endtest   = $1;
      $runtest   = 1;
   } elsif ($runtests eq "-l") {
      $starttest = 1;
      $feedback  = 1;
      $endtest   = 0;
      $runtest   = 0;
   } else {
      die "ERROR: unknown argument(s): $runtests";
   }

   my($tests_as_list) = 0;
   if (ref($tests) eq "ARRAY") {
      @tests   = @$tests;
      $tests_as_list = 1;

   } else {
      # Separate tests.

      my($comment)="#";
      my(@lines)=split(/\n/,$tests);
      my(@test);
      while (@lines) {
         my $line = shift(@lines);
         $line =~ s/^\s*//;
         $line =~ s/\s*$//;
         next  if ($line =~ /^$comment/);

         if ($line ne "") {
            push(@test,$line);
            next;
         }

         if (@test) {
            push(@tests,[ @test ]);
            @test=();
         }
      }
      if (@test) {
         push(@tests,[ @test ]);
      }

      # Get arg/val lists for each test.

      foreach my $test (@tests) {
         my(@tmp)=@$test;
         my(@arg,@val);

         # single line test
         @tmp = split(/\s+/,$tmp[0])  if ($#tmp == 0);

         my($sep)=-1;
         my($i);
         for ($i=0; $i<=$#tmp; $i++) {
            if ($tmp[$i] eq "~") {
               $sep=$i;
               last;
            }
         }

         if ($sep<0) {
            @val=pop(@tmp);
            @arg=@tmp;
         } else {
            @arg=@tmp[0..($sep-1)];
            @val=@tmp[($sep+1)..$#tmp];
         }
         $test = [ [@arg],[@val] ];
      }
   }

   my($ntest)=$#tests + 1;
   print "1..$ntest\n"  if ($feedback  &&  $runtest);

   my(@t);
   if ($endtest) {
      @t = ($starttest..$endtest);
   } else {
      @t = ($starttest..$ntest);
   }

   foreach my $t (@t) {
      $::testnum  = $t;

      my (@arg);
      if ($tests_as_list) {
         @arg     = @{ $tests[$t-1][0] };
      } else {
         my $arg  = dclone($tests[$t-1][0]);
         @arg     = @$arg;
         print_to_vals(\@arg);
      }

      my $argprt  = dclone(\@arg);
      my @argprt  = @$argprt;
      vals_to_print(\@argprt);

      my $exp     = dclone($tests[$t-1][1]);
      my @exp     = @$exp;
      print_to_vals(\@exp);
      vals_to_print(\@exp);

      # Run the test

      my ($ans,@ans);
      if ($runtest) {
         @ans = &$funcref(@arg,@extra);
      }
      vals_to_print(\@ans);

      # Compare the results

      foreach my $arg (@arg) {
         $arg = "_undef_"  if (! defined $arg);
         $arg = "_blank_"  if ($arg eq "");
      }
      $arg = join("\n           ",@argprt,@extra);
      $ans = join("\n           ",@ans);
      $exp = join("\n           ",@exp);

      if (! $runtest) {
         print "########################\n";
         print "Test     = $t\n";
         print "Args     = $arg\n";
         print "Expected = $exp\n";
      } elsif ($ans ne $exp) {
         print "not ok $t\n";
         warn "########################\n";
         warn "Args     = $arg\n";
         warn "Expected = $exp\n";
         warn "Got      = $ans\n";
         warn "########################\n";
      } else {
         print "ok $t\n"  if ($feedback);
      }
   }
}

# The following is similar but it takes input from an input file and
# sends output to an output file.
#
# $files is a reference to a list of tests.  If one of the tests is named
# "foobar", the input is from "foobar.in", output is to "foobar.out", and
# the expected output is in "foobar.exp".
#
# The function stored in $funcref is called as:
#    &$funcref($in,$out,@extra)
# where $in is the name of the input file, $out is the name of the output
# file, and @extra are any additional arguments that are required.
#
# The function should return 0 on success, or an error message.

sub test_File {
   my($funcref,$files,$runtests,@extra)=@_;
   my(@files)=@$files;

   $runtests=0  if (! $runtests);

   my($ntest)=$#files + 1;
   print "1..$ntest\n"  if (! $runtests);

   my(@t);
   if ($runtests > 0) {
      @t = ($runtests..$ntest);
   } elsif ($runtests < 0) {
      @t = (-$runtests);
   } else {
      @t = (1..$ntest);
   }

   foreach my $t (@t) {
      $::testnum = $t;
      my $test = $files[$t-1];
      my $expf = "$test.exp";
      my $outf = "$test.out";

      if (! -f $test  ||  ! -f $expf) {
         print "not ok $t\n";
         warn  "Test: $test: missing input/outpuf information\n";
         next;
      }

      my $err  = &$funcref($test,$outf,@extra);
      if ($err) {
         print "not ok $t\n";
         warn  "Test: $test: $err\n";
         next;
      }

      local *FH;
      open(FH,$expf)  ||  do {
         print "not ok $t\n";
         warn  "Test: $test: $!\n";
         next;
      };
      my @exp = <FH>;
      close(FH);
      my $exp = join("",@exp);
      open(FH,$outf)  ||  do {
         print "not ok $t\n";
         warn  "Test: $test: $!\n";
         next;
      };
      my @out = <FH>;
      close(FH);
      my $out = join("",@out);

      if ($out ne $exp) {
         print "not ok $t\n";
         warn  "Test: $test: output differs from expected value\n";
         next;
      }

      print "ok $t\n"  if (! $runtests);
   }
}

# Converts a printable version of arguments to actual arguments
sub print_to_vals {
   my($listref) = @_;

   foreach my $arg (@$listref) {
      next  if (! defined($arg));
      if ($arg eq "_undef_") {
         $arg = undef;

      } elsif ($arg eq "_blank_") {
         $arg = "";

      } elsif ($arg =~ /^\\\s*(.*)/) {
         $str = $1;
         $arg = \$str;

      } elsif ($arg =~ /^\[(.?)\]\s*(.*)/) {
         my($sep,$str) = ($1,$2);
         $sep = ","  if (! $sep);
         my @list = split(/\Q$sep\E/,$str);
         foreach my $e (@list) {
            $e = ""     if ($e eq "_blank_");
            $e = undef  if ($e eq "_undef_");
         }
         $arg = \@list;

      } elsif ($arg =~ /^\{(.?)\}\s*(.*)/) {
         my($sep,$str) = ($1,$2);
         $sep = ","  if (! $sep);
         my %hash = split(/\Q$sep\E/,$str);
         foreach my $key (keys %hash) {
            my $val = $hash{$key};
            $hash{$key} = undef  if ($val eq "_undef_");
            $hash{$key} = ""     if ($val eq "_blank_");
         }
         $arg = \%hash;
      }
   }
}

# Converts arguments to a printable version.
sub vals_to_print {
   my($listref) = @_;

   foreach my $arg (@$listref) {
      if (! defined $arg) {
         $arg = "_undef_";

      } elsif (! ref($arg)) {
         $arg = "_blank_"  if ($arg eq "");

      } else {
         my $ref = ref($arg);
         if      ($ref eq "SCALAR") {
            $arg = "\\ $$arg";

         } elsif ($ref eq "ARRAY") {
            my @list = @$arg;
            foreach my $e (@list) {
               $e = "_undef_", next   if (! defined($e));
               $e = "_blank_"         if ($e eq "");
            }
            $arg = join(" ","[",join(", ",@list),"]");

         } elsif ($ref eq "HASH") {
            %hash = %$arg;
            foreach my $key (keys %hash) {
               my $val = $hash{$key};
               $hash{$key} = "_undef_", next  if (! defined($val));
               $hash{$key} = "_blank_"        if ($val eq "_blank_");
            }
            $arg = join(" ","{",
                        join(", ",map { "$_ => $hash{$_}" }
                             (sort keys %hash)), "}");
            $arg =~ s/  +/ /g;
         }
      }
   }
}

1;
# Local Variables:
# mode: cperl
# indent-tabs-mode: nil
# cperl-indent-level: 3
# cperl-continued-statement-offset: 2
# cperl-continued-brace-offset: 0
# cperl-brace-offset: 0
# cperl-brace-imaginary-offset: 0
# cperl-label-offset: -2
# End:

