#!/bin/sh

# Tests a set of patches from a directory.
# Copyright (C) 2007-2024 Free Software Foundation, Inc.
# Contributed by Sebastian Pop <sebastian.pop@amd.com>

# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

cat <<EOF

WARNING: This script should only be fed with patches from known
         authorized and trusted sources.  Don't even think about
         hooking it up to a raw feed from the gcc-patches list or
         you'll regret it.

EOF

args=$@

svnpath=svn://gcc.gnu.org/svn/gcc
dashj=
default_standby=1
standby=$default_standby
default_watermark=0.60
watermark=$default_watermark
savecompilers=false
nopristinecache=false
nogpg=false
stop=false

usage() {
    cat <<EOF
patch_tester.sh [-j<N>] [-standby N] [-watermark N] [-savecompilers] [-nogpg]
                [-svnpath URL] [-stop] [-nopristinecache]
                <source_dir> [patches_dir [state_dir [build_dir]]]

    J is the flag passed to make.  Default is empty string.

    STANDBY is the number of minutes between checks for new patches in
    PATCHES_DIR.  Default is ${default_standby} minutes.

    WATERMARK is the 5 minute average system charge under which a new
    compile can start.  Default is ${default_watermark}.

    SAVECOMPILERS copies the compilers in the same directory as the
    test results for the non patched version.  Default is not copy.

    NOPRISTINECACHE prevents use of cached test results from any earlier
    test runs on the pristine version of the branch and revision under
    test (the default behaviour).  This should be used when testing the
    same revision and patch with multiple sets of configure options, as
    these may affect the set of baseline failures.

    NOGPG can be used to avoid checking the GPG signature of patches.

    URL is the location of the GCC SVN repository.  The default is
    ${svnpath}.

    STOP exits when PATCHES_DIR is empty.

    SOURCE_DIR is the directory containing GCC's toplevel configure.

    PATCHES_DIR is the directory containing the patches to be tested.
    Default is SOURCE_DIR/patches.

    STATE_DIR is where the tester maintains its internal state.
    Default is SOURCE_DIR/state.

    BUILD_DIR is the build tree, a temporary directory that this
    script will delete and recreate.  Default is SOURCE_DIR/obj.

EOF
    exit 1
}

makedir () {
    DIRNAME=$1
    mkdir -p $DIRNAME
    if [ $? -ne 0 ]; then
	echo "ERROR: could not make directory $DIRNAME"
	exit 1
    fi
}

while [ $# -ne 0 ]; do
    case $1 in
	-j*)
	    dashj=$1; shift
	    ;;
	-standby)
	    [[ $# > 2 ]] || usage
	    standby=$2; shift; shift
	    ;;
	-watermark)
	    [[ $# > 2 ]] || usage
	    watermark=$2; shift; shift
	    ;;
	-savecompilers)
	    savecompilers=true; shift
	    ;;
	-nopristinecache)
	    nopristinecache=true; shift
	    ;;
	-nogpg)
	    nogpg=true; shift
	    ;;
	-stop)
	    stop=true; shift
	    ;;
	-svnpath)
	    svnpath=$2; shift; shift
	    ;;
	-*) 
	    echo "Invalid option: $1"
	    usage
	    ;;
	*)
	    break
	    ;;
    esac
done

test $# -eq 0 && usage

SOURCE=$1
PATCHES=
STATE=
BUILD=

if [[ $# < 2 ]]; then
    PATCHES=$SOURCE/patches
else
    PATCHES=$2
fi
if [[ $# < 3 ]]; then
    STATE=$SOURCE/state
else
    STATE=$3
fi
if [[ $# < 4 ]]; then
    BUILD=$SOURCE/obj
else
    BUILD=$4
fi

[ -d $PATCHES ] || makedir $PATCHES
[ -d $STATE ] || makedir $STATE
[ -d $STATE/patched ] || makedir $STATE/patched
[ -d $SOURCE ] || makedir $SOURCE
[ -f $SOURCE/config.guess ] || {
    cd $SOURCE
    svn -q co $svnpath/trunk .
    if [ $? -ne 0 ]; then
	echo "ERROR: initial svn checkout failed"
	exit 1
    fi
}

# This can contain required local settings:
#  default_config  configure options, always passed
#  default_make    make bootstrap options, always passed
#  default_check   make check options, always passed
[ -f $STATE/defaults ] && . $STATE/defaults

VERSION=`svn info $SOURCE | grep "^Revision:" | sed -e "s/^Revision://g" -e "s/ //g"`

exec >> $STATE/tester.log 2>&1 || exit 1
set -x

TESTING=$STATE/testing
REPORT=$TESTING/report
PRISTINE=$TESTING/pristine
PATCHED=$TESTING/patched
PATCH=
TARGET=`$SOURCE/config.guess || exit 1` 
TESTLOGS="gcc/testsuite/gcc/gcc.sum
gcc/testsuite/gfortran/gfortran.sum
gcc/testsuite/g++/g++.sum
gcc/testsuite/objc/objc.sum
$TARGET/libstdc++-v3/testsuite/libstdc++.sum
$TARGET/libffi/testsuite/libffi.sum
$TARGET/libgomp/testsuite/libgomp.sum
$TARGET/libmudflap/testsuite/libmudflap.sum"
COMPILERS="gcc/cc1
gcc/cc1obj
gcc/cc1plus
gcc/f951
gcc/jc1
gcc/gnat1
gcc/tree1"

now () {
    echo `TZ=UTC date +"%Y_%m_%d_%H_%M_%S"`
}

report () {
    echo "$@" >> $REPORT
}

freport () {
    if [ -s $1 ]; then
	report "(cat $1"
	cat $1 >> $REPORT
	report "tac)"
    fi
}

cleanup () {
    cd $SOURCE
    svn cleanup && svn revert -R . && svn st | cut -d' ' -f5- | xargs rm -v
}

selfexec () {
    exec ${CONFIG_SHELL-/bin/sh} $0 $args
}

update () {
    svn_branch=`grep "^branch:" $PATCH | sed -e "s/^branch://g" -e "s/ //g"`
    if [ x$svn_branch = x ]; then
	svn_branch=trunk
    fi

    svn_revision=`grep "^revision:" $PATCH | sed -e "s/^revision://g" -e "s/ //g"`
    if [ x$svn_revision = x ]; then
	svn_revision=HEAD
    fi

    cleanup
    cd $SOURCE
    case $svn_branch in
	trunk)
	    if ! svn switch -r $svn_revision $svnpath/trunk &> $TESTING/svn ; then
		report "failed to update svn sources with"
		report "svn switch -r $svn_revision $svnpath/trunk"
		freport $TESTING/svn
		return 1
	    fi
	    ;;

	${svnpath}*)
	    if ! svn switch -r $svn_revision $svn_branch &> $TESTING/svn ; then
		report "failed to update svn sources with"
		report "svn switch -r $svn_revision $svn_branch"
		freport $TESTING/svn
		return 1
	    fi
	    ;;

	*)
	    if ! svn switch -r $svn_revision $svnpath/branches/$svn_branch &> $TESTING/svn ; then
		report "failed to update svn sources with"
		report "svn switch -r $svn_revision $svnpath/branches/$svn_branch"
		freport $TESTING/svn
		return 1
	    fi
	    ;;
    esac
    contrib/gcc_update --touch

    current_version=`svn info $SOURCE | grep "^Revision:" | sed -e "s/^Revision://g" -e "s/ //g"`
    if [[ $VERSION < $current_version ]]; then
	if [ -f $SOURCE/contrib/patch_tester.sh ]; then
	    selfexec
	fi
    fi

    return 0
}

apply_patch () {
    if [ $nogpg = false ]; then
	if ! gpg --batch --verify $PATCH &> $TESTING/gpgverify ; then
	    report "your patch failed to verify:"
	    freport $TESTING/gpgverify
	    return 1
	fi
    fi

    cd $SOURCE
    if ! patch -p0 < $PATCH &> $TESTING/patching ; then
	report "your patch failed to apply:"
	report "(check that the patch was created at the top level)"
	freport $TESTING/patching
	return 1
    fi

    # Just assume indexes for now -- not really great, but svn always
    # makes them.
    grep "^Index: " $PATCH | sed -e 's/Index: //' | while read file; do
	# If the patch resulted in an empty file, delete it.
	# This is how svn reports deletions.
	if [ ! -s $file ]; then
	    rm -f $file
	    report "Deleting empty file $file"
	fi
    done
}

save_compilers () {
    for COMPILER in $COMPILERS ; do
	if [ -f $BUILD/$COMPILER ]; then
	    cp $BUILD/$COMPILER $PRISTINE
	fi
    done
}

bootntest () {
    rm -rf $BUILD
    mkdir $BUILD
    cd $BUILD

    CONFIG_OPTIONS=`grep "^configure:" $PATCH | sed -e "s/^configure://g"`
    CONFIG_OPTIONS="$default_config $CONFIG_OPTIONS"
    if ! eval $SOURCE/configure $CONFIG_OPTIONS &> $1/configure ; then
	report "configure with `basename $1` version failed with:"
	freport $1/configure
	return 1
    fi

    MAKE_ARGS=`grep "^make:" $PATCH | sed -e "s/^make://g"`
    MAKE_ARGS="$default_make $MAKE_ARGS"
    if ! eval make $dashj $MAKE_ARGS &> $1/bootstrap ; then
	report "bootstrap with `basename $1` version failed with last lines:"
	tail -30 $1/bootstrap > $1/last_bootstrap
	freport $1/last_bootstrap
	report "grep --context=20 Error bootstrap:"
	grep --context=20 Error $1/bootstrap > $1/bootstrap_error
	freport $1/bootstrap_error
	return 1
    fi

    CHECK_OPTIONS=`grep "^check:" $PATCH | sed -e "s/^check://g"`
    CHECK_OPTIONS="$default_check $CHECK_OPTIONS"
    eval make $dashj $CHECK_OPTIONS -k check &> $1/check

    SUITESRUN="`grep 'Summary ===' $1/check | cut -d' ' -f 2 | sort`"
    if [ x$SUITESRUN = x ]; then
	report "check with `basename $1` version failed, no testsuites were run"
	return 1
    fi

    for LOG in $TESTLOGS ; do
	if [ -f $BUILD/$LOG ]; then
	    mv $BUILD/$LOG $1
	    mv `echo "$BUILD/$LOG" | sed -e "s/\.sum/\.log/g"` $1
	fi
    done

    return 0
}

bootntest_patched () {
    cleanup
    mkdir -p $PATCHED
    apply_patch && bootntest $PATCHED
    return $?
}

# Build the pristine tree with exactly the same options as the patch under test.
bootntest_pristine () {
    cleanup
    current_branch=`svn info $SOURCE | grep "^URL:" | sed -e "s/URL: //g" -e "s,${svnpath},,g"`
    current_version=`svn info $SOURCE | grep "^Revision:" | sed -e "s/^Revision://g" -e "s/ //g"`
    PRISTINE=$STATE/$current_branch/$current_version

    if [ $nopristinecache = true ]; then
      rm -rf $PRISTINE
    fi
    if [ -d $PRISTINE ]; then
	ln -s $PRISTINE $TESTING/pristine
	return 0
    else
	mkdir -p $PRISTINE
	ln -s $PRISTINE $TESTING/pristine
	bootntest $PRISTINE
	RETVAL=$?
	if [ $RETVAL = 0 -a $savecompilers = true ]; then
	    save_compilers
	fi
	return $RETVAL
    fi
}

regtest () {
    touch $1/report
    touch $1/passes
    touch $1/failed
    touch $1/regress

    for LOG in $TESTLOGS ; do
	NLOG=`basename $LOG`
	if [ -f $1/$NLOG ]; then
	    awk '/^FAIL: / { print "'$NLOG'",$2; }' $1/$NLOG
	fi
    done | sort | uniq > $1/failed

    comm -12 $1/failed $1/passes >> $1/regress
    NUMREGRESS=`wc -l < $1/regress | tr -d ' '`

    if [ $NUMREGRESS -eq 0 ] ; then
	for LOG in $TESTLOGS ; do
	    NLOG=`basename $LOG`
	    if [ -f $1/$NLOG ] ; then
		awk '/^PASS: / { print "'$NLOG'",$2; }' $1/$NLOG
	    fi
	done | sort | uniq | comm -23 - $1/failed > $1/passes
	echo "there are no regressions with your patch." >> $1/report
    else
	echo "with your patch there are $NUMREGRESS regressions." >> $1/report
	echo "list of regressions with your patch:" >> $1/report
	cat $1/regress >> $1/report
    fi
}

contrib_compare_tests () {
    report "comparing logs with contrib/compare_tests:"
    for LOG in $TESTLOGS ; do
 	NLOG=`basename $LOG`
 	if [ -f $PRISTINE/$NLOG -a -f $PATCHED/$NLOG ]; then
 	    $SOURCE/contrib/compare_tests $PRISTINE/$NLOG $PATCHED/$NLOG > $TESTING/compare_$NLOG
 	    freport $TESTING/compare_$NLOG
 	fi
    done
}

compare_passes () {
    regtest $PRISTINE
    cp $PRISTINE/passes $PATCHED
    regtest $PATCHED
    freport $PATCHED/report
    report "FAILs with patched version:"
    freport $PATCHED/failed
    report "FAILs with pristine version:"
    freport $PRISTINE/failed

    # contrib_compare_tests
}

write_report () {
    backup_patched=$STATE/patched/`now`
    report "The files used for the validation of your patch are stored in $backup_patched on the tester machine."

    EMAIL=`grep "^email:" $PATCH | sed -e "s/^email://g" -e "s/ //g"`
    if [ x$EMAIL != x ]; then
	mutt -s "[regtest] Results for `basename $PATCH` on $TARGET" -i $REPORT -a $PATCH $EMAIL
    fi

    mv $TESTING $backup_patched
}

announce () {
    EMAIL=`grep "^email:" $PATCH | sed -e "s/^email://g" -e "s/ //g"`
    if [ x$EMAIL != x ]; then

	START_REPORT=$TESTING/start_report
	echo "Hi, " >> $START_REPORT
	echo "I'm the automatic tester running on $TARGET." >> $START_REPORT
	echo "I just started to look at your patch `basename $PATCH`." >> $START_REPORT
	echo "Bye, your automatic tester." >> $START_REPORT
	mutt -s "[regtest] Starting bootstrap for `basename $PATCH` on $TARGET" -i $START_REPORT $EMAIL
    fi
}

# After selfexec, $TESTING is already set up.  
if [ -d $TESTING ]; then
    # The only file in $TESTING is the patch.
    PATCH=`ls -rt -1 $TESTING | head -1`
    PATCH=$TESTING/$PATCH
    if [ -f $PATCH ]; then
	bootntest_patched && bootntest_pristine && compare_passes
	write_report
    fi
fi

firstpatch=true
while true; do
    PATCH=`ls -rt -1 $PATCHES | head -1`
    if [ x$PATCH = x ]; then
	if [ $stop = true ]; then
	    if [ $firstpatch = true ]; then
		echo "No patches ready to test, quitting."
		exit 1
	    else
		echo "No more patches to test."
		exit 0
	    fi
	fi
	sleep ${standby}m
    else
	firstpatch=false
	sysload=`uptime | cut -d, -f 5`
	if [[ $sysload > $watermark ]]; then
	    # Wait a bit when system load is too high.
	    sleep ${standby}m
	else
	    mkdir -p $TESTING
	    mv $PATCHES/$PATCH $TESTING/
	    PATCH=$TESTING/$PATCH

	    announce
	    update && bootntest_patched && bootntest_pristine && compare_passes
	    write_report
	fi
    fi
done
