BASIS  r3148
basistest-master.sh
Go to the documentation of this file.
00001 #! /bin/bash
00002 __FILE__="$(cd -P -- "$(dirname -- "$BASH_SOURCE")" && pwd -P)/$(basename -- "$BASH_SOURCE")"; if [[ -n "$SGE_ROOT" ]] && [[ $__FILE__ =~ $SGE_ROOT/.* ]] && [[ -n "${BASIS_DIR}" ]] && [[ -f "${BASIS_DIR}/bin/basistest-master.sh" ]]; then __FILE__="${BASIS_DIR}/bin/basistest-master.sh"; fi; i=0; lnk="$__FILE__"; while [[ -h "$lnk" ]] && [[ $i -lt 100 ]]; do dir=`dirname -- "$lnk"`; lnk=`readlink -- "$lnk"`; lnk=`cd "$dir" && cd $(dirname -- "$lnk") && pwd`/`basename -- "$lnk"`; let i++; done; [[ $i -lt 100 ]] && __FILE__="$lnk"; unset -v i dir lnk; __DIR__="$(dirname -- "$__FILE__")"; BASIS_BASH_UTILITIES="$__DIR__/bash/basis/basis.sh"; BASHPATH="$__DIR__/.:$__DIR__/bash:$BASHPATH" # <-- added by BASIS
00003 ##############################################################################
00004 # @file  basistest-master.sh
00005 # @brief Test master which can be run as a cron job.
00006 #
00007 # This shell script is supposed to be scheduled as cron job, where possibly
00008 # the basistest-cron.sh script is in fact used as cron job command without
00009 # arguments where all the settings for the cron job are fixed within this
00010 # latter script. On execution, this master script parses the configuration
00011 # file and executes the configured tests using by default the
00012 # basistest-slave.sh script.
00013 #
00014 # Copyright (c) 2011 University of Pennsylvania. All rights reserved.<br />
00015 # See https://www.cbica.upenn.edu/sbia/software/license.html or COPYING file.
00016 #
00017 # Contact: SBIA Group <sbia-software at uphs.upenn.edu>
00018 #
00019 # @ingroup Tools
00020 ##############################################################################
00021 
00022 . ${BASIS_BASH_UTILITIES} || exit 1
00023 
00024 # ============================================================================
00025 # constants
00026 # ============================================================================
00027 
00028 exedir  _EXEC_DIR  && readonly _EXEC_DIR
00029 exename _EXEC_NAME && readonly _EXEC_NAME
00030 
00031 # ============================================================================
00032 # default settings
00033 # ============================================================================
00034 
00035 # absolute path of tests configuration file
00036 conf_file='/etc/basistest.conf'
00037 
00038 # absolute path of file with timestamps for next test execution
00039 schedule_file='/var/run/basistest.schedule'
00040 
00041 # ============================================================================
00042 # help/version
00043 # ============================================================================
00044 
00045 # ----------------------------------------------------------------------------
00046 ## @brief Print documentation of options.
00047 #
00048 # @returns Nothing.
00049 function print_options
00050 {
00051     cat - << EOF-OPTIONS
00052 Options:
00053   -c [ --conf ]       The test configuration file. Defaults to "${conf_file}".
00054   -t [ --testcmd ]    The test execution command. Defaults to the basistest
00055                       command in the same directory as this executable.
00056   -s [ --schedule ]   The test schedule file which is created and updated by
00057                       this program. Defaults to "${schedule_file}".
00058   --dry               Dry run, i.e., do not actually invoke the test execution command.
00059   -v [ --verbose ]    Increases verbosity of output messages. Can be given multiple times.
00060   -h [ --help ]       Print help and exit.
00061   -u [ --usage ]      Print short help and exit.
00062   -V [ --version ]    Print version information and exit.
00063 EOF-OPTIONS
00064 }
00065 
00066 # ----------------------------------------------------------------------------
00067 ## @brief Print help.
00068 #
00069 # @returns Nothing.
00070 function print_help
00071 {
00072     echo "Usage:"
00073     echo "  ${_EXEC_NAME} [options]"
00074     echo
00075     cat - << EOF-DESCRIPTION
00076 Description:
00077   This so-called testing master script is executed by the basistest-cron command.
00078   On each run, it reads in the configuration file given by the --config option
00079   line-by-line. Each line in the configuration file specifies one test job to be
00080   executed. See the next section for details on the format and content of such
00081   configuration file.
00082 
00083 Configuration:
00084   The format of the configuration file is detailed here. Comments within the
00085   configuration file start with a '#' character at the beginning of each line.
00086 
00087   For each test of a specific branch of a project, the configuration file
00088   contains a line following the format:
00089 
00090     <m> <h> <d> <project> <branch> <model> <options>
00091 
00092   where
00093 
00094     <m>         Interval in minutes between consecutive test runs.
00095                 Defaults to "0" if "*" is given.
00096     <h>         Interval in hours between consecutive test runs.
00097                 Defaults to "0" if "*" is given.
00098     <d>         Interval in days (i.e., multiples of 24 hours) between consecutive
00099                 test runs. Defaults to "0" if "*" is given.
00100     <project>   Name of the BASIS project.
00101     <branch>    Branch within the project's SVN repository, e.g., "tags/1.0.0".
00102                 Defaults to "trunk" if a "*" is given.
00103     <model>     Dashboard model, i.e., either one of "Nightly", "Continuous",
00104                 and "Experimental". Defaults to "Nightly".
00105     <options>   Additional options to the CTest script.
00106                 The "basistest.ctest" script of BASIS is used by default.
00107                 Run "ctest -S <path>/basistest.ctest,usage" to get a list of
00108                 available options. By default, the default options of the
00109                 CTest script are used. Note that this option can in particular
00110                 be used to define CMake variables for the build configuration.
00111 
00112   Attention: Neither of these entries may contain any whitespace character!
00113 
00114   For example, nightly tests of the main development branch (trunk) of the
00115   project BASIS itself which are run once every day including coverage
00116   analysis and memory checks are scheduled by
00117 
00118     * * 1 BASIS trunk Nightly coverage,memcheck
00119 
00120 Test execution:
00121   By default, the basistest-slave command is invoked for each entry in the
00122   configuration file. A custom test command can be set using the option --testcmd.
00123   The provided command has to support the following command line arguments.
00124 
00125     --project <arg>   The name of the project as given in the configuration.
00126     --branch <arg>    The branch as given in the configuration.
00127     --model <arg>     The name of the model as given in the configuration.
00128     --args <arg>      The additional options given in the configuration.
00129     --verbose         Enable verbose output messages. May be given multiple times.
00130 
00131   The --args and --verbose options have to be optional.
00132 EOF-DESCRIPTION
00133     echo
00134     print_options
00135     echo
00136     cat - << EOF-EXAMPLES
00137 Examples:
00138   ${_EXEC_NAME} --conf /etc/basis/testd.conf --schedule /var/run/basis/testd
00139 
00140     Runs this daemon with the configuration file "/etc/basis/testd.conf",
00141     where the test schedule "/var/run/basis/testd" is created (or updated).
00142     Note that this command should be setup as cron job instead of executing
00143     it manually.
00144 EOF-EXAMPLES
00145     echo
00146     print_contact
00147 }
00148 
00149 # ----------------------------------------------------------------------------
00150 ## @brief Print usage (i.e., only usage and options).
00151 #
00152 # @returns Nothing.
00153 function print_helpshort
00154 {
00155     echo "Usage:"
00156     echo "  ${_EXEC_NAME} [options]"
00157     echo
00158     print_options
00159     echo
00160     print_contact
00161 }
00162 
00163 # ============================================================================
00164 # helpers
00165 # ============================================================================
00166 
00167 # ----------------------------------------------------------------------------
00168 ## @brief Runs a test given the arguments in the configuration file.
00169 #
00170 # @param [in] project Name of the project to test.
00171 # @param [in] branch  Name of the branch to test.
00172 # @param [in] model   Name of the Dashboard model.
00173 # @param [in] options Additional options for the CTest script.
00174 #
00175 # @returns Whether the execution of the test was successful.
00176 #
00177 # @retval 0 On success.
00178 # @retval 1 On failure.
00179 function run_test
00180 {
00181     cmd="${test_cmd}"
00182     if [ ${verbose} -gt 1 ]; then cmd="${cmd} --verbose"; fi
00183     if [ ${verbose} -gt 2 ]; then cmd="${cmd} --verbose"; fi
00184     cmd="${cmd} --project $1 --branch $2 --model $3"
00185     if [ ! -z "$4" ]; then cmd="${cmd} --args $4"; fi
00186     if [ ${verbose} -gt 0 ]; then
00187         echo "$ ${cmd}"
00188     fi
00189     if [ "${dry}" == 'false' ]; then
00190         if [ ${verbose} -gt 0 ]; then
00191             ${cmd}
00192         else
00193             ${cmd} > /dev/null # avoid messages such as "Your job has been submitted"
00194         fi
00195         return $?
00196     fi
00197     return 0
00198 }
00199 
00200 # ----------------------------------------------------------------------------
00201 ## @brief Convert date to timestamp.
00202 #
00203 # @param [in] date Date.
00204 #
00205 # @returns Prints timestamp corresponding to given date to @c STDOUT.
00206 function date2stamp
00207 {
00208     if [ $(uname) == 'Darwin' ]; then
00209         date -j -f '%Y-%m-%d %T' "$1" +%s
00210     else
00211         date -d "$1" +%s
00212     fi
00213 }
00214 
00215 # ----------------------------------------------------------------------------
00216 ## @brief Convert timestamp to date.
00217 #
00218 # @param [in] stamp Timestamp.
00219 #
00220 # @return Prints date corresponding to given timestamp to @c STDOUT.
00221 function stamp2date
00222 {
00223     if [ $(uname) == 'Darwin' ]; then
00224       date -j -r $1 '+%Y-%m-%d %T'
00225     else
00226       date -d "1970-01-01 $1 sec UTC" '+%Y-%m-%d %T'
00227     fi
00228 }
00229 
00230 # ----------------------------------------------------------------------------
00231 ## @brief Adds a certain time interval to a given date.
00232 #
00233 # @param [in] unit     Unit of the time interval. Either one of -s, -m, -h, or -d.
00234 #                      Defaults to number of days.
00235 # @param [in] date     The date to which the time interval is added.
00236 # @param [in] interval The time interval given in the specified units.
00237 #
00238 # @returns Prints the date which is @p interval time units after the given
00239 #          date to @c STDOUT.
00240 function date_add
00241 {
00242     case $1 in
00243         -s) sec=1;      shift;;
00244         -m) sec=60;     shift;;
00245         -h) sec=3600;   shift;;
00246         -d) sec=86400;  shift;;
00247          *) sec=86400;;
00248     esac
00249     local dte1=$(date2stamp "$1")
00250     local interval=$2
00251     local add_sec=$((dte1 + interval * sec))
00252     echo $(stamp2date "${add_sec}")
00253 }
00254 
00255 # ----------------------------------------------------------------------------
00256 ## @brief Computes the time interval between two given dates.
00257 #
00258 # @param [in] unit  Unit of the time interval. Either one of -s, -m, -h, or -d.
00259 #                   Defaults to number of days.
00260 # @param [in] date1 The first date.
00261 # @param [in] date2 The second date.
00262 #
00263 # @return Prints time interval, i.e., an absolute value, in the given units
00264 #         to @c STDOUT.
00265 function date_diff
00266 {
00267     case $1 in
00268         -s) sec=1;      shift;;
00269         -m) sec=60;     shift;;
00270         -h) sec=3600;   shift;;
00271         -d) sec=86400;  shift;;
00272          *) sec=86400;;
00273     esac
00274     local dte1=$(date2stamp "$1")
00275     local dte2=$(date2stamp "$2")
00276     local interval=$((dte2 - dte1))
00277     echo $((interval / sec))
00278 }
00279 
00280 # ----------------------------------------------------------------------------
00281 ## @brief Get next scheduled date of a given test.
00282 #
00283 # @returns Prints date to @c STDOUT.
00284 function schedule_date
00285 {
00286     local retval=$(date '+%Y-%m-%d %T')
00287     idx=0
00288     numtests=${#schedule[@]}
00289     while [ ${idx} -lt ${numtests} ]; do
00290         parts=(${schedule[${idx}]})
00291         numparts=${#parts[@]}
00292         if [ ${numparts} -lt 5 -o ${numparts} -gt 6 ]; then
00293             continue
00294         fi
00295         if [    "${parts[2]}" == "$1" \
00296              -a "${parts[3]}" == "$2" \
00297              -a "${parts[4]}" == "$3" \
00298              -a "${parts[5]}" == "$4" ]
00299         then
00300             retval="${parts[0]} ${parts[1]}"
00301         fi
00302         ((idx++))
00303     done
00304     echo "${retval}"
00305 }
00306 
00307 # ----------------------------------------------------------------------------
00308 ## @brief Add entry to test schedule.
00309 #
00310 # @param [in] date    The date at which the test should be run next.
00311 # @param [in] time    The time at which the test should be run next.
00312 # @param [in] project Name of the project.
00313 # @param [in] branch  Name of the branch.
00314 # @param [in] model   Name of the model.
00315 #
00316 # @returns Nothing.
00317 function schedule_test
00318 {
00319     idx=${#new_schedule[@]}
00320     new_schedule[${idx}]="$1 $2 $3 $4 $5"
00321 }
00322 
00323 # ============================================================================
00324 # options
00325 # ============================================================================
00326 
00327 test_cmd="${_EXEC_DIR}/basistest-slave" # command used to run tests
00328 verbose=0                               # verbosity of output messages
00329 dry='false'                             # whether this is a dry testing run
00330 
00331 while [ $# -gt 0 ]; do
00332     case "$1" in
00333         -c|--conf)
00334             shift
00335             if [ $# -gt 0 ]; then
00336                 conf_file=$1
00337             else
00338                 echo "Option --conf requires an argument!" 1>&2
00339                 exit 1
00340             fi
00341             ;;
00342         -t|--testcmd)
00343             shift
00344             if [ $# -gt 0 ]; then
00345                 test_cmd=$1
00346             else
00347                 echo "Option --testcmd requires an argument!" 1>&2
00348                 exit 1
00349             fi
00350             ;;
00351         -s|--schedule)
00352             shift
00353             if [ $# -gt 0 ]; then
00354                 schedule_file=$1
00355             else
00356                 echo "Option --schedule requires an argument!" 1>&2
00357                 exit 1
00358             fi
00359             ;;
00360         --dry) dry='true'; ;;
00361 
00362         # standard options
00363         -h|--help)    print_help; exit 0; ;;
00364         -u|--usage)   print_helpshort; exit 0; ;;
00365         -V|--version) print_version ""; exit 0; ;;
00366         -v|--verbose) (( verbose++ )); ;;
00367 
00368         # invalid option
00369         *)
00370             print_helpshort
00371             echo
00372             echo "Invalid option $1!" 1>&2
00373             exit 1
00374             ;;
00375     esac
00376     shift
00377 done
00378 
00379 # ============================================================================
00380 # main
00381 # ============================================================================
00382 
00383 # check existence of configuration file
00384 if [ ! -f "${conf_file}" ]; then
00385     echo "Missing configuration file \"${conf_file}\"" 1>&2
00386     exit 1
00387 fi
00388 
00389 # parse existing test schedule
00390 schedule=()
00391 new_schedule=()
00392 if [ -f "${schedule_file}" ]; then
00393     idx=0
00394     while read line; do
00395         schedule[${idx}]=${line}
00396         ((idx++))
00397     done < ${schedule_file}
00398 fi
00399 
00400 # variables set by readConfLine () which store the configuration for a
00401 # particular test run
00402 minutes=0
00403 hours=0
00404 days=0
00405 project=''
00406 branch=''
00407 model=''
00408 options=''
00409 
00410 # read configuration file line by line
00411 linenumber=0
00412 errors=0
00413 while read line; do
00414     ((linenumber++))
00415     # skip empty lines
00416     if [ -z "${line}" ]; then continue; fi
00417     # skip comments
00418     if [[ "${line}" =~ "^#" ]]; then continue; fi
00419     # sanitize line
00420     line=${line//\*/x}
00421     # "parse" line
00422     parts=(${line})
00423     num=${#parts[@]}
00424     if [ ${num} -lt 4 ]; then
00425         echo "${conf_file}:${linenumber}: Invalid configuration, skipping test" 1>&2
00426         (( errors++ ))
00427         continue
00428     fi
00429     minutes=${parts[0]}
00430     hours=${parts[1]}
00431     days=${parts[2]}
00432     project=${parts[3]}
00433     branch=${parts[4]}
00434     model=${parts[5]}
00435     options=${parts[6]}
00436     # check arguments
00437     if [ -z "${minutes}" -o -z "${hours}" -o -z "${days}" ]; then
00438         echo "${conf_file}:${linenumber}: Invalid configuration, skipping test" 1>&2
00439         (( errors++ ))
00440         continue
00441     fi
00442     if [ "${minutes}" == "x" ]; then minutes=0; fi
00443     if [ "${hours}"   == "x" ]; then hours=0;   fi
00444     if [ "${days}"    == "x" ]; then days=0;   fi
00445     if [ ${minutes} -eq 0 -a ${hours} -eq 0 -a ${days} -eq 0 ]; then
00446         echo "${conf_file}:${linenumber}: Invalid test interval, skipping test" 1>&2
00447         (( errors++ ))
00448         continue
00449     fi
00450     if [ -z "${project}" ]; then
00451         echo "${conf_file}:${linenumber}: No project name given, skipping test" 1>&2
00452         (( errors++ ))
00453         continue
00454     fi
00455     if [ -z "${branch}" ]; then
00456         branch='trunk'
00457     fi
00458     if [ -z "${model}" ]; then
00459         model='Nightly'
00460     fi
00461     # determine whether test is already due for execution
00462     next_date=$(schedule_date ${project} ${branch} ${model} ${options})
00463     if [ $(date_diff -m "$(date '+%Y-%m-%d %T')" "${next_date}") -gt 0 ]; then
00464         if [ ${verbose} -gt 0 ]; then
00465             echo "Next ${model} test of ${project} (${branch}) with options \"${options}\" is scheduled for ${next_date}"
00466         fi
00467         # skip test as it is not yet scheduled for execution
00468         schedule_test "${next_date}" "${project}" "${branch}" "${model}" "${options}"
00469         continue
00470     fi
00471     # run test
00472     run_test "${project}" "${branch}" "${model}" "${options}"
00473     if [ $? -ne 0 ]; then
00474         echo "${conf_file}:${linenumber}: Failed to run test" 1>&2
00475         (( errors++ ))
00476         # do not retry failing test too often
00477         minutes=0
00478         hours=1
00479         days=0
00480     fi
00481     # update time of next execution
00482     minutes=$((minutes + hours * 60 + days * 1440))
00483     next_date=$(date_add -m "$(date '+%Y-%m-%d %T')" "${minutes}")
00484     schedule_test "${next_date}" "${project}" "${branch}" "${model}" "${options}"
00485     if [ $? -ne 0 ]; then
00486         echo "${conf_file}:${linenumber}: Failed to reschedule test" 1>&2
00487         (( errors++ ))
00488     fi
00489     if [ ${verbose} -gt 0 ]; then
00490         echo "Test will re-execute in ${minutes} minutes from now ($(date '+%Y-%m-%d %T')), i.e., not before ${next_date}"
00491     fi
00492 done < "${conf_file}"
00493 
00494 # write new schedule to temporary file
00495 idx=0
00496 num=${#new_schedule[@]}
00497 if [ -f "${schedule_file}.temp" ]; then
00498     rm -f "${schedule_file}.temp"
00499 fi
00500 while [ ${idx} -lt ${num} ]; do
00501     echo ${new_schedule[${idx}]} >> "${schedule_file}.temp"
00502     if [ $? -ne 0 ]; then
00503         echo "Failed to write schedule to temporary file \"${schedule_file}.temp\"!" 1>&2
00504         exit 1;
00505     fi
00506     (( idx++ ))
00507 done
00508 # sort schedule
00509 if [ -f "${schedule_file}.temp" ]; then
00510     sort "${schedule_file}.temp" -o "${schedule_file}.temp"
00511     if [ $? -ne 0 ]; then
00512         echo "Failed to sort temporary schedule file \"${schedule_file}.temp\"!" 1>&2
00513         exit 1
00514     fi
00515 fi
00516 # and then replace previous schedule file
00517 if [ -f "${schedule_file}.temp" ]; then
00518     mv -f "${schedule_file}.temp" "${schedule_file}"
00519     if [ $? -ne 0 ]; then
00520         echo "Failed to update schedule file \"${schedule_file}\"!" 1>&2
00521         exit 1
00522     fi
00523 fi
00524 
00525 # done
00526 [ ${errors} -eq 0 ]