#!/bin/bash

############################################################################
#
# Script for running those SCST regression tests that can be run automatically.
#
# Copyright (C) 2008 Bart Van Assche <bart.vanassche@gmail.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, version 2
# of the License.
#
# 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.
#
############################################################################

############################################################################
# This script performs the following actions:
# - Creates a temporary directory for storing the output of the regression
#   tests. No existing files are modified by this script.
# - Verifies whether the top-level *.patch files apply cleanly to the SCST
#   tree.
# - Duplicates the entire source tree to the temporary directory and
#   compiles the SCST source code.
# - Duplicates the entire source tree to the temporary directory, applies
#   the full-perf patches and again compiles the SCST source code.
# - Checks whether the kernel version specified through option -k is present
#   in the directory specified through option -c.
# - If the source code of the kernel specified through -k is not present,
#   download it.
# - Convert the SCST source code into a kernel patch.
# - Extract the kernel sources.
# - Run checkpatch on the SCST kernel patch.
# - Apply the SCST kernel patch to the kernel tree.
# - Run 'make allmodconfig'.
# - Run the sparse source code checker on the SCST directory.
# - Run 'make headers_check'.
# - Compile the kernel tree.
# - Run 'make checkstack'.
# - Run 'make namespacecheck'.
# - Run 'make htmldocs'.
#
# Note: the results of the individual steps are not verified by this script
# -- the output generated by the individual steps has to be verified by
# reviewing the output files written into the temporary directory.
############################################################################


########################
# Function definitions #
########################

function usage {
  echo "Usage: $0 [-c] [-d] [-f] [-h] [-j] [-k <kver1>] [-k <kver2>] ..."
  echo "        -c - cache directory for Linux kernel tarballs."
  echo "        -d - directory for temporary regression test files."
  echo "        -f - full check -- do not only compile SCST but the whole" \
       "kernel tree."
  echo "        -h - display this help information."
  echo "        -j - number of jobs that 'make' should run simultaneously."
  echo "        -k <kver> - kernel version to use during test."
}

# First three components of the kernel version number.
function kernel_version {
  echo "$1" | sed -n 's/^\([0-9]*\.[0-9]*\.[0-9]*\).*$/\1/p'
}

# Last component of the kernel version, or the empty string if $1 has only
# three components.
function patchlevel {
  echo "$1" | sed -n 's/^\([0-9]*\.[0-9]*\.[0-9]*\)[.-]\(.*\)$/\2/p'
}

# Create a linux-$1 tree in the current directory, where $1 is a kernel
# version number with either three or four components.
function extract_kernel_tree {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local tmpdir=kernel-tree-tmp-$$

  rm -rf "linux-$1" "${tmpdir}"
  mkdir "${tmpdir}" || return $?
  (
    cd "${tmpdir}" || return $?
    tar xjf "${kernel_sources}/linux-${kver}.tar.bz2" || return $?
    cd "linux-${kver}" || return $?
    if [ "${plevel}" != "" ]; then
      bzip2 -cd "${kernel_sources}/patch-$1.bz2" \
        | patch -p1 -f -s || return $?
    fi
    cd ..
    mv "linux-${kver}" "../linux-$1" || return $?
    if [ "${kver}" = "2.6.29" ]; then
      cd "../linux-$1" || return $?
      patch -f -s -p1 <<'EOF'
Make sure that branch profiling does not trigger sparse warnings.
See also http://bugzilla.kernel.org/show_bug.cgi?id=12925
--- orig/linux-2.6.29/include/linux/compiler.h	2009-03-23 19:12:14.000000000 -0400
+++ linux-2.6.29/include/linux/compiler.h	2009-03-24 08:46:46.000000000 -0400
@@ -75,7 +75,8 @@ struct ftrace_branch_data {
  * Note: DISABLE_BRANCH_PROFILING can be used by special lowlevel code
  * to disable branch tracing on a per file basis.
  */
-#if defined(CONFIG_TRACE_BRANCH_PROFILING) && !defined(DISABLE_BRANCH_PROFILING)
+#if defined(CONFIG_TRACE_BRANCH_PROFILING) \
+    && !defined(DISABLE_BRANCH_PROFILING) && !defined(__CHECKER__)
 void ftrace_likely_update(struct ftrace_branch_data *f, int val, int expect);
 
 #define likely_notrace(x)	__builtin_expect(!!(x), 1)
EOF
    fi
  )
  rmdir "${tmpdir}"
}

# Test whether the *.patch files in the SCST top-level directory apply cleanly
# to the SCST tree. Does not modify any files nor produce any output files.
function test_scst_tree_patches {
  local rc=0
  echo "Testing whether the SCST patches apply cleanly to the SCST tree ..."
  for p in *.patch srpt/patches/scst_increase_max_tgt_cmds.patch
  do
    if ! patch -p0 -f --dry-run -s <$p &>/dev/null; then
      echo "ERROR: patch $p does not apply cleanly."
      rc=1
    fi
  done
  if [ "${rc}" = 0 ]; then
    echo "OK"
  fi
}

# Copy the entire SCST source code tree from "$1" into the current directory.
# Only copy those files which are administered by Subversion.
function duplicate_scst_source_tree {
  if [ -e "$1/AskingQuestions" ]; then
    ( cd "$1" && svn status -v | cut -c41- \
      | while read f; do [ ! -d "$f" ] && echo "$f"; done ) \
    | tar -C "$1" --files-from=- -c -f - | tar -x -f -
  else
    return 1
  fi
}

# Compile the unpatched SCST source code.
function compile_scst_unpatched {
  local scst="$PWD"
  local outputfile="${outputdir}/compilation-output-unpatched.txt"
  local workingdirectory="${outputdir}/scst-unpatched"

  echo "Testing whether the SCST tree compiles fine ..."
  (
    if mkdir -p "${workingdirectory}"  \
       && cd "${workingdirectory}"     \
       && duplicate_scst_source_tree "${scst}"  \
       && (make -s clean \
           && make -s scst iscsi-scst  \
           && if "${scst_local}" = "true" ; then make -C scst_local clean; fi \
           && if "${scst_local}" = "true" ; then make -C scst_local -s   ; fi \
           && if "${mpt_scst}"   = "true" ; then make -C mpt clean; fi \
           && if "${mpt_scst}"   = "true" ; then make -C mpt -s   ; fi \
           && make -C srpt -s clean    \
           && make -C srpt -s )    \
           &> "${outputfile}"
    then
      echo "OK"
    else
      echo "FAILED"
    fi
  )
}

# Test out-of-tree compilation agains the kernel header files in
# /lib/modules/$(uname -r)/build.
function compile_scst_patched {
  local scst="$PWD"
  local outputfile="${outputdir}/compilation-output-patched.txt"
  local workingdirectory="${outputdir}/scst-patched"

  echo "Testing whether the full-perf SCST tree compiles fine ..."
  (
    if mkdir -p "${workingdirectory}"  \
       && cd "${workingdirectory}"     \
       && duplicate_scst_source_tree "${scst}"  \
       && patch -p0 -f -s <"${scst}/iscsi-full_perf.patch"    \
       && patch -p0 -f -s <"${scst}/qla2x00t-full_perf.patch" \
       && patch -p0 -f -s <"${scst}/scst-full_perf.patch"     \
       && (make -s clean               \
           && make -s scst iscsi-scst  \
           && if "${scst_local}" = "true" ; then make -C scst_local clean; fi \
           && if "${scst_local}" = "true" ; then make -C scst_local -s   ; fi \
           && if "${mpt_scst}"   = "true" ; then make -C mpt clean; fi \
           && if "${mpt_scst}"   = "true" ; then make -C mpt -s   ; fi \
           && make -C srpt -s clean    \
           && make -C srpt -s )    \
          &> "${outputfile}"
    then
      echo "OK"
    else
      echo "FAILED"
    fi
  )
}

# Download the file from URL $1 and save it in the current directory.
function download_file {
  if [ ! -e "$(basename "$1")" ]; then
    echo "Downloading $1 ..."
    if ! wget -q -nc "$1"; then
      echo "Downloading $1 failed."
      return 1
    fi
  fi
}

# Make sure the kernel tarball and patch file are present in directory
# ${kernel_sources}. Download any missing files from ${kernel_mirror}.
function download_kernel {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"

  mkdir -p "${kernel_sources}" || return $?
  test -w "${kernel_sources}" || return $?
  (
    cd "${kernel_sources}" || return $?
    download_file "${kernel_mirror}/linux-$(kernel_version $1).tar.bz2" \
      || return $?
    if [ "${plevel}" != "" ]; then
      download_file "${kernel_mirror}/patch-$1.bz2" || return $?
    fi
  )
}

# Generate a kernel patch from the SCST source tree for kernel version $1.
function generate_kernel_patch {
  local scst_dir="${PWD}"
  local kver="$(kernel_version $1)"
  local patchfile="${outputdir}/scst-$1-kernel.patch"

  SIGNED_OFF_BY="..." \
  scripts/generate-kernel-patch \
    $([ "${scst_local}" = "true" ] && echo -- "-l") \
    $([ "${mpt_scst}"   = "true" ] && echo -- "-m") \
    $([ "${qla2x00t}"   = "true" ] && echo -- "-q") \
    ${kver} > "${patchfile}"
}

# Generate a kernel patch through scripts/generate-kernel-patch and test
# whether it applies cleanly to kernel version $1. Leaves a vanilla kernel
# in directory "${outputdir}/linux-$1" at exit.
function test_if_patch_applies_cleanly {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/kernel-$1-patch-output.txt"
  local patchfile="${outputdir}/scst-$1-kernel.patch"
  local rc=0

  echo "Testing whether the generated kernel patch applies cleanly ..."
  ( cd "${outputdir}" && extract_kernel_tree "$1" )
  ( cd "${outputdir}/linux-$1" \
    && patch -p1 --dry-run -f < "${patchfile}" &> "${outputfile}" )
  if [ $? != 0 ]; then
    echo "FAILED"
    rc=1
  fi
  return $rc
}

# Run checkpatch on the generated kernel patch. Assumes that there is a
# vanilla kernel tree present in directory "${outputdir}/linux-$1", and leaves
# this kernel tree clean.
function run_checkpatch  {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/checkpatch-$1-output.txt"
  local patchfile="${outputdir}/scst-$1-kernel.patch"

  if [ -e "${outputdir}/linux-$1/scripts/checkpatch.pl" ]
  then
    echo "Running checkpatch on the SCST kernel patch ..."
    ( cd "${outputdir}/linux-$1" \
      && scripts/checkpatch.pl - < "${patchfile}" &> "${outputfile}")
    local errors=$(grep -c '^ERROR' "${outputfile}")
    local warnings=$(grep -c '^WARNING' "${outputfile}")
    echo "${errors} errors / ${warnings} warnings."
    grep -E '^WARNING|^ERROR' "${outputfile}" | sort | uniq -c
  else
    echo "Skipping checkpatch step for kernel $1."
  fi
  return 0
}

function patch_and_configure_kernel {
  local patchfile="${outputdir}/scst-$1-kernel.patch"

  echo "Patching and configuring kernel ..."
  (
    cd "${outputdir}/linux-$1"                                     \
    && patch -p1 -f -s <"${patchfile}"                             \
                       >"${outputdir}/patch-command-output-$1.txt" \
    && make -s allmodconfig &>/dev/null
  )
}

# Patches and compiles a kernel tree. Assumes that there is a vanilla kernel
# tree present in directory "${outputdir}/linux-$1".
function compile_patched_kernel {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/kernel-$1-compilation-output.txt"

  echo "Compiling kernel $1 ..."
  (
    (
      cd "${outputdir}/linux-$1" \
      && LC_ALL=C make -s -k bzImage modules
    )
  ) &> "${outputfile}"
  echo "See also ${outputfile}."
  return 0
}

# Run the source code verification tool 'sparse' on the SCST code. Assumes that
# there is a patched kernel tree present in directory "${outputdir}/linux-$1".
# For more information about endianness annotations, see also
# http://lwn.net/Articles/205624/.
function run_sparse {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/sparse-$1-output.txt"

  echo "Running sparse on the patched kernel ..."
  (
    cd "${outputdir}/linux-$1" \
    && make -s prepare \
    && make -s scripts \
    && LC_ALL=C make -k C=2 M=drivers/scst # CF=-D__CHECK_ENDIAN__
  ) &> "${outputfile}"
  local errors=$(grep -c ' error:' "${outputfile}")
  local warnings=$(grep -c ' warning:' "${outputfile}")
  echo "${errors} errors / ${warnings} warnings."
  cat "${outputfile}" \
    | grep -E 'warning:|error:' \
    | sed -e 's/^[^ ]*:[^ ]*:[^ ]*: //' \
          -e "s/context imbalance in '[^']*':/context imbalance in <function>:/g" \
          -e "s/context problem in '[^']*': '[^']*'/context problem in <function>: <function>/g" \
          -e "s/function '[^']*'/function/g" \
          -e "s/symbol '[^']*'/symbol/g" \
    | sort \
    | uniq -c
  return 0
}

function run_checkstack {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/checkstack-$1-output.txt"

  echo "Running checkstack on the patched $1 kernel ..."
  (
    cd "${outputdir}/linux-$1" \
    && make -s prepare \
    && make -s scripts \
    && LC_ALL=C make -k checkstack
  ) &> "${outputfile}"
  echo "See also ${outputfile}."
  return 0
}

function run_namespacecheck {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/namespacecheck-$1-output.txt"

  echo "Running namespacecheck on the patched $1 kernel ..."
  (
    cd "${outputdir}/linux-$1" \
    && make -s prepare \
    && make -s scripts \
    && LC_ALL=C make -k namespacecheck
  ) &> "${outputfile}"
  echo "See also ${outputfile}."
  return 0
}

function run_headers_check {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/headers_check-$1-output.txt"

  echo "Running headers check on the patched $1 kernel ..."
  (
    cd "${outputdir}/linux-$1" \
    && make -s prepare \
    && make -s scripts \
    && LC_ALL=C make -k headers_check
  ) &> "${outputfile}"
  local errors=$(grep -c '^[^ ]' "${outputfile}")
  echo "${errors} errors."
  grep '^[^ ]' "${outputfile}" | sed 's/.*: //' | sort | uniq -c
  return 0
}

function run_make_htmldocs {
  local kver="$(kernel_version $1)"
  local plevel="$(patchlevel $1)"
  local outputfile="${outputdir}/htmldocs-$1-output.txt"

  echo "Generating HTML documentation for the patched $1 kernel ..."
  (
    cd "${outputdir}/linux-$1" \
    && make -s prepare \
    && make -s scripts \
    && LC_ALL=C make -k htmldocs
  ) &> "${outputfile}"
  echo "See also ${outputfile}."
  return 0
}


#########################
# Argument verification #
#########################

if [ ! -e scst -o ! -e iscsi-scst -o ! -e srpt ]; then
  echo "Please run this script from inside the SCST subversion source tree."
  exit 1
fi

full_check="false"
# Where to store persistenly downloaded kernel tarballs and kernel patches.
kernel_sources="$HOME/software/downloads"
# URL for downloading kernel tarballs and kernel patches.
kernel_mirror="ftp://ftp.eu.kernel.org/pub/linux/kernel/v2.6"
kernel_versions=""
# Directory in which the regression test output files will be stored. Must be
# an absolute path.
outputdir="${PWD}/regression-test-output-$(date +%Y-%m-%d_%Hh%Mm%Ss)"
# Driver configuration.
mpt_scst="false"
qla2x00t="false"
scst_local="true"

set -- $(/usr/bin/getopt "c:d:fj:hk:" "$@")
while [ "$1" != "${1#-}" ]
do
  case "$1" in
    '-c') kernel_sources="$2"; shift; shift;;
    '-d') outputdir="$2"; shift; shift;;
    '-f') full_check="true"; shift;;
    '-h') usage; exit 1;;
    '-j') export MAKEFLAGS="-j$2"; shift; shift;;
    '-k') kernel_versions="${kernel_versions} $2"; shift; shift;;
    '--') shift;;
    *)    usage; exit 1;;
  esac
done

if [ $# != 0 ]; then
  usage
  exit 1
fi

# Default kernel versions to use for the test.
if [ "${kernel_versions}" = "" ]; then
  # RHEL 4.x / CentOS 4.x has a kernel based on version 2.6.9.
  # RHEL 5.x / CentOS 5.x has a kernel based on version 2.6.18.
  # Ubuntu 8.04 (Hardy Heron) has a kernel based on version 2.6.24.
  # Ubuntu 8.10 (Intrepid Ibex) has a kernel based on version 2.6.27.
  # openSUSE 11.0 has a kernel based on version 2.6.25.
  # openSUSE 11.1 has a kernel based on version 2.6.27.
  #kernel_versions="2.6.24.7 2.6.25.20 2.6.26.8 2.6.27.20 2.6.28.8 2.6.29"
  kernel_versions="2.6.28.8"
fi


####################
# Regression tests #
####################

if ! mkdir -p "${outputdir}"; then
  if [ -e "${outputdir}" ]; then
    echo "Error: directory ${outputdir} already exists."
  else
    echo "Error: could not create directory ${outputdir}."
  fi
fi

test_scst_tree_patches || exit $?
compile_scst_unpatched || exit $?
compile_scst_patched || exit $?

for k in ${kernel_versions}
do
  echo "===================="
  printf "= kernel %-9s =\n" "${k}"
  echo "===================="

  download_kernel $k || continue
  generate_kernel_patch $k || continue
  test_if_patch_applies_cleanly $k || continue
  run_checkpatch $k
  patch_and_configure_kernel $k
  run_sparse $k
  run_headers_check $k
  [ "${full_check}" = "true" ] || continue
  compile_patched_kernel $k
  run_checkstack $k
  run_namespacecheck $k
  run_make_htmldocs $k
done
