#!/usr/bin/perl -w
# $Id: tbfilt 1065 2018-11-04 11:32:06Z mueller $
#
# Copyright 2016-2018 by Walter F.J. Mueller <W.F.J.Mueller@gsi.de>
#
# This program is free software; you may redistribute and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3, 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 complete details.
#
#  Revision History:
# Date         Rev Version  Comment
# 2018-11-03  1065   1.1    add and use bailout; update exit code usage
# 2016-09-10   806   1.0    Initial version
# 2016-08-05   795   0.1    First draft
#

use 5.14.0;                                 # require Perl 5.14 or higher
use strict;                                 # require strict checking

use Getopt::Long;
use FileHandle;
use POSIX qw(strftime);

my %opts = ();

GetOptions(\%opts, "tee=s",  "pcom",
           "find=s", "all",
           "summary", "wide", "compact", "format=s", "nohead"
          )
  or bailout("bad command options");

sub conv_fd;
sub conv_ft;
sub conv_fs;
sub conv_fa;
sub conv_tr;
sub conv_tu;
sub conv_ts;
sub conv_tc;
sub conv_tg;
sub conv_st;
sub conv_ss;
sub conv_sc;
sub conv_sg;
sub conv_sp;
sub conv_sm;
sub conv_ec;
sub conv_pf;
sub conv_nf;
sub conv_ns;

my %fmttbl = (fd => {conv => \&conv_fd, head=>' file-date'},
              ft => {conv => \&conv_ft, head=>'    time'},
              fs => {conv => \&conv_fs, head=>' time'},
              fa => {conv => \&conv_fa, head=>'age'},
              tr => {conv => \&conv_tr, head=>'  time-real'},
              tu => {conv => \&conv_tu, head=>'  time-user'},
              ts => {conv => \&conv_ts, head=>'   time-sys'},
              tc => {conv => \&conv_tc, head=>'   time-cpu'},
              tg => {conv => \&conv_tg, head=>'       time t'},
              st => {conv => \&conv_st, head=>'stime(ns)'},
              ss => {conv => \&conv_ss, head=>'stime'},
              sc => {conv => \&conv_sc, head=>'  cycles'},
              sg => {conv => \&conv_sg, head=>' cyc|tim'},
              sp => {conv => \&conv_sp, head=>'sperf'},
              sm => {conv => \&conv_sm, head=>'MHz'},
              ec => {conv => \&conv_ec, head=>'err'},
              pf => {conv => \&conv_pf, head=>'stat'},
              nf => {conv => \&conv_nf, head=>'filename'},
              ns => {conv => \&conv_ns, head=>'filename'});
my @fmtlst;

my $format = $ENV{TBFILT_FORMAT};
$format = '%fd %fs %tr %tc %sc %ec %pf %nf' if $opts{wide};
$format = '%fa %tg %sg %ec %pf %ns' if $opts{compact};
$format = $opts{format} if defined $opts{format};
$format = '%ec %pf %nf' unless defined $format;

while (length($format)) {
  if ($format =~ m/^([^%]*)%([a-z][a-z])/) {
    my $pref = $1;
    my $code = $2;
    if (exists $fmttbl{$code}) {
      push @fmtlst, {pref => $pref,
                     conv => $fmttbl{$code}{conv},
                     head => $fmttbl{$code}{head}};
    } else { last; };
    $format = $';
  } else { last; };
}
bailout("bad format '$format'") if length($format);

autoflush STDOUT 1 if (-p STDOUT);

my $fh_tee;
if (defined $opts{tee} && $opts{tee} ne '') {
  $fh_tee = new FileHandle;
  $fh_tee->open($opts{tee},'>') or
    bailout("failed to open for write '$opts{tee}': $!");
}

my @flist = @ARGV;

# if find pattern has no '*', expand it
if (defined $opts{find}) {
  unless ($opts{find} =~ m/\*/) {
    $opts{find} = '.*/tb_.*_' . $opts{find} . '.*\.log';
  }
}

if (defined $opts{all}) {
  if (defined $opts{find}) {
    print STDERR "tbfilt-I: -find ignored because -all given\n";
  }
  $opts{find} = '.*/tb_.*_[bfsorept]sim(_.*)?\.log';
}

if (defined $opts{find}) {
  if (scalar (@flist)) {
    print STDERR "tbfilt-I: file names ignored because -all or -find given\n";
    @flist = ();
  }
  open FIND,'-|',"find -regextype egrep -regex '$opts{find}'"
    or bailout("failed to open find pipe: $!");

  while (<FIND>) {
    chomp;
    s|^\./||;                               # drop leading ./
    push @flist, $_;
  }

  close FIND;
  @flist = sort @flist;
  bailout("no files found by -find or -all") if (scalar (@flist) == 0);

} else {
  push @flist, '-' if (scalar(@flist) == 0);
}

my $manyfile = scalar(@flist) > 1;
my $notsumm  = not $opts{summary};
my %vals;
my $exitcode = 0;

if ($opts{summary} && (not $opts{nohead})) {
  foreach my $item (@fmtlst) {
    print $item->{pref};
    print $item->{head};
  }
  print "\n";
}

foreach my $fnam (@flist) {
  my $nfail = do_file($fnam);
  $exitcode = 2 if $nfail;
}

exit $exitcode;

#-------------------------------------------------------------------------------

sub bailout {
  my ($msg) = @_;
  print STDERR "tbfilt-F: $msg\n";
  exit 1;
}

#-------------------------------------------------------------------------------

sub do_file {
  my ($fnam) = @_;

  %vals = ();
  $vals{fnam}  = $fnam;
  $vals{nfail} = 0;

  my $fh;
  if ($fnam eq '-') {
    $fh = *STDIN;
  } else {
    $fh = new FileHandle;
    $fh->open($fnam,'<') or bailout("failed to open for read '$fnam': $!");
  }

  if ($manyfile && $notsumm) {
    print "-- $fnam";
    my $npad = 74-length($fnam);
    print ' '.('-' x $npad) if $npad > 0;
    print "\n";
  }

  while (<$fh>) {
    print $fh_tee $_ if defined  $fh_tee;
    chomp;
    my $show;
    my $fail;

    $fail = 1 if m/-[EF]:/;
    $fail = 1 if m/(ERROR|FAIL)/;
    $show = 1 if m/-W:/;
    $show = 1 if m/(PASS)/;
    $show = 1 if $opts{pcom} && m/^C/;      # show lines starting with C

    # ghdl reports or assertions (warning and higher)
    if (m/:\((report|assertion) (warning|error|failure)\):/) {
      # ignore ieee lib warnings at t=0
      next if /:\@0ms:\(assertion warning\): NUMERIC_STD.*metavalue detected/;
      next if /:\@0ms:\(assertion warning\): CONV_INTEGER: There is an 'U'/;
      next if /std_logic_arith.*:\@0ms:\(assertion warning\): There is an 'U'/;
      # ignore ' Simulation Finished' report failure (used to end ghdl sim)
      next if /:\(report failure\): Simulation Finished/;
      $fail = 1;
    }

    # check for DONE line accept
    #           920 ns: DONE                   -- tb'swithout clock
    #     7798080.0 ns 389893: DONE            -- single clock tb's
    #       56075.0 ns   2094: DONE-w          -- multiclock tb's  (max taken)
    #
    if (m/^\s*(\d+\.?\d*)\s+ns\s*(\d*):\s+DONE(-\S+)?\s*$/) {
      $show = 1;
      $vals{done_ns}  = $1;
      if ($2 ne '') {
        if (defined $vals{done_cyc}) {
          $vals{done_cyc} = $2 if $2 > $vals{done_cyc};
        } else {
          $vals{done_cyc} = $2;
        }
      }
    }

    # check for time line
    # Note: don't root the pattern with /^ --> allow arbitary text before
    #       the 'time' output. In practice 'time' output (to stderr by bash)
    #       and ghdl 'report' (also to stderr) get mixed and one might get
    #         tb_w11a_b3real 0m49.179s   user 0m0.993s   sys 0m0.293s
    #
    if (m/real\s+(\d*)m(\d+\.\d*)s\s+
          user\s+(\d*)m(\d+\.\d*)s\s+
           sys\s+(\d*)m(\d+\.\d*)s/x) {
      $show = 1;
      $vals{treal} = [$1,$2];
      $vals{tuser} = [$3,$4];
      $vals{tsys}  = [$5,$6];
    }

    print "$_\n" if ($show || $fail) && $notsumm;
    $vals{nfail} += 1 if $fail;
  }

  if (not defined $vals{done_ns}) {
    print "tbfilt-I: no DONE seen; FAIL\n" if $notsumm;
    $vals{nfail} += 1;
  }

  $vals{mtime} = ($fnam eq '-') ? time :  (stat($fh))[9];

  if ($opts{summary}) {
    foreach my $item (@fmtlst) {
      print $item->{pref};
      print &{$item->{conv}};
    }
    print "\n";
  }

  return $vals{nfail};
}

#-------------------------------------------------------------------------------
sub time_val {
  my ($tdsc) = @_;
  return undef unless defined $tdsc;
  return 60.*$tdsc->[0] + $tdsc->[1];
}

sub time_str {
  my ($tdsc) = @_;
  return '          -' unless defined $tdsc;
  return sprintf '%3dm%06.3fs', $tdsc->[0],$tdsc->[1];
}

sub time_sum {
  my ($tdsc1,$tdsc2) = @_;
  return undef unless defined $tdsc1 && defined $tdsc2;
  return time_val($tdsc1) + time_val($tdsc2);
}

sub gconv {
  my ($val) = @_;
  my $str = sprintf '%4.2f', $val;
  return substr($str,0,4);
}

#-------------------------------------------------------------------------------
sub conv_fd {
  return strftime "%F", localtime($vals{mtime});
}

sub conv_ft {
  return strftime "%T", localtime($vals{mtime});
}

sub conv_fs {
  return strftime "%H:%M", localtime($vals{mtime});
}

sub conv_fa {
  my $dt = time - $vals{mtime};
             return sprintf '%2ds', $dt if $dt < 99;
  $dt /= 60; return sprintf '%2dm', $dt if $dt < 99;
  $dt /= 60; return sprintf '%2dh', $dt if $dt < 60;
  $dt /= 24; return sprintf '%2dd', $dt if $dt < 99;
             return 'old';
}

sub conv_tr {
  return time_str($vals{treal});
}

sub conv_tu {
  return time_str($vals{tuser});
                }

sub conv_ts {
  return time_str($vals{tsys});
}

sub conv_tc {
  my $tsum = time_sum($vals{tuser}, $vals{tsys});
  return '          -' unless defined $tsum;
  my $min = int($tsum/60.);
  my $sec = $tsum - 60. * $min;
  return sprintf '%3dm%06.3fs', $min, $sec;
}

sub conv_tg {
  my $treal = time_val($vals{treal});
  my $tcpu  = time_sum($vals{tuser}, $vals{tsys});
  if (defined $treal && defined $tcpu && $tcpu > 0.4 * $treal) {
    return conv_tc() . ' c' ;
  } else {
    return conv_tr() . ((defined $treal) ? ' r': ' -');
  }
}

sub conv_st {
  return '        -' unless defined $vals{done_ns};
  return sprintf '%9d', $vals{done_ns};
}

sub conv_ss {
  return '    -' unless defined $vals{done_ns};
  my $stim = 0.001 * $vals{done_ns};
                  return gconv($stim) . 'u' if $stim < 999;
  $stim *= 0.001; return gconv($stim) . 'm' if $stim < 999;
  $stim *= 0.001; return gconv($stim) . 's';
}

sub conv_sc {
  return '       -' unless defined $vals{done_cyc};
  return sprintf '%8d', $vals{done_cyc};
}

sub conv_sg {
  return conv_sc() if defined $vals{done_cyc};
  return '   ' . conv_ss();
}

sub conv_sp {
  my $nc   = $vals{done_cyc};
  my $tsum = time_sum($vals{tuser}, $vals{tsys});
  return '    -' unless defined $nc && defined $tsum;
  my $sperf = 1000000. * $tsum / $nc;
                   return gconv($sperf) . 'u' if $sperf < 999;
  $sperf *= 0.001; return gconv($sperf) . 'm';
}

sub conv_sm {
  return '  -' unless defined $vals{done_ns} && $vals{done_ns} > 200 &&
                      defined $vals{done_cyc};
  my $mhz = (1000. * $vals{done_cyc}) / ($vals{done_ns} - 200);
  return sprintf '%3d', int($mhz+0.5);
}

sub conv_ec {
  return sprintf '%3d', $vals{nfail};
}

sub conv_pf {
  return $vals{nfail} ? 'FAIL' : 'PASS';
}

sub conv_nf {
  return $vals{fnam};
}

sub conv_ns {
  my $val = $vals{fnam};
  $val =~ s|^.*/||;
  return $val;
}
