#!/usr/bin/perl -w # $Id: tbrun 1097 2018-12-29 11:20:14Z mueller $ # # Copyright 2016-2018 by Walter F.J. Mueller # # 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-12-29 1097 1.1.2 show correct 'found count' in summary message # 2018-11-16 1069 1.1.1 add --all # 2018-11-09 1066 1.1 add and use bailout; update exit code usage # 2017-04-15 875 1.0.1 add --help option # 2016-09-17 808 1.0 Initial version # 2016-08-09 796 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 YAML::XS; use Cwd; use IO::Select; use Time::HiRes qw(gettimeofday); my %opts = (); GetOptions(\%opts, "tag=s@", "exclude=s@", "mode=s", "all", "jobs=i", "tee=s", "tmax=i", "dry", "trace", "nomake", "norun", "rlmon", "rbmon", "bwait=i", "swait=i", "help" ) or bailout("bad command options"); my @tlist; my @olist; my @wlist; my %keys_include = ( include => { mode => 'm', ref => ''}, tag => { mode => 'o', ref => 'ARRAY'} ); my %keys_default = ( default => { mode => 'm', ref => 'HASH'} ); my %keys_defhash = ( tag => { mode => 'o', ref => 'ARRAY'}, mode => { mode => 'o', ref => ''} ); my %keys_itest = ( test => { mode => 'm', ref => ''}, tag => { mode => 'o', ref => 'ARRAY'}, mode => { mode => 'o', ref => ''} ); my $nseen = 0; my $ntest = 0; my $ndone = 0; my $nfail = 0; my $inicwd = getcwd(); my %gblvars; $gblvars{ise_modes} = '[bsft]sim,ISim_[bsft]sim'; $gblvars{ise_modes_noisim} = '[bsft]sim'; # when ISim not possible $gblvars{ise_modes_nossim} = 'bsim,ISim_bsim'; # when ssim not available # $gblvars{viv_modes} = '[bsor]sim,XSim_[bsorept]sim'; $gblvars{viv_modes_nossim} = 'bsim,XSim_bsim'; # when ssim not available if ($opts{help}) { print_usage(); exit 0; } autoflush STDOUT 1 if -p STDOUT || -t STDOUT; my $ticker_on = -t 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}': $!"); } if ($opts{all}) { bailout("-all not compatible with --tag options") if defined $opts{tag}; $opts{tag} = ['.*'] } $opts{tag} = ['default'] unless defined $opts{tag}; $opts{mode} = 'bsim' unless defined $opts{mode}; my %modecache; my @modelist; foreach (split /,/,$opts{mode}) { $_ .= '_bsim' if m/^[IX]Sim$/; push @modelist, $_; } push @ARGV, 'tbrun.yml' unless scalar( @ARGV); my @tagincl = setup_tagfilter($opts{tag}); my @tagexcl = setup_tagfilter($opts{exclude}); foreach my $fnam (@ARGV) { include_file($fnam); } $ntest = scalar(@tlist); unless ($ntest) { tpre(sprintf "tbrun-E: %d tests found, none selected\n", $nseen); exit 1; } if (defined $opts{jobs}) { run_tests_multi(); } else { run_tests_single(); } if (defined $opts{dry}) { tpr(sprintf "#tbrun-I: %d tests found, %d selected\n", $nseen,$ntest); } else { if ($nfail) { tpr(sprintf "tbrun-I: %d tests failed of %d tests executed\n",$nfail,$ndone); } else { tpr(sprintf "tbrun-I: all tests passed, %d tests selected from %d found\n", $ndone,$nseen); } } exit ($nfail ? 2 : 0); #------------------------------------------------------------------------------- sub setup_tagfilter { my ($targlist) = @_; return () unless defined $targlist; my @tagfiltlist; foreach my $targ (@$targlist) { my @tagfilt = map { "^($_)\$" } split /,/, $targ; push @tagfiltlist, \@tagfilt; } return @tagfiltlist; } #------------------------------------------------------------------------------- sub check_tagfilter { my ($tfiltlist,$tlist) = @_; foreach my $tfilt (@$tfiltlist) { # loop over filters my $fok = 1; foreach my $tfele (@$tfilt) { # loop over filter elements my $match = 0; foreach my $tag (@$tlist) { # loop over tags $match = $tag =~ m/$tfele/; # tag matchs filter element last if $match; } $fok = 0 unless $match; # filter missed if one element missed } return 1 if $fok; # return ok of one filter matched } return 0; # here if no filter matched } #------------------------------------------------------------------------------- sub check_modefilter { my ($mode,$mlist) = @_; unless (exists $modecache{$mlist}) { my %mh; foreach my $mi (split /,/,$mlist) { if ($mi =~ m/^(.*)\[([a-z]+)\](.*)$/) { foreach (split //,$2) { $mh{$1.$_.$3} = 1; } } else { $mh{$mi} = 1; } } $modecache{$mlist} = \%mh; } my $rmh = $modecache{$mlist}; return exists $$rmh{$mode}; } #------------------------------------------------------------------------------- sub include_file { my ($fnam) = @_; my $fdat = read_file($fnam); exit 1 unless defined $fdat; my $ylst = load_yaml($fdat, $fnam); exit 1 unless defined $ylst; my $oldcwd = getcwd(); if ($fnam =~ m|^(.*)/(.*)$|) { chdir $1 or bailout("chdir to '$1' failed with '$!'"); } my %defhash; foreach my $yele (@$ylst) { if (exists $yele->{include}) { handle_include($yele); } elsif (exists $yele->{default}) { handle_default($yele, \%defhash); } elsif (exists $yele->{test}) { handle_itest($yele, \%defhash); } else { tpr(sprintf "tbrun-E: unknown list element in '%s'\n found keys: %s\n", $fnam, join(',',sort keys %$yele)); exit 1; } } chdir $oldcwd or bailout("chdir to '$oldcwd' failed with '$!'"); return; } #------------------------------------------------------------------------------- sub read_file { my ($fnam) = @_; my $fh = new FileHandle; if (not open $fh, '<', $fnam) { my $err = $!; tpre(sprintf "tbrun-E: failed to open '%s'\n cwd: %s\n error: %s\n", $fnam, getcwd(), $err); return undef; } # nice trick to slurp the whole file into a variable my $fdat = do { local $/ = undef; <$fh>; }; close $fh; return $fdat; } #------------------------------------------------------------------------------- sub load_yaml { my ($fdat,$fnam) = @_; my $ylst; eval { $ylst = YAML::XS::Load($fdat); }; if ($@ ne '') { my $err = $@; tpre(sprintf "tbrun-E: failed to yaml load '%s'\n cwd: %s\n error: %s\n", $fnam, getcwd(), $err); return undef; } if (ref $ylst ne 'ARRAY') { tpre(sprintf "tbrun-E: top level yaml is not a list but '%s'\n", ref $ylst); return undef; } foreach my $yele (@$ylst) { if (ref $yele ne 'HASH') { tpre(sprintf "tbrun-E: second level yaml is not a hash '%s'\n", ref $yele); return undef; } } return $ylst; } #------------------------------------------------------------------------------- sub check_keys { my ($yele, $href) = @_; foreach my $keyele ( keys %$yele ) { if (not exists $href->{$keyele}) { tpre(sprintf "tbrun-E: unexpected key '%s'\n", $keyele); return 0; } my $ref = ref $yele->{$keyele}; if ($ref ne $href->{$keyele}->{ref}) { tpre(sprintf "tbrun-E: key '%s' is type'%s', expected '%s'\n", $keyele, $ref, $href->{$keyele}->{ref}); return 0; } } foreach my $keyref ( keys %$href ) { next if $href->{$keyref}->{mode} eq 'o'; if (not exists $yele->{$keyref}) { tpre(sprintf "tbrun-E: key '%s' missing\n", $keyref); return 0; } } return 1; } #------------------------------------------------------------------------------- sub lookup_var { my ($vnam,$hrefs) = @_; return $gblvars{$vnam} if exists $gblvars{$vnam}; if ($vnam =~ m/[A-Z][A-Z0-9_]*/) { return $ENV{$vnam} if exists $ENV{$vnam}; } tpre(sprintf "tbrun-E: can't replace '$vnam'\n"); exit 1; } #------------------------------------------------------------------------------- sub expand_vars { my ($txt,$hrefs) = @_; my $res = ''; while ($txt ne '') { if ($txt =~ m/\$\{([a-zA-Z][a-zA-Z0-9_]*)\}/) { my $vnam = $1; my $vrep = lookup_var($vnam, $hrefs); $res .= $`; $res .= $vrep; $txt = $'; } else { $res .= $txt; last; } } return $res; } #------------------------------------------------------------------------------- sub merge_lines { my ($txt) = @_; $txt =~ s|\s*\\\n\s*| |mg; chomp $txt; return $txt; } #------------------------------------------------------------------------------- sub merge_expand { my ($txt,$hrefs) = @_; return expand_vars(merge_lines($txt), $hrefs); } #------------------------------------------------------------------------------- sub key_or_def { my ($tag,$yele,$defhash) = @_; return $yele->{$tag} if exists $yele->{$tag}; return $defhash->{$tag} if exists $defhash->{$tag}; return undef; } #------------------------------------------------------------------------------- sub handle_include { my ($yele) = @_; check_keys($yele, \%keys_include) or exit 1; my $fnam = merge_expand($yele->{include}, undef); include_file($fnam); return; } #------------------------------------------------------------------------------- sub handle_default { my ($yele, $defhash) = @_; check_keys($yele, \%keys_default) or exit 1; check_keys($yele->{default}, \%keys_defhash) or exit 1; foreach my $key (keys %{$yele->{default}}) { $$defhash{$key} = $$yele{default}{$key}; } return; } #------------------------------------------------------------------------------- sub handle_itest { my ($yele, $defhash) = @_; check_keys($yele, \%keys_itest) or exit 1; $nseen += 1; my $tlist = key_or_def('tag', $yele, $defhash); if (defined $tlist) { return unless check_tagfilter(\@tagincl, $tlist); return if check_tagfilter(\@tagexcl, $tlist); } my $mlist = merge_expand(key_or_def('mode', $yele, $defhash), undef); foreach my $mode (@modelist) { next unless check_modefilter($mode, $mlist); my $ms = '_' . $mode; $ms =~ s/_bsim$//; $gblvars{ms} = $ms; my $test = merge_expand($yele->{test}, undef); # forward options for tbrun_tbw or tbrun_tbwrri commands if ($test =~ m/^\s*(tbrun_tbw|tbrun_tbwrri)\s+(.*)$/) { my $cmd = $1; my $rest = $2; $test = $cmd; $test .= ' --nomake' if $opts{nomake}; $test .= ' --norun' if $opts{norun}; if ($cmd eq 'tbrun_tbwrri') { $test .= ' --rlmon' if $opts{rlmon}; $test .= ' --rbmon' if $opts{rbmon}; $test .= ' --bwait '.$opts{bwait} if $opts{bwait}; $test .= ' --swait '.$opts{swait} if $opts{swait}; } $test .= ' ' . $rest; } my $tid = scalar(@tlist); my $tmsg = sprintf "t%03d - tags: ", $tid; $tmsg .= join ',',@$tlist if defined $tlist; my %titem; $titem{id} = $tid; $titem{cd} = getcwd(); $titem{test} = $test; $titem{tag} = $tlist; $titem{tmsg} = $tmsg; push @{$titem{locks}}, $titem{cd}; push @tlist, \%titem; delete $gblvars{ms}; } return; } #------------------------------------------------------------------------------- sub tpr { my ($txt) = @_; print $txt; print $fh_tee $txt if defined $fh_tee; return; } #------------------------------------------------------------------------------- sub tpre { my ($txt) = @_; print STDERR $txt; print $fh_tee $txt if defined $fh_tee; return; } #------------------------------------------------------------------------------- sub max { my ($a,$b) = @_; return ($a > $b) ? $a : $b; } #------------------------------------------------------------------------------- sub open_job_fh { my ($cmd) = @_; my $fh = new FileHandle; # add STDERR->STDOUT redirect (create sub shell of needed) $cmd = '(' . $cmd . ')' if $cmd =~ m/\n/g; $cmd .= ' 2>&1'; # open returns pid of created process in case an in or out pipe is created my $pid = open $fh, '-|', $cmd; # print "+++1 $pid\n"; if (not $pid) { my $err = $!; my $msg = sprintf "tbrun-E: failed to start '%s'\n cwd: %s\n error: %s\n", $cmd, getcwd(), $err; return (undef, undef, $msg); } return ($fh, $pid, undef); } #------------------------------------------------------------------------------- sub run_tests_single { my $drycd = ''; foreach my $titem (@tlist) { my $cdir = $titem->{cd}; my $test = $titem->{test}; chdir $inicwd or bailout("chdir to '$inicwd' failed with '$!'"); if ($opts{dry}) { if ($cdir ne $drycd) { tpr("#------------------------------------------------------------\n"); tpr("cd $cdir\n"); $drycd = $cdir; } tpr("#----------------------------------------\n"); tpr("# $titem->{tmsg}\n"); tpr("$test\n"); } else { tpr("#----------------------------------------\n"); tpr("# $titem->{tmsg}\n"); $ndone += 1; my $cmd = ''; $cmd .= "cd $cdir"; $cmd .= "\n"; $cmd .= "$test"; my ($fh,$pid,$msg) = open_job_fh($cmd); if (not defined $fh) { tpre($msg); } else { while (<$fh>) { print $_; } if (not close $fh) { my $err = $?; tpr(sprintf "tbrun-I: test FAILed with exit status %d,%d\n", ($err>>8), ($err&0xff)); $nfail += 1; } } } } if ($opts{dry}) { tpr("#------------------------------------------------------------\n"); tpr(sprintf "cd %s\n", $inicwd); } return; } #------------------------------------------------------------------------------- sub print_ticker { return unless $ticker_on; my ($rwlist) = @_; my $msg = ''; state $lastlength = 0; if (defined $rwlist) { my $time_now = gettimeofday(); $msg = '#-I: ' . join '; ', map { sprintf('t%03d: %dl %3.1fs', $_->{id}, $_->{nlines}, $time_now-$_->{tstart}) } @$rwlist; $msg = substr($msg,0,75) . ' ...' if length($msg) >79; unless (defined $opts{trace}) { my $suff = sprintf '(%dt,%dw,%do)', scalar(@tlist), scalar(@wlist), scalar(@olist); if (length($suff) + length($msg) + 1 <= 79) { $msg .= ' ' . $suff; } else { $msg = substr($msg,0,79-6-length($suff)) . ' ... ' . $suff; } } } my $newlength = length($msg); $msg .= ' ' x ($lastlength - $newlength) if $lastlength > $newlength; print $msg . "\r"; $lastlength = $newlength; return; } #------------------------------------------------------------------------------- sub print_jobs { while (defined $olist[0]->{exitcode}) { print_ticker(); my $titem = shift @olist; tpr("#----------------------------------------\n"); tpr("# $titem->{tmsg}\n"); tpr($titem->{out}); } return; } #------------------------------------------------------------------------------- sub print_trace { my ($titem) = @_; my $pref = ''; my $suff = sprintf '(%dt,%dw,%do)', scalar(@tlist), scalar(@wlist), scalar(@olist); if (defined $titem->{exitcode}) { $pref = ($titem->{exitcode}==0) ? 'pass ' : 'FAIL '; } else { $pref = 'start'; } my $txt = '#-I: ' . $pref . ' ' . $titem->{tmsg}; $txt .= ' ' . $suff; $txt .= "\n"; print_ticker(); tpr($txt); return; } #------------------------------------------------------------------------------- sub start_jobs { # initialize lock hash my %locks; foreach my $titem (@wlist) { foreach my $lock (@{$titem->{locks}}) { $locks{$lock} = 1; } } # look for suitable tasks for (my $i=0; $i < scalar(@tlist) && scalar(@wlist) < $opts{jobs}; ) { my $titem = $tlist[$i]; my $nlock = 0; foreach my $lock (@{$titem->{locks}}) { if ($locks{$lock}) { $nlock += 1; last; } } # suitable task found if ($nlock == 0) { my $cdir = $titem->{cd}; my $test = $titem->{test}; $ndone += 1; my $cmd = ''; if ($opts{dry}) { $cmd .= "cd $cdir"; $cmd .= "\n"; $cmd .= "perl -e 'select(undef, undef, undef, 0.2+1.6*rand( 1.))'"; $cmd .= "\n"; $cmd .= "echo \"cd $cdir\""; $cmd .= "\n"; $cmd .= "echo \"$test\""; } else { $cmd .= "cd $cdir"; $cmd .= "\n"; $cmd .= "$test"; } # start job my ($fh,$pid,$msg) = open_job_fh($cmd); if (not defined $fh) { $titem->{out} = $msg; $titem->{exitcode} = 1; print_trace($titem) if $opts{trace}; print_jobs(); } else { $titem->{fh} = $fh; $titem->{fd} = fileno($fh); $titem->{pid} = $pid; $titem->{out} = ''; $titem->{tstart} = gettimeofday(); $titem->{nlines} = 0; push @wlist, $titem; foreach my $lock (@{$titem->{locks}}) { $locks{$lock} = 1; } print_trace($titem) if $opts{trace}; } splice @tlist, $i, 1; # remove from tlist next; # and re-test i'th list element } # if ($nlock == 0) $i += 1; # inspect nexyt list element } # for (my $i=0; ... return; } #------------------------------------------------------------------------------- sub kill_job { my ($titem, $trun) = @_; my $pid = $titem->{pid}; my $pgid = getpgrp(0); my %phash; $titem->{killed} = $trun; # get process tree data (for whole user, no pgid filtering possible my $rank = 0; open PS,"ps -H -o pid,ppid,pgid,comm --user $ENV{USER}|"; while () { chomp; next unless m/^\s*(\d+)\s+(\d+)\s+(\d+)\s(.*)$/; my $cpid = $1; my $cppid = $2; my $cpgid = $3; my $cargs = $4; next unless $cpgid == $pgid; # only current process group next if $cargs =~ m/^\s*ps\s*$/; # skip the 'ps' process itself $phash{$cpid}->{ppid} = $cppid; $phash{$cpid}->{pgid} = $cpgid; $phash{$cpid}->{args} = $cargs; $phash{$cpid}->{rank} = $rank++; push @{$phash{$cppid}->{childs}}, $cpid; } close PS; # sanity check 1: own tbrun process should be included unless (exists $phash{$$}) { print_ticker(); printf "-E: tmax kill logic error: tbrun master pid not in phash\n"; return; } # sanity check 2: job to be killed should be child of master tbrun unless ($phash{$pid}->{ppid} == $$) { print_ticker(); printf "-E: tmax kill logic error: job not child of tbrun\n"; return; } # determine number of leading blanks in master tbrun line my $nstrip = 0; $nstrip = length($1) if ($phash{$$}->{args} =~ m/^(\s*)/); # recursively mark all childs of job master my @pids = ($pid); while (scalar(@pids)) { my $cpid = shift @pids; if (not exists $phash{$cpid}) { print_ticker(); printf "-E: tmax kill logic error: child pid not in phash\n"; return; } $phash{$cpid}->{kill} = 1; if (exists $phash{$cpid}->{childs}) { push @pids, @{$phash{$cpid}->{childs}}; } } # build list of pid to be killed, and trace message my @kpids; my @ktext; foreach my $cpid (sort {$phash{$a}->{rank} <=> $phash{$b}->{rank} } grep {$phash{$_}->{kill}} keys %phash) { push @kpids, $cpid; push @ktext, sprintf "# %6d %6d %6d %s", $cpid, $phash{$cpid}->{ppid}, $phash{$cpid}->{pgid}, substr($phash{$cpid}->{args}, $nstrip); } # print trace message, if selected if ($opts{trace}) { print_ticker(); printf "#-I: kill t%03d after %3.1fs, kill proccesses:\n", $titem->{id}, $trun, join("\n"); print "# pid ppid pgid command\n"; print join("\n",@ktext) . "\n"; } # and finally kill all processes of the job kill 'TERM', @kpids; return; } #------------------------------------------------------------------------------- sub run_tests_multi { @olist = @tlist; while (scalar(@tlist) || scalar(@wlist)) { # while something to do # start new jobs, if available and job slots free start_jobs(); my @fhlist = map { $_->{fh} } @wlist; my %fdhash; foreach my $titem (@wlist) { $fdhash{$titem->{fd}} = $titem; } my $sel = IO::Select->new(@fhlist); my $neof = 0; my $time_ticker = gettimeofday() + 0.1; while ($neof == 0) { my $wait_ticker = max(0.1, $time_ticker - gettimeofday() + 0.1); my @fhlist = $sel->can_read($wait_ticker); my $time_now = gettimeofday(); if ($time_now >= $time_ticker) { print_ticker(\@wlist); $time_ticker = $time_now + 0.9; } foreach my $fh (@fhlist) { my $fd = fileno($fh); my $titem = $fdhash{$fd}; my $buf = ''; my $nb = sysread $fh, $buf, 1024; # data read if ($nb) { $titem->{out} .= $buf; $titem->{nlines} += ($buf =~ tr/\n/\n/); # count \n in $buf # eof or error } else { if (defined $titem->{killed}) { $titem->{out} .= sprintf "tbrun-I: test killed after %3.1fs\n", $titem->{killed}; } if (not close $fh) { my $err = $?; $titem->{out} .= sprintf "tbrun-I: test FAILed with exit status %d,%d\n", ($err>>8), ($err&0xff); $nfail += 1; $titem->{exitcode} = $err; } else { $titem->{exitcode} = 0; } $neof += 1; for (my $i=0; $i < scalar(@wlist); $i++) { next unless $wlist[$i]->{fd} == $fd; splice @wlist, $i, 1; last; } print_trace($titem) if $opts{trace}; } } # foreach my $fh ... # handle tmax if (defined $opts{tmax}) { foreach my $titem (@wlist) { my $trun = $time_now - $titem->{tstart}; if ($trun > $opts{tmax}) { kill_job($titem, $trun) unless defined $titem->{killed}; } } } } # while ($neof == 0) # here if at least one job finished print_jobs(); } return; } # ---------------------------------------------------------------------------- sub bailout { my ($msg) = @_; print STDERR "tbrun-F: $msg\n"; exit 1; } # ---------------------------------------------------------------------------- sub print_usage { print "usage: tbrun [options] [dcsfile]\n"; print " --tag= list of tags to run (def='default')\n"; print " --exclude= tag combinations to exclude\n"; print " --mode= list of modes to run\n"; print " --jobs= number of parallel jobs (def=1)\n"; print " --tee= addition logfile for al stdout output\n"; print " --dry dry run, prints only the generated commands\n"; print " --trace prints additional information on job control\n"; print " --nomake don't execute make step of test bench\n"; print " --norun don't execute run step of test bench\n"; print " --rlmon enable the rlink monitor\n"; print " --rbmon enable the rbus monitor\n"; print " --bwait=
startup wait for behavioral simulations (in ns)\n"; print " --swait=
startup wait for post-* simulations (in ns)\n"; print " --help print short help text\n"; print "\n"; print " If no [dcsfile] is specified 'tbrun.yml' is used\n"; return; }