#!/bin/bash # Copyright (C) The Arvados Authors. All rights reserved. # # SPDX-License-Identifier: AGPL-3.0 . `dirname "$(readlink -f "$0")"`/libcloud-pin.sh COLUMNS=80 . `dirname "$(readlink -f "$0")"`/run-library.sh read -rd "\000" helpmessage <&2 "Fatal: $* (encountered in ${FUNCNAME[1]} at ${BASH_SOURCE[1]} line ${BASH_LINENO[0]})" exit 1 } exit_cleanly() { trap - INT create-plot-data-from-log.sh $BUILD_NUMBER "$WORKSPACE/apps/workbench/log/test.log" "$WORKSPACE/apps/workbench/log/" rotate_logfile "$WORKSPACE/apps/workbench/log/" "test.log" stop_services rotate_logfile "$WORKSPACE/services/api/log/" "test.log" report_outcomes clear_temp exit ${#failures} } sanity_checks() { ( [[ -n "$WORKSPACE" ]] && [[ -d "$WORKSPACE/services" ]] ) \ || fatal "WORKSPACE environment variable not set to a source directory (see: $0 --help)" echo Checking dependencies: echo -n 'virtualenv: ' virtualenv --version \ || fatal "No virtualenv. Try: apt-get install virtualenv (on ubuntu: python-virtualenv)" echo -n 'ruby: ' ruby -v \ || fatal "No ruby. Install >=2.1.9 (using rbenv, rvm, or source)" echo -n 'go: ' go version \ || fatal "No go binary. See http://golang.org/doc/install" [[ $(go version) =~ go1.([0-9]+) ]] && [[ ${BASH_REMATCH[1]} -ge 8 ]] \ || fatal "Go >= 1.8 required. See http://golang.org/doc/install" echo -n 'gcc: ' gcc --version | egrep ^gcc \ || fatal "No gcc. Try: apt-get install build-essential" echo -n 'fuse.h: ' find /usr/include -path '*fuse/fuse.h' | egrep --max-count=1 . \ || fatal "No fuse/fuse.h. Try: apt-get install libfuse-dev" echo -n 'gnutls.h: ' find /usr/include -path '*gnutls/gnutls.h' | egrep --max-count=1 . \ || fatal "No gnutls/gnutls.h. Try: apt-get install libgnutls28-dev" echo -n 'Python2 pyconfig.h: ' find /usr/include -path '*/python2*/pyconfig.h' | egrep --max-count=1 . \ || fatal "No Python2 pyconfig.h. Try: apt-get install python2.7-dev" echo -n 'Python3 pyconfig.h: ' find /usr/include -path '*/python3*/pyconfig.h' | egrep --max-count=1 . \ || fatal "No Python3 pyconfig.h. Try: apt-get install python3-dev" echo -n 'nginx: ' PATH="$PATH:/sbin:/usr/sbin:/usr/local/sbin" nginx -v \ || fatal "No nginx. Try: apt-get install nginx" echo -n 'perl: ' perl -v | grep version \ || fatal "No perl. Try: apt-get install perl" for mod in ExtUtils::MakeMaker JSON LWP Net::SSL; do echo -n "perl $mod: " perl -e "use $mod; print \"\$$mod::VERSION\\n\"" \ || fatal "No $mod. Try: apt-get install perl-modules libcrypt-ssleay-perl libjson-perl libwww-perl" done echo -n 'gitolite: ' which gitolite \ || fatal "No gitolite. Try: apt-get install gitolite3" echo -n 'npm: ' npm --version \ || fatal "No npm. Try: wget -O- https://nodejs.org/dist/v6.11.2/node-v6.11.2-linux-x64.tar.xz | sudo tar -C /usr/local -xJf - && sudo ln -s ../node-v6.11.2-linux-x64/bin/{node,npm} /usr/local/bin/" echo -n 'cadaver: ' cadaver --version | grep -w cadaver \ || fatal "No cadaver. Try: apt-get install cadaver" echo -n 'libattr1 xattr.h: ' find /usr/include -path '*/attr/xattr.h' | egrep --max-count=1 . \ || fatal "No libattr1 xattr.h. Try: apt-get install libattr1-dev" echo -n 'libcurl curl.h: ' find /usr/include -path '*/curl/curl.h' | egrep --max-count=1 . \ || fatal "No libcurl curl.h. Try: apt-get install libcurl4-gnutls-dev" echo -n 'libpq libpq-fe.h: ' find /usr/include -path '*/postgresql/libpq-fe.h' | egrep --max-count=1 . \ || fatal "No libpq libpq-fe.h. Try: apt-get install libpq-dev" echo -n 'services/api/config/database.yml: ' if [[ ! -f "$WORKSPACE/services/api/config/database.yml" ]]; then fatal "Please provide a database.yml file for the test suite" else echo "OK" fi echo -n 'postgresql: ' psql --version || fatal "No postgresql. Try: apt-get install postgresql postgresql-client-common" echo -n 'phantomjs: ' phantomjs --version || fatal "No phantomjs. Try: apt-get install phantomjs" echo -n 'xvfb: ' which Xvfb || fatal "No xvfb. Try: apt-get install xvfb" echo -n 'graphviz: ' dot -V || fatal "No graphviz. Try: apt-get install graphviz" } rotate_logfile() { # i.e. rotate_logfile "$WORKSPACE/apps/workbench/log/" "test.log" # $BUILD_NUMBER is set by Jenkins if this script is being called as part of a Jenkins run if [[ -f "$1/$2" ]]; then THEDATE=`date +%Y%m%d%H%M%S` mv "$1/$2" "$1/$THEDATE-$BUILD_NUMBER-$2" gzip "$1/$THEDATE-$BUILD_NUMBER-$2" fi } declare -a failures declare -A skip declare -A testargs skip[apps/workbench_profile]=1 while [[ -n "$1" ]] do arg="$1"; shift case "$arg" in --help) echo >&2 "$helpmessage" echo >&2 exit 1 ;; --skip) skip[$1]=1; shift ;; --only) only="$1"; skip[$1]=""; shift ;; --short) short=1 ;; --skip-install) only_install=nothing ;; --only-install) only_install="$1"; shift ;; --temp) temp="$1"; shift temp_preserve=1 ;; --leave-temp) temp_preserve=1 ;; --repeat) repeat=$((${1}+0)); shift ;; --retry) retry=1 ;; *_test=*) suite="${arg%%_test=*}" args="${arg#*=}" testargs["$suite"]="$args" ;; *=*) eval export $(echo $arg | cut -d= -f1)=\"$(echo $arg | cut -d= -f2-)\" ;; *) echo >&2 "$0: Unrecognized option: '$arg'. Try: $0 --help" exit 1 ;; esac done start_services() { echo 'Starting API, keepproxy, keep-web, ws, arv-git-httpd, and nginx ssl proxy...' if [[ ! -d "$WORKSPACE/services/api/log" ]]; then mkdir -p "$WORKSPACE/services/api/log" fi # Remove empty api.pid file if it exists if [[ -f "$WORKSPACE/tmp/api.pid" && ! -s "$WORKSPACE/tmp/api.pid" ]]; then rm -f "$WORKSPACE/tmp/api.pid" fi cd "$WORKSPACE" \ && eval $(python sdk/python/tests/run_test_server.py start --auth admin) \ && export ARVADOS_TEST_API_HOST="$ARVADOS_API_HOST" \ && export ARVADOS_TEST_API_INSTALLED="$$" \ && python sdk/python/tests/run_test_server.py start_keep_proxy \ && python sdk/python/tests/run_test_server.py start_keep-web \ && python sdk/python/tests/run_test_server.py start_arv-git-httpd \ && python sdk/python/tests/run_test_server.py start_ws \ && python sdk/python/tests/run_test_server.py start_nginx \ && (env | egrep ^ARVADOS) } stop_services() { if [[ -z "$ARVADOS_TEST_API_HOST" ]]; then return fi unset ARVADOS_TEST_API_HOST cd "$WORKSPACE" \ && python sdk/python/tests/run_test_server.py stop_nginx \ && python sdk/python/tests/run_test_server.py stop_arv-git-httpd \ && python sdk/python/tests/run_test_server.py stop_ws \ && python sdk/python/tests/run_test_server.py stop_keep-web \ && python sdk/python/tests/run_test_server.py stop_keep_proxy \ && python sdk/python/tests/run_test_server.py stop } interrupt() { failures+=("($(basename $0) interrupted)") exit_cleanly } trap interrupt INT sanity_checks echo "WORKSPACE=$WORKSPACE" if [[ -z "$CONFIGSRC" ]] && [[ -d "$HOME/arvados-api-server" ]]; then # Jenkins expects us to use this by default. CONFIGSRC="$HOME/arvados-api-server" fi # Clean up .pyc files that may exist in the workspace cd "$WORKSPACE" find -name '*.pyc' -delete if [[ -z "$temp" ]]; then temp="$(mktemp -d)" fi # Set up temporary install dirs (unless existing dirs were supplied) for tmpdir in VENVDIR VENV3DIR GOPATH GEMHOME PERLINSTALLBASE do if [[ -z "${!tmpdir}" ]]; then eval "$tmpdir"="$temp/$tmpdir" fi if ! [[ -d "${!tmpdir}" ]]; then mkdir "${!tmpdir}" || fatal "can't create ${!tmpdir} (does $temp exist?)" fi done setup_ruby_environment() { if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then source "$HOME/.rvm/scripts/rvm" using_rvm=true elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then source "/usr/local/rvm/scripts/rvm" using_rvm=true else using_rvm=false fi if [[ "$using_rvm" == true ]]; then # If rvm is in use, we can't just put separate "dependencies" # and "gems-under-test" paths to GEM_PATH: passenger resets # the environment to the "current gemset", which would lose # our GEM_PATH and prevent our test suites from running ruby # programs (for example, the Workbench test suite could not # boot an API server or run arv). Instead, we have to make an # rvm gemset and use it for everything. [[ `type rvm | head -n1` == "rvm is a function" ]] \ || fatal 'rvm check' # Put rvm's favorite path back in first place (overriding # virtualenv, which just put itself there). Ignore rvm's # complaint about not being in first place already. rvm use @default 2>/dev/null # Create (if needed) and switch to an @arvados-tests-* gemset, # salting the gemset name so it doesn't interfere with # concurrent builds in other workspaces. Leave the choice of # ruby to the caller. gemset="arvados-tests-$(echo -n "${WORKSPACE}" | md5sum | head -c16)" rvm use "@${gemset}" --create \ || fatal 'rvm gemset setup' rvm env else # When our "bundle install"s need to install new gems to # satisfy dependencies, we want them to go where "gem install # --user-install" would put them. (However, if the caller has # already set GEM_HOME, we assume that's where dependencies # should be installed, and we should leave it alone.) if [ -z "$GEM_HOME" ]; then user_gempath="$(gem env gempath)" export GEM_HOME="${user_gempath%%:*}" fi PATH="$(gem env gemdir)/bin:$PATH" # When we build and install our own gems, we install them in our # $GEMHOME tmpdir, and we want them to be at the front of GEM_PATH and # PATH so integration tests prefer them over other versions that # happen to be installed in $user_gempath, system dirs, etc. tmpdir_gem_home="$(env - PATH="$PATH" HOME="$GEMHOME" gem env gempath | cut -f1 -d:)" PATH="$tmpdir_gem_home/bin:$PATH" export GEM_PATH="$tmpdir_gem_home" echo "Will install dependencies to $(gem env gemdir)" echo "Will install arvados gems to $tmpdir_gem_home" echo "Gem search path is GEM_PATH=$GEM_PATH" fi bundle config || gem install bundler \ || fatal 'install bundler' } with_test_gemset() { if [[ "$using_rvm" == true ]]; then "$@" else GEM_HOME="$tmpdir_gem_home" GEM_PATH="$tmpdir_gem_home" "$@" fi } gem_uninstall_if_exists() { if gem list "$1\$" | egrep '^\w'; then gem uninstall --force --all --executables "$1" fi } setup_virtualenv() { local venvdest="$1"; shift if ! [[ -e "$venvdest/bin/activate" ]] || ! [[ -e "$venvdest/bin/pip" ]]; then virtualenv --setuptools "$@" "$venvdest" || fatal "virtualenv $venvdest failed" fi if [[ $("$venvdest/bin/python" --version 2>&1) =~ \ 3\.[012]\. ]]; then # pip 8.0.0 dropped support for python 3.2, e.g., debian wheezy "$venvdest/bin/pip" install 'setuptools>=18.5' 'pip>=7,<8' else "$venvdest/bin/pip" install 'setuptools>=18.5' 'pip>=7' fi # ubuntu1404 can't seem to install mock via tests_require, but it can do this. "$venvdest/bin/pip" install 'mock>=1.0' 'pbr<1.7.0' } export PERLINSTALLBASE export PERLLIB="$PERLINSTALLBASE/lib/perl5:${PERLLIB:+$PERLLIB}" export GOPATH mkdir -p "$GOPATH/src/git.curoverse.com" rmdir -v --parents --ignore-fail-on-non-empty "$GOPATH/src/git.curoverse.com/arvados.git/tmp/GOPATH" for d in \ "$GOPATH/src/git.curoverse.com/arvados.git/arvados.git" \ "$GOPATH/src/git.curoverse.com/arvados.git"; do [[ -d "$d" ]] && rmdir "$d" [[ -h "$d" ]] && rm "$d" done ln -vsnfT "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git" \ || fatal "symlink failed" go get -v github.com/kardianos/govendor \ || fatal "govendor install failed" setup_virtualenv "$VENVDIR" --python python2.7 . "$VENVDIR/bin/activate" # Needed for run_test_server.py which is used by certain (non-Python) tests. pip freeze 2>/dev/null | egrep ^PyYAML= \ || pip install PyYAML >/dev/null \ || fatal "pip install PyYAML failed" # Preinstall forked version of libcloud, because nodemanager "pip install" # won't pick it up by default. pip freeze 2>/dev/null | egrep ^apache-libcloud==$LIBCLOUD_PIN \ || pip install --pre --ignore-installed https://github.com/curoverse/libcloud/archive/apache-libcloud-$LIBCLOUD_PIN.zip >/dev/null \ || fatal "pip install apache-libcloud failed" # We need an unreleased (as of 2017-08-17) llfuse bugfix, otherwise our fuse test suite deadlocks. pip freeze | grep -x llfuse==1.2.0 || ( set -e yes | pip uninstall llfuse || true cython --version || fatal "no cython; try sudo apt-get install cython" cd "$temp" (cd python-llfuse 2>/dev/null || git clone https://github.com/curoverse/python-llfuse) cd python-llfuse git checkout 620722fd990ea642ddb8e7412676af482c090c0c git checkout setup.py sed -i -e "s:'1\\.2':'1.2.0':" setup.py python setup.py build_cython python setup.py install --force ) || fatal "llfuse fork failed" pip freeze | grep -x llfuse==1.2.0 || fatal "error: installed llfuse 1.2.0 but '$(pip freeze | grep llfuse)' ???" # Deactivate Python 2 virtualenv deactivate declare -a pythonstuff pythonstuff=( sdk/pam sdk/python sdk/python:py3 sdk/cwl services/dockercleaner:py3 services/fuse services/nodemanager tools/crunchstat-summary ) # If Python 3 is available, set up its virtualenv in $VENV3DIR. # Otherwise, skip dependent tests. PYTHON3=$(which python3) if [[ ${?} = 0 ]]; then setup_virtualenv "$VENV3DIR" --python python3 else PYTHON3= cat >&2 </dev/null then gem install --user-install bundler || fatal 'Could not install bundler' fi retry() { remain="${repeat}" while : do if ${@}; then if [[ "$remain" -gt 1 ]]; then remain=$((${remain}-1)) title "Repeating ${remain} more times" else break fi elif [[ "$retry" == 1 ]]; then read -p 'Try again? [Y/n] ' x if [[ "$x" != "y" ]] && [[ "$x" != "" ]] then break fi else break fi done } do_test() { case "${1}" in apps/workbench_units | apps/workbench_functionals | apps/workbench_integration) suite=apps/workbench ;; services/nodemanager | services/nodemanager_integration) suite=services/nodemanager_suite ;; *) suite="${1}" ;; esac if [[ -z "${skip[$suite]}" && -z "${skip[$1]}" && \ (-z "${only}" || "${only}" == "${suite}" || \ "${only}" == "${1}") ]]; then retry do_test_once ${@} else title "Skipping ${1} tests" fi } do_test_once() { unset result title "Running $1 tests" timer_reset if [[ "$2" == "go" ]] then covername="coverage-$(echo "$1" | sed -e 's/\//_/g')" coverflags=("-covermode=count" "-coverprofile=$WORKSPACE/tmp/.$covername.tmp") # We do "go get -t" here to catch compilation errors # before trying "go test". Otherwise, coverage-reporting # mode makes Go show the wrong line numbers when reporting # compilation errors. go get -t "git.curoverse.com/arvados.git/$1" && \ cd "$GOPATH/src/git.curoverse.com/arvados.git/$1" && \ [[ -z "$(gofmt -e -d . | tee /dev/stderr)" ]] && \ if [[ -n "${testargs[$1]}" ]] then # "go test -check.vv giturl" doesn't work, but this # does: go test ${short:+-short} ${testargs[$1]} else # The above form gets verbose even when testargs is # empty, so use this form in such cases: go test ${short:+-short} ${coverflags[@]} "git.curoverse.com/arvados.git/$1" fi result=${result:-$?} if [[ -f "$WORKSPACE/tmp/.$covername.tmp" ]] then go tool cover -html="$WORKSPACE/tmp/.$covername.tmp" -o "$WORKSPACE/tmp/$covername.html" rm "$WORKSPACE/tmp/.$covername.tmp" fi elif [[ "$2" == "pip" ]] then tries=0 cd "$WORKSPACE/$1" && while : do tries=$((${tries}+1)) # $3 can name a path directory for us to use, including trailing # slash; e.g., the bin/ subdirectory of a virtualenv. "${3}python" setup.py ${short:+--short-tests-only} test ${testargs[$1]} result=$? if [[ ${tries} < 3 && ${result} == 137 ]] then printf '\n*****\n%s tests killed -- retrying\n*****\n\n' "$1" continue else break fi done elif [[ "$2" != "" ]] then "test_$2" else "test_$1" fi result=${result:-$?} checkexit $result "$1 tests" title "End of $1 tests (`timer`)" return $result } do_install() { if [[ -z "${only_install}" || "${only_install}" == "${1}" ]]; then retry do_install_once ${@} else title "Skipping $1 install" fi } do_install_once() { title "Running $1 install" timer_reset if [[ "$2" == "go" ]] then go get -t "git.curoverse.com/arvados.git/$1" elif [[ "$2" == "pip" ]] then # $3 can name a path directory for us to use, including trailing # slash; e.g., the bin/ subdirectory of a virtualenv. # Need to change to a different directory after creating # the source dist package to avoid a pip bug. # see https://arvados.org/issues/5766 for details. # Also need to install twice, because if it believes the package is # already installed, pip it won't install it. So the first "pip # install" ensures that the dependencies are met, the second "pip # install" ensures that we've actually installed the local package # we just built. cd "$WORKSPACE/$1" \ && "${3}python" setup.py sdist rotate --keep=1 --match .tar.gz \ && cd "$WORKSPACE" \ && "${3}pip" install --quiet "$WORKSPACE/$1/dist"/*.tar.gz \ && "${3}pip" install --quiet --no-deps --ignore-installed "$WORKSPACE/$1/dist"/*.tar.gz elif [[ "$2" != "" ]] then "install_$2" else "install_$1" fi result=$? checkexit $result "$1 install" title "End of $1 install (`timer`)" return $result } bundle_install_trylocal() { ( set -e echo "(Running bundle install --local. 'could not find package' messages are OK.)" if ! bundle install --local --no-deployment; then echo "(Running bundle install again, without --local.)" bundle install --no-deployment fi bundle package --all ) } install_doc() { cd "$WORKSPACE/doc" \ && bundle_install_trylocal \ && rm -rf .site } do_install doc install_gem() { gemname=$1 srcpath=$2 with_test_gemset gem_uninstall_if_exists "$gemname" \ && cd "$WORKSPACE/$srcpath" \ && bundle_install_trylocal \ && gem build "$gemname.gemspec" \ && with_test_gemset gem install --no-ri --no-rdoc $(ls -t "$gemname"-*.gem|head -n1) } install_ruby_sdk() { install_gem arvados sdk/ruby } do_install sdk/ruby ruby_sdk install_perl_sdk() { cd "$WORKSPACE/sdk/perl" \ && perl Makefile.PL INSTALL_BASE="$PERLINSTALLBASE" \ && make install INSTALLDIRS=perl } do_install sdk/perl perl_sdk install_cli() { install_gem arvados-cli sdk/cli } do_install sdk/cli cli install_login-sync() { install_gem arvados-login-sync services/login-sync } do_install services/login-sync login-sync # Install the Python SDK early. Various other test suites (like # keepproxy) bring up run_test_server.py, which imports the arvados # module. We can't actually *test* the Python SDK yet though, because # its own test suite brings up some of those other programs (like # keepproxy). for p in "${pythonstuff[@]}" do dir=${p%:py3} if [[ ${dir} = ${p} ]]; then if [[ -z ${skip[python2]} ]]; then do_install ${dir} pip fi elif [[ -n ${PYTHON3} ]]; then if [[ -z ${skip[python3]} ]]; then do_install ${dir} pip "$VENV3DIR/bin/" fi fi done install_apiserver() { cd "$WORKSPACE/services/api" \ && RAILS_ENV=test bundle_install_trylocal rm -f config/environments/test.rb cp config/environments/test.rb.example config/environments/test.rb if [ -n "$CONFIGSRC" ] then for f in database.yml do cp "$CONFIGSRC/$f" config/ || fatal "$f" done fi # Clear out any lingering postgresql connections to the test # database, so that we can drop it. This assumes the current user # is a postgresql superuser. cd "$WORKSPACE/services/api" \ && test_database=$(python -c "import yaml; print yaml.load(file('config/database.yml'))['test']['database']") \ && psql "$test_database" -c "SELECT pg_terminate_backend (pg_stat_activity.procpid::int) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$test_database';" 2>/dev/null mkdir -p "$WORKSPACE/services/api/tmp/pids" cert="$WORKSPACE/services/api/tmp/self-signed" if [[ ! -e "$cert.pem" || "$(date -r "$cert.pem" +%s)" -lt 1512659226 ]]; then ( dir="$WORKSPACE/services/api/tmp" set -ex openssl req -newkey rsa:2048 -nodes -subj '/C=US/ST=State/L=City/CN=localhost' -out "$cert.csr" -keyout "$cert.key"