#!@PERL@ # -*-perl-*- # # aebisect - aegis regression detective # # Copyright (C) 2007 Ralph Smith. # # Portions shamelessly copied from: # # aeintegratq - aegis integration manager # Copyright (C) 2005-2008 Peter Miller. # # Copyright (C) 1998-2006 Endocardial Solutions, 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 3 of the License, 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 . # ################################################################# # # This script is designed to identify the culprit delta which # introduced a bug or made some other verifiable alteration in # project behavior. # # @configure_input@ # # Configure additions? my $TmpDir = "/var/tmp"; my $ProgramName = "aebisect"; require 5.004; use strict; use Getopt::Long; Getopt::Long::Configure( "auto_abbrev" ); $ENV{'SHELL'} = "/bin/sh"; # set signal handlers so stuff is cleaned up on kill $SIG{'INT'} = \&cleanup_and_quit; $SIG{'QUIT'} = \&cleanup_and_quit; $SIG{'TERM'} = \&cleanup_and_quit; sub usage { warn <<"EO_USAGE"; # Usage: # $ProgramName [options] [-b branch] -del delta1 [-b branch] -del delta2 \ -- command # Accepts options: # -p project_name - specify project name # -c change_num - specify change to use for building and testing # -dir directory - specify development directory # -h - show this information # -k - keep some working files (in a temporary directory) # -v - be verbose # -l logfile # -m - minimum builds # -n - skip the builds (i.e. test only needs source files) # -z - treat all results other than 0 as equivalent # # For the time being, deltas must be specified as numbers. # Use "-b -" to specify the trunk as a branch. EO_USAGE exit 1; } # required args my @branches; my @deltas; my $testcmd; # needed args with smart defaults my $proj = ''; my $change = -1; # assorted optional flags my $help = ''; my $Help = ''; my $Keep = ''; my $verbose = ''; my $LogFile = ''; my $minibuild = ''; my $nobuild = ''; my $zero_only = ''; my $devdir = ''; my $DebugMe = ''; # other globals my $mainbranch; my $trunk_name; my $cstate; my $TmpD; my $Tmp; my $aecmd; my $logf; # for temporary logfiles my @dlist; my @blist; # Convention: use warn and exit 1 for arglist errors, # use die and exit 2 for processing errors. # TODO?: # allow for "-d ${branch}.D${delta}" # allow for delta specified by name # second delta could default to current baseline sub next_delta { shift; my $val = shift; if ($#deltas > 0) { warn "$ProgramName: only two deltas may be specified\n"; exit 1; } @deltas = (@deltas, $val); if ($#branches < $#deltas) { $branches[$#deltas] = ""; } } # Note: because of perl conventions, our internals differ from # those of aecp. We use "-" for the trunk internally, whereas # an empty branch becomes that of the specified project. sub next_branch { shift; my $val = shift; if ($#branches > 0) { warn "$ProgramName: only two branches may be specified, " . "each before its delta\n"; exit 1; } @branches = (@branches, $val); } GetOptions( "help" => \$help, "keep" => \$Keep, "branch=s" => \&next_branch, "delta=i" => \&next_delta, "change=i" => \$change, "project=s" => \$proj, "verbose" => \$verbose, "logfile=s" => \$LogFile, "minimum" => \$minibuild, "nobuild" => \$nobuild, "zero_only" => \$zero_only, "directory=s" => \$devdir, "quick_debug" => \$DebugMe, # let's not tell them about this one, eh? ) || usage(); $testcmd = join(' ',@ARGV); # if they asked for help - just do it if ( $help ) { &usage; } if (! $testcmd) { warn "$ProgramName: no command specified.\n"; &usage; } # We would like use aesub to enjoy the usual context. # but if there are multiple open changes, aesub fails. if (! $proj) { if (defined($ENV{'AEGIS_PROJECT'})) { $proj = $ENV{'AEGIS_PROJECT'}; } } if (! $proj) { chomp($proj = `aesub \'\$proj\'`); if (! $proj) { warn "$ProgramName: you must give the project name explicitly " . "to this command\n"; exit 1; } } if ($change < 0) { chomp($change = `aesub -p $proj \'\${change number}\'`); if (! $change) { warn "$ProgramName: you must give the change explicitly " . "to this command\n"; exit 1; } } if ($#deltas != 1) { warn "$ProgramName: you must specify two deltas explicitly " . "to this command\n"; exit 1; } chomp($trunk_name = `aesub -p $proj -c $change \'\${proj trunk_name}\'`); $mainbranch = substr($proj,length($trunk_name)+1); if (! $mainbranch) { $mainbranch = "-"; } if ($trunk_name . "." . $mainbranch ne $proj) { warn "trunk=\"$trunk_name\" mainbranch=\"$mainbranch\" proj=\"$proj\"\n"; } if (! $branches[0]) { $branches[0] = $mainbranch; } if (! $branches[1]) { $branches[1] = $mainbranch; } chomp($cstate = `aesub -p $proj -c $change \'\${change state}\'`); #:# The change must be in the awaiting_development state. #:# This is because we will have to do aedbu later anyway, since #:# it is otherwise painful (and unreliable) to undo a "aecp -delta". if ($cstate ne "awaiting_development") { warn "$ProgramName: project \"$proj\": change \"$change\":" . "this change must be in the 'awaiting_development' state\n"; exit 1; } # define location for logging if (! $LogFile) { if ( defined( $ENV{'AEGIS_TEST_DIR'} ) ) { $LogFile = "$ENV{'AEGIS_TEST_DIR'}/aebisect.log"; }else{ $LogFile = "$ENV{'HOME'}/aebisect.log"; } warn "$ProgramName: logging to $LogFile\n"; } &write_log("Beginning bisection processing for project \"$proj\"" . " change \"$change\""); if ($verbose) { &write_log("main branch is $mainbranch, search from " . $branches[0] . ".D" . $deltas[0] . " to " . $branches[1] . ".D" . $deltas[1] . "\n" . " for change in result from command\n " . $testcmd); if ($zero_only) { &write_log("treating nonzero results as equivalent."); } } # now the real work starts $SIG{'__DIE__'} = \&cleanup_ere_dying; $TmpD = $TmpDir . "/bisect.$$"; mkdir $TmpD, 0777 or die "Can't create tempdir"; $Tmp = $TmpD . "/$$"; &write_log("Using $TmpD for temporary logs."); #:# We use the change inventory to review the project history. #:# This recurses properly through branches. $aecmd = "aegis -list -p $proj -ter change_inventory"; $logf = $Tmp . ".dlist"; die "Failed:$aecmd" if &system_cmd($aecmd, $logf); &clean_inventory($logf); if (! $Keep) { unlink $logf; } my $dcount = $#dlist +1; my $idx_lo = -1; my $idx_hi = -1; my $idx_try = -1; for (my $i=0; $i < $dcount; $i++) { if ($dlist[$i] == $deltas[0] && $blist[$i] eq $branches[0]) { $idx_lo = $i; } if ($dlist[$i] == $deltas[1] && $blist[$i] eq $branches[1]) { $idx_hi = $i; } } if ($idx_lo == -1 || $idx_hi == -1) { die "$ProgramName: Failed to find requested deltas in the inventory\n"; } if ($DebugMe) { &write_log("*** debug run - no real tests ***"); } my $idx_start = $idx_lo; # only used for debugging my $idx_end = $idx_hi; my $res_lo = $DebugMe ? &check_test_dbg($idx_lo): &check_test($idx_lo); my $res_hi = $DebugMe ? &check_test_dbg($idx_hi) : &check_test($idx_hi); my $res_try; die "$ProgramName: specified deltas do not bracket change in test result\n" unless ($res_lo != $res_hi); # at last we are ready to do the search while ($idx_hi - $idx_lo > 1) { if ($res_lo == $res_hi) { &write_log( "Puzzlement: test results identical for endpoints " . $blist[$idx_lo] . ".D" . $dlist[$idx_lo] . " and " . $blist[$idx_hi] . ".D" . $dlist[$idx_hi] . ""); die "$ProgramName: nonmonotonic test results\n"; } $idx_try = int(($idx_lo + $idx_hi) / 2); $res_try = $DebugMe ? &check_test_dbg($idx_try) : &check_test($idx_try); if ($res_try == $res_lo) { $idx_lo = $idx_try; } elsif ($res_try == $res_hi) { $idx_hi = $idx_try; } else { &write_log( "Confusion: test is not a dichotomy"); die "$ProgramName: test results inconsistent with binary search\n"; } } &write_log( "$ProgramName done.\nResults of the command\n $testcmd\n" . "changed between " . $blist[$idx_lo] . ".D" . $dlist[$idx_lo] . " and " . $blist[$idx_hi] . ".D" . $dlist[$idx_hi] . ""); print "Final bracketing deltas: " . $blist[$idx_lo] . ".D" . $dlist[$idx_lo] . " and " . $blist[$idx_hi] . ".D" . $dlist[$idx_hi] . "\n"; &cleanup_and_quit; ############ sub check_test_dbg { my $idx = shift; my $testres; # DEBUG: don't actually do anything in the working directories $testres = ($idx > int((3 * $idx_start + 2 * $idx_end) / 5)) ? 1 : 0; if ($verbose) { warn $ProgramName . ": " .$blist[$idx] . ".D" . $dlist[$idx] . " yields " . $testres . " idx=" . $idx . "\n"; } $testres; } #:# Each test is done in a clean development directory. #:# It is possible for the build to fail. #:# (For example, sometimes derived files from #:# the baseline are hard to get rid of automatically, #:# and these may poison the build.) #:# In this case, we bail out, leaving the dev. dir. open. #:# The clever user may clean things up, rebuild, perform #:# the test, and use the logfile to see how to proceed. # or he/she could read this and use the AEBISECT_DB_HOOK sub check_test { my $idx = shift; my $testres; my $delta_str = $blist[$idx] . ".D" . $dlist[$idx]; my $ddopt = ($devdir) ? " -dir $devdir" : ""; $logf = $Tmp . "." . $delta_str . ".log"; $aecmd = "aegis -db -p $proj -c $change" . $ddopt; if ($verbose) { &write_log($aecmd . " [$delta_str]"); } die "Failed:$aecmd" if &system_cmd($aecmd, $logf); my $bropt = ($blist[$idx] eq $mainbranch) ? "" : "-branch $blist[$idx]"; $aecmd = "aegis -cp -p $proj -c $change $bropt -delta $dlist[$idx] -bare ."; if ($verbose) { &write_log($aecmd . " [$delta_str]"); } die "Failed:$aecmd" if &system_cmd($aecmd, $logf); my $cwd; chomp ($cwd = `pwd`); my $wrk; # this must be done after aedb chomp($wrk = `aesub -p $proj -c $change \'\${DD}\'`); die "Failed:aecd" unless chdir $wrk; if (! $nobuild) { if (defined($ENV{'AEBISECT_DB_HOOK'})) { $aecmd = $ENV{'AEBISECT_DB_HOOK'}; if ( open(LOG, ">> $logf") ) { print LOG "$aecmd\n"; close(LOG); } if ($verbose) { &write_log("db_hook: $aecmd [$delta_str]"); } die "Failed:$aecmd" if &system_cmd($aecmd, $logf); } my $mflag = $minibuild ? "-mini" : ""; $aecmd = "aegis -build -p $proj -c $change $mflag"; if ($verbose) { &write_log($aecmd . " [$delta_str]"); } die "Failed:$aecmd" if &system_cmd($aecmd, $logf); } $testres = &system_cmd($testcmd, $logf); if ($verbose) { warn $ProgramName . ": " .$delta_str . " yields " . $testres . "\n"; } &write_log("Test command on " .$delta_str . " yields " . $testres); chdir $cwd; $aecmd = "aegis -dbu -p $proj -c $change"; if ($verbose) { &write_log($aecmd . " [$delta_str]"); } die "Failed:$aecmd" if &system_cmd($aecmd, $logf); if (!$Keep) { unlink $logf; } if ($zero_only && ($testres != 0)) { $testres = 1; } $testres; } sub clean_inventory { my $cinf = shift; my $count = 0; my $pbranch = ''; my $pdelta = ''; my $branch; my $delta; open(CIN, "< $cinf") or die "Unable to open $cinf:$!\n"; while () { ($branch,$delta) = /^([0-9\.]*)D([0-9]*)\s/; # trunk entries have no dot, otherwise strip $branch =~ s/\.$//; # aecp thinks 001 is a string, not a number $delta =~ s/^0+//; if (! $branch) { $branch = '-'; } # trunk # some changesets have multiple uuids for one delta, # so uniq'ify if (($branch ne $pbranch) || ($delta != $pdelta)) { $blist[$count] = $branch; $dlist[$count] = $delta; $pbranch = $branch; $pdelta = $delta; $count++; } } close(CIN); } # entry in the processing log as necessary # Errors are fatal since the whole point is to produce results. sub write_log { my $msg = shift; chop(my $date = `date +"%d %b %T"`); if ( open(LOG, ">> $LogFile") ) { print LOG "$date $msg\n"; close(LOG); } else { die "unable to open $LogFile:$!\n"; } } sub system_cmd { my($cmdstring, $resfile) = @_; my $sysres; my $logfile = ($resfile =~ /\w+/) ? $resfile : "/dev/null"; $sysres = system("$cmdstring >> $logfile 2>&1"); if ( $sysres == 0xff00 ) { warn "command ($cmdstring) failed"; } elsif ( $sysres > 0x80 ) { $sysres >>= 8; } elsif ( $sysres & 0x80 ) { my $sig = $sysres & ~0x80; warn "command ($cmdstring) core signal $sig\n"; } $sysres; } sub cleanup_and_quit { if ( (! $Keep) && (-d $TmpD) ) { unlink <$Tmp.*>; rmdir $TmpD; } exit 0; } sub cleanup_ere_dying { my $msg = $_[0]; if ($LogFile) { &write_log($msg); &write_log("$ProgramName: aborted run\n"); } if ( (! $Keep) && (-d $TmpD) ) { unlink <$Tmp.*>; rmdir $TmpD; } $! = 2; # is this adequate? # this is a callback, so fall through } # EOF script/aebisect.in