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 ]