Edit

kc3-lang/automake/contrib/tap-driver.pl

Branch :

  • Show log

    Commit

  • Author : Yves Orton
    Date : 2024-06-15 18:18:35
    Hash : f3752617
    Message : automake: sort keys for reproducibility. Fixes https://bugs.gnu.org/25629. * bin/aclocal.in: sort keys for reproducibility. * bin/automake.in: likewise. * contrib/tap-driver.pl: likewise. * lib/Automake/Channels.pm: likewise. * lib/Automake/Condition.pm: likewise. * lib/Automake/DisjConditions.pm: likewise. * lib/Automake/Getopt.pm: likewise. * lib/Automake/Rule.pm: likewise. * THANKS: Add Yves. * NEWS: mention this.

  • contrib/tap-driver.pl
  • #! /usr/bin/env perl
    # Copyright (C) 2011-2024 Free Software Foundation, Inc.
    #
    # This program is free software; you can redistribute it and/or modify
    # it under the terms of the GNU General Public License as published by
    # the Free Software Foundation; either version 2, or (at your option)
    # any later version.
    #
    # This program is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU General Public License for more details.
    #
    # You should have received a copy of the GNU General Public License
    # along with this program.  If not, see <https://www.gnu.org/licenses/>.
    
    # As a special exception to the GNU General Public License, if you
    # distribute this file as part of a program that contains a
    # configuration script generated by Autoconf, you may include it under
    # the same distribution terms that you use for the rest of that program.
    
    # This file is maintained in Automake, please report
    # bugs to <bug-automake@gnu.org> or send patches to
    # <automake-patches@gnu.org>.
    
    # ---------------------------------- #
    #  Imports, static data, and setup.  #
    # ---------------------------------- #
    
    use warnings FATAL => 'all';
    use strict;
    use Getopt::Long ();
    use TAP::Parser;
    
    my $VERSION = '2013-12-24.15'; # UTC
    
    my $ME = "tap-driver.pl";
    
    my $USAGE = <<'END';
    Usage:
      tap-driver --test-name NAME --log-file PATH --trs-file PATH
                 [--expect-failure {yes|no}] [--color-tests {yes|no}]
                 [--enable-hard-errors {yes|no}] [--ignore-exit]
                 [--diagnostic-string STRING] [--merge|--no-merge]
                 [--comments|--no-comments] [--] TEST-COMMAND
    The '--test-name', '--log-file' and '--trs-file' options are mandatory.
    END
    
    my $HELP = "$ME: TAP-aware test driver for Automake testsuite harness." .
               "\n" . $USAGE;
    
    # Keep this in sync with 'lib/am/check.am:$(am__tty_colors)'.
    my %COLOR = (
      red => "\e[0;31m",
      grn => "\e[0;32m",
      lgn => "\e[1;32m",
      blu => "\e[1;34m",
      mgn => "\e[0;35m",
      brg => "\e[1m",
      std => "\e[m",
    );
    
    # It's important that NO_PLAN evaluates "false" as a boolean.
    use constant NO_PLAN => 0;
    use constant EARLY_PLAN => 1;
    use constant LATE_PLAN => 2;
    
    # ------------------- #
    #  Global variables.  #
    # ------------------- #
    
    my $testno = 0;     # Number of test results seen so far.
    my $bailed_out = 0; # Whether a "Bail out!" directive has been seen.
    my $parser;         # TAP parser object (will be initialized later).
    
    # Whether the TAP plan has been seen or not, and if yes, which kind
    # it is ("early" is seen before any test result, "late" otherwise).
    my $plan_seen = NO_PLAN;
    
    # ----------------- #
    #  Option parsing.  #
    # ----------------- #
    
    my %cfg = (
      "color-tests" => 0,
      "expect-failure" => 0,
      "merge" => 0,
      "comments" => 0,
      "ignore-exit" => 0,
    );
    
    my $test_script_name = undef;
    my $log_file = undef;
    my $trs_file = undef;
    my $diag_string = "#";
    
    Getopt::Long::GetOptions
      (
        'help' => sub { print $HELP; exit 0; },
        'version' => sub { print "$ME $VERSION\n"; exit 0; },
        'test-name=s' => \$test_script_name,
        'log-file=s' => \$log_file,
        'trs-file=s' => \$trs_file,
        'color-tests=s'  => \&bool_opt,
        'expect-failure=s'  => \&bool_opt,
        'enable-hard-errors=s' => sub {}, # No-op.
        'diagnostic-string=s' => \$diag_string,
        'comments' => sub { $cfg{"comments"} = 1; },
        'no-comments' => sub { $cfg{"comments"} = 0; },
        'merge' => sub { $cfg{"merge"} = 1; },
        'no-merge' => sub { $cfg{"merge"} = 0; },
        'ignore-exit' => sub { $cfg{"ignore-exit"} = 1; },
      ) or exit 1;
    
    # ------------- #
    #  Prototypes.  #
    # ------------- #
    
    sub add_test_result ($);
    sub bool_opt ($$);
    sub colored ($$);
    sub copy_in_global_log ();
    sub decorate_result ($);
    sub extract_tap_comment ($);
    sub finish ();
    sub get_global_test_result ();
    sub get_test_exit_message ();
    sub get_test_results ();
    sub handle_tap_bailout ($);
    sub handle_tap_plan ($);
    sub handle_tap_result ($);
    sub is_null_string ($);
    sub main (@);
    sub must_recheck ();
    sub report ($;$);
    sub setup_io ();
    sub setup_parser (@);
    sub stringify_result_obj ($);
    sub testsuite_error ($);
    sub trap_perl_warnings_and_errors ();
    sub write_test_results ();
    sub yn ($);
    
    # -------------- #
    #  Subroutines.  #
    # -------------- #
    
    sub bool_opt ($$)
    {
      my ($opt, $val) = @_;
      if ($val =~ /^(?:y|yes)\z/i)
        {
          $cfg{$opt} = 1;
        }
      elsif ($val =~ /^(?:n|no)\z/i)
        {
          $cfg{$opt} = 0;
        }
      else
        {
          die "$ME: invalid argument '$val' for option '$opt'\n";
        }
    }
    
    # If the given string is undefined or empty, return true, otherwise
    # return false.  This function is useful to avoid pitfalls like:
    #   if ($message) { print "$message\n"; }
    # which wouldn't print anything if $message is the literal "0".
    sub is_null_string ($)
    {
      my $str = shift;
      return ! (defined $str and length $str);
    }
    
    # Convert a boolean to a "yes"/"no" string.
    sub yn ($)
    {
      my $bool = shift;
      return $bool ? "yes" : "no";
    }
    
    TEST_RESULTS :
    {
      my (@test_results_list, %test_results_seen);
    
      sub add_test_result ($)
      {
        my $res = shift;
        push @test_results_list, $res;
        $test_results_seen{$res} = 1;
      }
    
      sub get_test_results ()
      {
        return @test_results_list;
      }
    
      # Whether the test script should be re-run by "make recheck".
      sub must_recheck ()
      {
        return grep { !/^(?:XFAIL|PASS|SKIP)$/ } (sort keys %test_results_seen);
      }
    
      # Whether the content of the log file associated to this test should
      # be copied into the "global" test-suite.log.
      sub copy_in_global_log ()
      {
        return grep { not $_ eq "PASS" } (sort keys %test_results_seen);
      }
    
      sub get_global_test_result ()
      {
        return "ERROR"
          if $test_results_seen{"ERROR"};
        return "FAIL"
          if $test_results_seen{"FAIL"} || $test_results_seen{"XPASS"};
        return "SKIP"
          if scalar keys %test_results_seen == 1 && $test_results_seen{"SKIP"};
        return "PASS";
      }
    
    }
    
    sub write_test_results ()
    {
      open RES, ">", $trs_file or die "$ME: opening $trs_file: $!\n";
      print RES ":global-test-result: " . get_global_test_result . "\n";
      print RES ":recheck: " . yn (must_recheck) . "\n";
      print RES ":copy-in-global-log: " . yn (copy_in_global_log) . "\n";
      foreach my $result (get_test_results)
        {
          print RES ":test-result: $result\n";
        }
      close RES or die "$ME: closing $trs_file: $!\n";
    }
    
    sub trap_perl_warnings_and_errors ()
    {
      $SIG{__WARN__} = $SIG{__DIE__} = sub
        {
          # Be sure to send the warning/error message to the original stderr
          # (presumably the console), not into the log file.
          open STDERR, ">&OLDERR";
          die @_;
        }
    }
    
    sub setup_io ()
    {
      # Redirect stderr and stdout to a temporary log file.  Save the
      # original stdout stream, since we need it to print testsuite
      # progress output. Save original stderr stream, so that we can
      # redirect warning and error messages from perl there.
      open LOG, ">", $log_file or die "$ME: opening $log_file: $!\n";
      open OLDOUT, ">&STDOUT" or die "$ME: duplicating stdout: $!\n";
      open OLDERR, ">&STDERR" or die "$ME: duplicating stdout: $!\n";
      *OLDERR = *OLDERR; # To pacify a "used only once" warning.
      trap_perl_warnings_and_errors;
      open STDOUT, ">&LOG" or die "$ME: redirecting stdout: $!\n";
      open STDERR, ">&LOG" or die "$ME: redirecting stderr: $!\n";
    }
    
    sub setup_parser (@)
    {
      local $@ = '';
      eval { $parser = TAP::Parser->new ({exec => \@_, merge => $cfg{merge}}) };
      if ($@ ne '')
        {
          # Don't use the error message in $@ as set by TAP::Parser, since
          # currently it's both too generic (at the point of being basically
          # useless) and quite long.
          report "ERROR", "- couldn't execute test script";
          finish;
        }
    }
    
    sub get_test_exit_message ()
    {
      my $wstatus = $parser->wait;
      # Watch out for possible internal errors.
      die "$ME: couldn't get the exit status of the TAP producer"
        unless defined $wstatus;
      # Return an undefined value if the producer exited with success.
      return unless $wstatus;
      # Otherwise, determine whether it exited with error or was terminated
      # by a signal.
      use POSIX qw (WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG);
      if (WIFEXITED ($wstatus))
      {
        return sprintf "exited with status %d", WEXITSTATUS ($wstatus);
      }
      elsif (WIFSIGNALED ($wstatus))
        {
          return sprintf "terminated by signal %d", WTERMSIG ($wstatus);
        }
      else
        {
          return "terminated abnormally";
        }
    }
    
    sub stringify_result_obj ($)
    {
      my $result_obj = shift;
      my $COOKED_PASS = $cfg{"expect-failure"} ? "XPASS": "PASS";
      my $COOKED_FAIL = $cfg{"expect-failure"} ? "XFAIL": "FAIL";
      if ($result_obj->is_unplanned || $result_obj->number != $testno)
        {
          return "ERROR";
        }
      elsif ($plan_seen == LATE_PLAN)
        {
          return "ERROR";
        }
      elsif (!$result_obj->directive)
        {
          return $result_obj->is_ok ? $COOKED_PASS: $COOKED_FAIL;
        }
      elsif ($result_obj->has_todo)
        {
          return $result_obj->is_actual_ok ? "XPASS" : "XFAIL";
        }
      elsif ($result_obj->has_skip)
        {
          return $result_obj->is_ok ? "SKIP" : $COOKED_FAIL;
        }
      die "$ME: INTERNAL ERROR"; # NOTREACHED
    }
    
    sub colored ($$)
    {
      my ($color_name, $text) = @_;
      return $COLOR{$color_name} . $text . $COLOR{'std'};
    }
    
    sub decorate_result ($)
    {
      my $result = shift;
      return $result unless $cfg{"color-tests"};
      my %color_for_result =
        (
          "ERROR" => 'mgn',
          "PASS"  => 'grn',
          "XPASS" => 'red',
          "FAIL"  => 'red',
          "XFAIL" => 'lgn',
          "SKIP"  => 'blu',
        );
      if (my $color = $color_for_result{$result})
        {
          return colored ($color, $result);
        }
      else
        {
          return $result; # Don't colorize unknown stuff.
        }
    }
    
    sub report ($;$)
    {
      my ($msg, $result, $explanation) = (undef, @_);
      if ($result =~ /^(?:X?(?:PASS|FAIL)|SKIP|ERROR)/)
        {
          $msg = ": $test_script_name";
          add_test_result $result;
        }
      elsif ($result eq "#")
        {
          $msg = " $test_script_name:";
        }
      else
        {
          die "$ME: INTERNAL ERROR"; # NOTREACHED
        }
      $msg .= " $explanation" if defined $explanation;
      $msg .= "\n";
      # Output on console might be colorized.
      print OLDOUT decorate_result ($result) . $msg;
      # Log the result in the log file too, to help debugging (this is
      # especially true when said result is a TAP error or "Bail out!").
      print $result . $msg;
    }
    
    sub testsuite_error ($)
    {
      report "ERROR", "- $_[0]";
    }
    
    sub handle_tap_result ($)
    {
      $testno++;
      my $result_obj = shift;
    
      my $test_result = stringify_result_obj $result_obj;
      my $string = $result_obj->number;
    
      my $description = $result_obj->description;
      $string .= " $description"
        unless is_null_string $description;
    
      if ($plan_seen == LATE_PLAN)
        {
          $string .= " # AFTER LATE PLAN";
        }
      elsif ($result_obj->is_unplanned)
        {
          $string .= " # UNPLANNED";
        }
      elsif ($result_obj->number != $testno)
        {
          $string .= " # OUT-OF-ORDER (expecting $testno)";
        }
      elsif (my $directive = $result_obj->directive)
        {
          $string .= " # $directive";
          my $explanation = $result_obj->explanation;
          $string .= " $explanation"
            unless is_null_string $explanation;
        }
    
      report $test_result, $string;
    }
    
    sub handle_tap_plan ($)
    {
      my $plan = shift;
      if ($plan_seen)
        {
          # Error, only one plan per stream is acceptable.
          testsuite_error "multiple test plans";
          return;
        }
      # The TAP plan can come before or after *all* the TAP results; we speak
      # respectively of an "early" or a "late" plan.  If we see the plan line
      # after at least one TAP result has been seen, assume we have a late
      # plan; in this case, any further test result seen after the plan will
      # be flagged as an error.
      $plan_seen = ($testno >= 1 ? LATE_PLAN : EARLY_PLAN);
      # If $testno > 0, we have an error ("too many tests run") that will be
      # automatically dealt with later, so don't worry about it here.  If
      # $plan_seen is true, we have an error due to a repeated plan, and that
      # has already been dealt with above.  Otherwise, we have a valid "plan
      # with SKIP" specification, and should report it as a particular kind
      # of SKIP result.
      if ($plan->directive && $testno == 0)
        {
          my $explanation = is_null_string ($plan->explanation) ?
                            undef : "- " . $plan->explanation;
          report "SKIP", $explanation;
        }
    }
    
    sub handle_tap_bailout ($)
    {
      my ($bailout, $msg) = ($_[0], "Bail out!");
      $bailed_out = 1;
      $msg .= " " . $bailout->explanation
        unless is_null_string $bailout->explanation;
      testsuite_error $msg;
    }
    
    sub extract_tap_comment ($)
    {
      my $line = shift;
      if (index ($line, $diag_string) == 0)
        {
          # Strip leading '$diag_string' from '$line'.
          $line = substr ($line, length ($diag_string));
          # And strip any leading and trailing whitespace left.
          $line =~ s/(?:^\s*|\s*$)//g;
          # Return what is left (if any).
          return $line;
        }
      return "";
    }
    
    sub finish ()
    {
      write_test_results;
      close LOG or die "$ME: closing $log_file: $!\n";
      exit 0;
    }
    
    sub main (@)
    {
      setup_io;
      setup_parser @_;
    
      while (defined (my $cur = $parser->next))
        {
          # Verbatim copy any input line into the log file.
          print $cur->raw . "\n";
          # Parsing of TAP input should stop after a "Bail out!" directive.
          next if $bailed_out;
    
          if ($cur->is_plan)
            {
              handle_tap_plan ($cur);
            }
          elsif ($cur->is_test)
            {
              handle_tap_result ($cur);
            }
          elsif ($cur->is_bailout)
            {
              handle_tap_bailout ($cur);
            }
          elsif ($cfg{comments})
            {
              my $comment = extract_tap_comment ($cur->raw);
              report "#", "$comment" if length $comment;
           }
        }
      # A "Bail out!" directive should cause us to ignore any following TAP
      # error, as well as a non-zero exit status from the TAP producer.
      if (!$bailed_out)
        {
          if (!$plan_seen)
            {
              testsuite_error "missing test plan";
            }
          elsif ($parser->tests_planned != $parser->tests_run)
            {
              my ($planned, $run) = ($parser->tests_planned, $parser->tests_run);
              my $bad_amount = $run > $planned ? "many" : "few";
              testsuite_error (sprintf "too %s tests run (expected %d, got %d)",
                                       $bad_amount, $planned, $run);
            }
          if (!$cfg{"ignore-exit"})
            {
              my $msg = get_test_exit_message ();
              testsuite_error $msg if $msg;
            }
        }
      finish;
    }
    
    # ----------- #
    #  Main code. #
    # ----------- #
    
    main @ARGV;
    
    # Local Variables:
    # perl-indent-level: 2
    # perl-continued-statement-offset: 2
    # perl-continued-brace-offset: 0
    # perl-brace-offset: 0
    # perl-brace-imaginary-offset: 0
    # perl-label-offset: -2
    # cperl-indent-level: 2
    # cperl-brace-offset: 0
    # cperl-continued-brace-offset: 0
    # cperl-label-offset: -2
    # cperl-extra-newline-before-brace: t
    # cperl-merge-trailing-else: nil
    # cperl-continued-statement-offset: 2
    # eval: (add-hook 'before-save-hook 'time-stamp)
    # time-stamp-start: "my $VERSION = "
    # time-stamp-format: "'%:y-%02m-%02d.%02H'"
    # time-stamp-time-zone: "UTC0"
    # time-stamp-end: "; # UTC"
    # End: