#!/bin/bash
#
# Copyright (C) 2020 Konsulko Group
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#
# gcovr wrapper for generating coverage reports against AGL bindings
# on target.  With a given binding name or coverage widget file, the
# coverage version will be installed, and the pyagl tests for the
# binding run before generating a report with gcovr.  The afm-test
# test widget or a user-supplied command may be run instead of the
# pyagl tests, see usage below, or run with "--help".
#

usage() {
    cat <<-EOF
	Usage:
	  $(basename $0) [options] <binding name | widget file>

	Options:
	  -h, --help
	    Print this help and exit

	  -k, --keep
	    Do not remove temporary files/directories

	  -o, --gcovr-options
	    Additional gcovr options, multiple options should be quoted

	  -w, --workdir
	    gcov/gcovr temporary working directory, defaults to /tmp/gcov
	    The directory will be removed after running without --keep if it
	    is empty, use caution if specifying existing system directories!

	  --pyagl
	    Run pyagl tests for binding, enabled by default

	  --afm-test
	    Run afm-test test widget tests for binding.
	    If specified, disables pyagl tests; note that the last argument in
	    the command-line will take precedence.

	  -c, --command
	    Test command to use in place of pyagl or afm-test, should be quote
	    If specified, disables pyagl and afm-test tests.

	EOF
}

# Helper to validate widget install dir
check_wgt_install() {
    if [ ! \( -d $1 -o -f $1/config.xml \) ]; then
        echo "ERROR: No widget install at $1"
        exit 1
    elif [ ! -d $1/src ]; then
        echo "ERROR: No source in $1/src"
        exit 1
    fi
}

# Helper to run gcovr inside mount namespace environment
gcovr_runner() {
    wgt_install_dir=/var/local/lib/afm/applications/$1
    check_wgt_install ${wgt_install_dir}

    if [ ! -d $workdir/$1 ]; then
        echo "ERROR: No coverage data in $workdir/$1"
        exit 1
    fi

    # Get original source path
    gcno=$(cd $workdir/$1 && find -name '*.gcno' | head -n 1 | cut -d/ -f2-)
    if [ -z "$gcno" ]; then
        echo "ERROR: no gcno file found in $workdir/$1"
        exit 1
    fi
    srcfile=$(strings $workdir/$1/${gcno} | grep "$(basename ${gcno%.gcno})$" | uniq)
    srcdir=$(echo $srcfile | sed "s|/${gcno%%/*}/.*$||")

    # Set up mounts for chroot to run gcovr in
    # NOTE: We do not unmount these later, as we assume we are in a
    #       private mount namespace and they will go away on exit from
    #       it.
    echo "Setting up mounts"
    tmpdir=$(mktemp -d)
    echo $tmpdir > $workdir/.runner_tmpdir
    mkdir -p $tmpdir/{lower,upper,work,merged}
    # NOTE: Could potentially use rbind here, but explicitly mounting
    #        just what we need seems safer
    mount --bind / $tmpdir/lower
    mount -t overlay -o lowerdir=$tmpdir/lower,upperdir=$tmpdir/upper,workdir=$tmpdir/work overlay $tmpdir/merged
    mount --bind /proc $tmpdir/merged/proc
    mount --bind /sys $tmpdir/merged/sys
    mount --bind /dev $tmpdir/merged/dev
    mount --bind /tmp $tmpdir/merged/tmp
    # Bind in the data files
    # NOTE: $workdir is bound instead of specifically just $workdir/$1,
    #       so that e.g. html output to another directory in /tmp will
    #       work as expected.  A determined user may be able to shoot
    #       themselves in the foot, but for now the trade off seems
    #       acceptable.
    mkdir -p $tmpdir/merged/$workdir
    mount --bind $workdir $tmpdir/merged/$workdir
    # Bind the source files to their expected location
    mkdir -p $tmpdir/merged/$srcdir
    mount --bind ${wgt_install_dir}/src $tmpdir/merged/$srcdir

    echo "Entering chroot"
    echo
    exec chroot $tmpdir/merged \
        /usr/bin/gcovr -r $srcdir --object-directory $workdir/$1 --gcov-filter-source-errors -s ${GCOV_RUNNER_GCOVR_OPTIONS}
}

# Helper to clean up after runner
gcovr_runner_cleanup() {
    rm -rf $workdir/$1
    if [ -f $workdir/.runner_tmpdir ]; then
        tmpdir=$(cat $workdir/.runner_tmpdir)
        rm -rf $tmpdir
        rm -f $workdir/.runner_tmpdir
    fi
    if [ "$workdir" != "/tmp" ]; then
        rmdir $workdir 2>/dev/null || true
    fi
}

# Parse arguments
OPTS=$(getopt -o +hko:pw:c: --longoptions gcovr-runner,afm-test,command:,help,keep,gcovr-options:,pyagl,workdir: -n "$(basename $0)" -- "$@")
if [ $? -ne 0 ]; then
    exit 1
fi
eval set -- "$OPTS"

runner=false
keep=false
wgt=""
cmd=""
options=""
afmtest=false
pyagl=true
workdir="/tmp/gcov"

while true; do
    case "$1" in
        --gcovr-runner) runner=true; shift;;
        --afm-test) afmtest=true; pyagl=false; shift;;
        -c|--command) cmd="$2"; shift; shift;;
        -h|--help) usage; exit 0;;
        -k|--keep) keep=true; shift;;
        -o|--gcovr-options) options="$2"; shift; shift;;
        -p|--pyagl) pyagl=true; afmtest=false; shift;;
        -w|--workdir) workdir="$2"; shift; shift;;
        --) shift; break;;
        *) break;;
    esac
done

# Encode the assumption that a specified command means it runs instead
# of any other tests.
if [ -s "$cmd" ]; then
    pyagl=false
    afmtest=false
fi

if [ $# -ne 1 ]; then
    # Always expect widget name as single non-option argument
    usage
    exit 1
fi

# Rationalize workdir just in case
workdir=$(realpath "$workdir")

if [ "$runner" = "true" ]; then
    if [ "${GCOV_RUNNER_READY}" != "true" ]; then
        echo "ERROR: gcovr environment not ready!"
        exit 1
    fi
    gcovr_runner $1
    # If we get here, it'd be an error, so return 1
    exit 1
fi

binding=$1
if [ "${1%.wgt}" != "$1" ]; then
    # User has specified path to a widget file
    wgt=$(realpath $1)
    binding=$(basename "${1%-coverage.wgt}")
else
    wgt=/usr/AGL/apps/coverage/${binding}-coverage.wgt
fi
if [ ! -f $wgt ]; then
    echo "ERROR: No widget $wgt"
    exit 1
elif [ "$afmtest" = "true" -a ! -f /usr/AGL/apps/test/${binding}-test.wgt ]; then
    echo "ERROR: No test widget for $binding"
    exit 1
fi

# Determine starting systemd unit name
service=$(systemctl --all |grep afm-service-$binding |sed 's/^[ *] \([^ ]*\).*/\1/')
if [ -z "$service" ]; then
    echo "ERROR: Could not determine systemd service unit for $binding"
    exit 1
fi

# Install coverage widget
echo "Removing $binding widget"
systemctl stop $service
afm-util remove $binding
echo
echo "Installing $binding coverage widget"
afm-util install $wgt
echo

wgt_install_dir=/var/local/lib/afm/applications/$binding
check_wgt_install ${wgt_install_dir}
gcov_src=${wgt_install_dir}/coverage
if [ ! -d ${gcov_src} ]; then
	echo "ERROR: No coverage information in ${gcov_src}"
	exit 1
elif [ ! -f ${gcov_src}/gcov.env ]; then
	echo "ERROR: No gcov environment file at ${gcov_src}/gcov.env"
	exit 1
fi

#
# NOTE: In theory, the coverage data collection could be done inside
#       the mount namespace / chroot, but the potential for issues
#       when doing that seems higher than just running gcovr there,
#       so a conservative approach is taken.
#

# Set up things for the binary to write out gcda data files
#
# Having the matching build directory hierarchy in place and
# writeable by the target binary before any restart and testing is
# key to things working.
#
# As well, the environment file with the GCOV_PREFIX and
# GCOV_PREFIX_STRIP values needs to be present before running so the
# gcda files will get written into the relocated build hierarchy.
#
echo "Installing coverage information for $binding"
mkdir -p $workdir
rm -rf $workdir/$binding
cp -dr ${gcov_src} $workdir/$binding
chsmack -r -a System::Log $workdir
chmod -R go+w $workdir

# Install the gcov environment file
mkdir -p /etc/afm/widget.env.d/$binding
if [ "${workdir}" = "/tmp/gcov" ]; then
    cp ${gcov_src}/gcov.env /etc/afm/widget.env.d/$binding/gcov
else
    # Update GCOV_PREFIX to point into workdir
    sed "s|^GCOV_PREFIX=.*|GCOV_PREFIX=${workdir}/$binding|" ${gcov_src}/gcov.env > /etc/afm/widget.env.d/$binding/gcov
fi
chsmack -r -a _ /etc/afm/widget.env.d/$binding

# Determine new systemd unit name (version may now be different)
service=$(systemctl --all |grep afm-service-$binding |sed 's/^[ *] \([^ ]*\).*/\1/')
if [ -z "$service" ]; then
    echo "ERROR: Could not determine systemd service unit for $binding"
    exit 1
fi

# Restart the binding
systemctl start $service
echo

# Run tests or given command
if [ -n "$cmd" ]; then
    echo "Running command: $cmd"
    export AGL_AVAILABLE_INTERFACES=${AGL_AVAILABLE_INTERFACES:-ethernet}
    eval $cmd
elif [ "$pyagl" = "true" ]; then
    echo "Running $binding pyagl tests"
    export AGL_AVAILABLE_INTERFACES=${AGL_AVAILABLE_INTERFACES:-ethernet}
    pytest -k "${binding#agl-service-} and not hwrequired" /usr/lib/python3.?/site-packages/pyagl
else
    echo "Running $binding test widget"
    # NOTE: su to agl-driver is required here to avoid fallout from
    #       the "afm-util run" in afm-test seemingly triggering the
    #       start of other per-user bindings for the root user.
    su -l -c "/usr/bin/afm-test /usr/AGL/apps/test/${binding}-test.wgt" agl-driver
fi

# Restart again to trigger data file writing
systemctl restart $service
echo

# Run ourselves in gcovr runner mode inside a private mount namespace
export GCOV_RUNNER_READY=true
# NOTE: Passing gcovr options in the environment to avoid quoting hassles
export GCOV_RUNNER_GCOVR_OPTIONS="$options"
runner_options="--workdir ${workdir}"
unshare -m $0 --gcovr-runner ${runner_options} $binding
rc=$?

if [ "$keep" != "true" ]; then
    # Clean up after ourselves
    gcovr_runner_cleanup $1
    rm -f /etc/afm/widget.env.d/$1/gcov
    rmdir /etc/afm/widget.env.d/$1 2>/dev/null || true
fi

exit $rc

