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