refs #8558
[arvados-dev.git] / jenkins / run-tests.sh
index 5830565c4cc6ffe60f4172169a0098f3710b6552..3b3ddff053dd8011a77513ab621d76a26cb54f10 100755 (executable)
@@ -1,5 +1,7 @@
 #!/bin/bash
 
 #!/bin/bash
 
+. `dirname "$(readlink -f "$0")"`/libcloud-pin
+
 read -rd "\000" helpmessage <<EOF
 $(basename $0): Install and test Arvados components.
 
 read -rd "\000" helpmessage <<EOF
 $(basename $0): Install and test Arvados components.
 
@@ -12,20 +14,27 @@ Options:
 
 --skip FOO     Do not test the FOO component.
 --only FOO     Do not test anything except the FOO component.
 
 --skip FOO     Do not test the FOO component.
 --only FOO     Do not test anything except the FOO component.
+--temp DIR     Install components and dependencies under DIR instead of
+               making a new temporary directory. Implies --leave-temp.
 --leave-temp   Do not remove GOPATH, virtualenv, and other temp dirs at exit.
 --leave-temp   Do not remove GOPATH, virtualenv, and other temp dirs at exit.
-               Instead, show which directories were used this time so they
-               can be reused in subsequent invocations.
+               Instead, show the path to give as --temp to reuse them in
+               subsequent invocations.
 --skip-install Do not run any install steps. Just run tests.
                You should provide GOPATH, GEMHOME, and VENVDIR options
                from a previous invocation if you use this option.
 --skip-install Do not run any install steps. Just run tests.
                You should provide GOPATH, GEMHOME, and VENVDIR options
                from a previous invocation if you use this option.
+--only-install Run specific install step
 WORKSPACE=path Arvados source tree to test.
 CONFIGSRC=path Dir with api server config files to copy into source tree.
 WORKSPACE=path Arvados source tree to test.
 CONFIGSRC=path Dir with api server config files to copy into source tree.
-apiserver_test="TEST=test/functional/arvados/v1/collections_test.rb"
+               (If none given, leave config files alone in source tree.)
+services/api_test="TEST=test/functional/arvados/v1/collections_controller_test.rb"
                Restrict apiserver tests to the given file
                Restrict apiserver tests to the given file
-python_sdk_test="--test-suite test.test_keep_locator"
+sdk/python_test="--test-suite test.test_keep_locator"
                Restrict Python SDK tests to the given class
                Restrict Python SDK tests to the given class
-workbench_test="TEST=test/integration/pipeline_instances_test.rb"
+apps/workbench_test="TEST=test/integration/pipeline_instances_test.rb"
                Restrict Workbench tests to the given file
                Restrict Workbench tests to the given file
+services/arv-git-httpd_test="-check.vv"
+               Show all log messages, even when tests pass (also works
+               with services/keepstore_test etc.)
 ARVADOS_DEBUG=1
                Print more debug messages
 envvar=value   Set \$envvar to value. Primarily useful for WORKSPACE,
 ARVADOS_DEBUG=1
                Print more debug messages
 envvar=value   Set \$envvar to value. Primarily useful for WORKSPACE,
@@ -36,9 +45,44 @@ into \$GOPATH, \$VENDIR, and \$GEMHOME before running any tests. Many
 test suites depend on other components being installed, and installing
 everything tends to be quicker than debugging dependencies.
 
 test suites depend on other components being installed, and installing
 everything tends to be quicker than debugging dependencies.
 
+As a special concession to the current CI server config, CONFIGSRC
+defaults to $HOME/arvados-api-server if that directory exists.
+
 More information and background:
 
 https://arvados.org/projects/arvados/wiki/Running_tests
 More information and background:
 
 https://arvados.org/projects/arvados/wiki/Running_tests
+
+Available tests:
+
+apps/workbench
+apps/workbench_benchmark
+apps/workbench_profile
+doc
+services/api
+services/arv-git-httpd
+services/crunchstat
+services/dockercleaner
+services/fuse
+services/keep-web
+services/keepproxy
+services/keepstore
+services/login-sync
+services/nodemanager
+services/crunch-dispatch-local
+sdk/cli
+sdk/pam
+sdk/python
+sdk/ruby
+sdk/go/arvadosclient
+sdk/go/keepclient
+sdk/go/manifest
+sdk/go/blockdigest
+sdk/go/streamer
+sdk/go/crunchrunner
+sdk/cwl
+tools/crunchstat-summary
+tools/keep-rsync
+
 EOF
 
 # First make sure to remove any ARVADOS_ variables from the calling
 EOF
 
 # First make sure to remove any ARVADOS_ variables from the calling
@@ -50,48 +94,31 @@ unset $(env | cut -d= -f1 | grep \^ARVADOS_)
 GITDIR=
 GOPATH=
 VENVDIR=
 GITDIR=
 GOPATH=
 VENVDIR=
+VENV3DIR=
 PYTHONPATH=
 GEMHOME=
 PYTHONPATH=
 GEMHOME=
+PERLINSTALLBASE=
 
 COLUMNS=80
 
 
 COLUMNS=80
 
-cli_test=
-workbench_test=
-apiserver_test=
-python_sdk_test=
-ruby_sdk_test=
-fuse_test=
-leave_temp=
 skip_install=
 skip_install=
+temp=
+temp_preserve=
 
 
-if [[ -f /etc/profile.d/rvm.sh ]]
-then
-    source /etc/profile.d/rvm.sh
-fi
-
-declare -A leave_temp
 clear_temp() {
 clear_temp() {
-    leaving=""
-    for var in VENVDIR GOPATH GITDIR GEMHOME
-    do
-        if [[ -z "${leave_temp[$var]}" ]]
-        then
-            if [[ -n "${!var}" ]]
-            then
-                rm -rf "${!var}"
-            fi
-        else
-            leaving+=" $var=\"${!var}\""
-        fi
-    done
-    if [[ -z "$leaving" ]]; then
-        echo "Leaving behind temp dirs: $leaving"
+    if [[ -z "$temp" ]]; then
+        # we didn't even get as far as making a temp dir
+        :
+    elif [[ -z "$temp_preserve" ]]; then
+        rm -rf "$temp"
+    else
+        echo "Leaving behind temp dirs in $temp"
     fi
 }
 
 fatal() {
     clear_temp
     fi
 }
 
 fatal() {
     clear_temp
-    echo >&2 "Fatal: $* in ${FUNCNAME[1]} at ${BASH_SOURCE[1]} line ${BASH_LINENO[0]}"
+    echo >&2 "Fatal: $* (encountered in ${FUNCNAME[1]} at ${BASH_SOURCE[1]} line ${BASH_LINENO[0]})"
     exit 1
 }
 
     exit 1
 }
 
@@ -112,11 +139,67 @@ report_outcomes() {
         done
     fi
 }
         done
     fi
 }
+
+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 'go: '
+    go version \
+        || fatal "No go binary. 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 -wholename '*fuse/fuse.h' \
+        || fatal "No fuse/fuse.h. Try: apt-get install libfuse-dev"
+    echo -n 'pyconfig.h: '
+    find /usr/include -name pyconfig.h | egrep --max-count=1 . \
+        || fatal "No pyconfig.h. Try: apt-get install python-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"
+}
+
+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 failures
 declare -A skip
-
-# Always skip CLI tests. They don't know how to use run_test_server.py.
-skip[cli]=1
+declare -A testargs
+skip[apps/workbench_profile]=1
 
 while [[ -n "$1" ]]
 do
 
 while [[ -n "$1" ]]
 do
@@ -132,18 +215,32 @@ do
             skip[$skipwhat]=1
             ;;
         --only)
             skip[$skipwhat]=1
             ;;
         --only)
-            only="$1"; shift
+            only="$1"; skip[$1]=""; shift
             ;;
         --skip-install)
             skip_install=1
             ;;
             ;;
         --skip-install)
             skip_install=1
             ;;
+        --only-install)
+            skip_install=1
+            only_install="$1"; shift
+            ;;
+        --temp)
+            temp="$1"; shift
+            temp_preserve=1
+            ;;
         --leave-temp)
         --leave-temp)
-            leave_temp[VENVDIR]=1
-            leave_temp[GOPATH]=1
-            leave_temp[GEMHOME]=1
+            temp_preserve=1
+            ;;
+        --retry)
+            retry=1
+            ;;
+        *_test=*)
+            suite="${arg%%_test=*}"
+            args="${arg#*=}"
+            testargs["$suite"]="$args"
             ;;
         *=*)
             ;;
         *=*)
-            eval $(echo $arg | cut -d= -f1)=\"$(echo $arg | cut -d= -f2-)\"
+            eval export $(echo $arg | cut -d= -f1)=\"$(echo $arg | cut -d= -f2-)\"
             ;;
         *)
             echo >&2 "$0: Unrecognized option: '$arg'. Try: $0 --help"
             ;;
         *)
             echo >&2 "$0: Unrecognized option: '$arg'. Try: $0 --help"
@@ -152,41 +249,222 @@ do
     esac
 done
 
     esac
 done
 
-# Sanity check
-echo "WORKSPACE=$WORKSPACE"
-[[ -n "$WORKSPACE" ]] || fatal "WORKSPACE not set"
+start_api() {
+    echo 'Starting API server...'
+    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="$$" \
+        && (env | egrep ^ARVADOS)
+}
 
 
-if [[ -n "$CONFIGSRC" ]]; then
-    if [[ -d "$HOME/arvados-api-server" ]]; then
-        # Jenkins expects us to use this by default.
-        CONFIGSRC="$HOME/arvados-api-server"
+start_nginx_proxy_services() {
+    echo 'Starting keepproxy, keep-web, arv-git-httpd, and nginx ssl proxy...'
+    cd "$WORKSPACE" \
+        && 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_nginx \
+        && export ARVADOS_TEST_PROXY_SERVICES=1
+}
+
+stop_services() {
+    if [[ -n "$ARVADOS_TEST_PROXY_SERVICES" ]]; then
+        unset ARVADOS_TEST_PROXY_SERVICES
+        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_keep-web \
+            && python sdk/python/tests/run_test_server.py stop_keep_proxy
     fi
     fi
+    if [[ -n "$ARVADOS_TEST_API_HOST" ]]; then
+        unset ARVADOS_TEST_API_HOST
+        cd "$WORKSPACE" \
+            && python sdk/python/tests/run_test_server.py stop
+    fi
+}
+
+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)
 fi
 
 # Set up temporary install dirs (unless existing dirs were supplied)
-for tmpdir in VENVDIR GOPATH GEMHOME
+for tmpdir in VENVDIR VENV3DIR GOPATH GEMHOME PERLINSTALLBASE
 do
 do
-    if [[ -n "${!tmpdir}" ]]; then
-        leave_temp[$tmpdir]=1
-    else
-        eval $tmpdir=$(mktemp -d)
+    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
     fi
 done
-PATH="$GEMHOME/.gem/ruby/2.1.0/bin:$PATH"
+
+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. (Leave the choice of ruby to the caller.)
+        rvm use @arvados-tests --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:$(gem env gempath)"
+
+        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
+}
+
+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
+    "$venvdest/bin/pip" install 'setuptools>=18' 'pip>=7'
+    # 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"
 ln -sfn "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git" \
     || fatal "symlink failed"
 
 export GOPATH
 mkdir -p "$GOPATH/src/git.curoverse.com"
 ln -sfn "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git" \
     || fatal "symlink failed"
 
-virtualenv --setuptools "$VENVDIR" || fatal "virtualenv $VENVDIR failed"
-PATH="$VENVDIR/bin:$PATH"
+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"
+
+# Deactivate Python 2 virtualenv
+deactivate
+
+# 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=
+    skip[services/dockercleaner]=1
+    cat >&2 <<EOF
+
+Warning: python3 could not be found
+services/dockercleaner install and tests will be skipped
+
+EOF
+fi
+
+# Reactivate Python 2 virtualenv
+. "$VENVDIR/bin/activate"
+
+# Note: this must be the last time we change PATH, otherwise rvm will
+# whine a lot.
+setup_ruby_environment
+
+echo "PATH is $PATH"
+
+if ! which bundler >/dev/null
+then
+    gem install --user-install bundler || fatal 'Could not install bundler'
+fi
 
 checkexit() {
 
 checkexit() {
-    if [[ "$?" != "0" ]]; then
-        title "!!!!!! $1 FAILED !!!!!!"
-        failures+=("$1 (`timer`)")
+    if [[ "$1" != "0" ]]; then
+        title "!!!!!! $2 FAILED !!!!!!"
+        failures+=("$2 (`timer`)")
     else
     else
-        successes+=("$1 (`timer`)")
+        successes+=("$2 (`timer`)")
     fi
 }
 
     fi
 }
 
@@ -199,35 +477,101 @@ timer() {
 }
 
 do_test() {
 }
 
 do_test() {
+    while ! do_test_once ${@} && [[ "$retry" == 1 ]]
+    do
+        read -p 'Try again? [Y/n] ' x
+        if [[ "$x" != "y" ]] && [[ "$x" != "" ]]
+        then
+            break
+        fi
+    done
+}
+
+do_test_once() {
+    unset result
     if [[ -z "${skip[$1]}" ]] && ( [[ -z "$only" ]] || [[ "$only" == "$1" ]] )
     then
         title "Running $1 tests"
         timer_reset
         if [[ "$2" == "go" ]]
         then
     if [[ -z "${skip[$1]}" ]] && ( [[ -z "$only" ]] || [[ "$only" == "$1" ]] )
     then
         title "Running $1 tests"
         timer_reset
         if [[ "$2" == "go" ]]
         then
-            go test "git.curoverse.com/arvados.git/$1"
+            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.
+            if [[ -n "${testargs[$1]}" ]]
+            then
+                # "go test -check.vv giturl" doesn't work, but this
+                # does:
+                cd "$WORKSPACE/$1" && \
+                    go get -t "git.curoverse.com/arvados.git/$1" && \
+                    go test ${coverflags[@]} ${testargs[$1]}
+            else
+                # The above form gets verbose even when testargs is
+                # empty, so use this form in such cases:
+                go get -t "git.curoverse.com/arvados.git/$1" && \
+                    go test ${coverflags[@]} "git.curoverse.com/arvados.git/$1"
+            fi
+            result="$?"
+            go tool cover -html="$WORKSPACE/tmp/.$covername.tmp" -o "$WORKSPACE/tmp/$covername.html"
+            rm "$WORKSPACE/tmp/.$covername.tmp"
+        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.
+            cd "$WORKSPACE/$1" \
+                && "${3}python" setup.py test ${testargs[$1]}
+        elif [[ "$2" != "" ]]
+        then
+            "test_$2"
         else
             "test_$1"
         fi
         else
             "test_$1"
         fi
-        checkexit "$1 tests"
+        result=${result:-$?}
+        checkexit $result "$1 tests"
         title "End of $1 tests (`timer`)"
         title "End of $1 tests (`timer`)"
+        return $result
     else
         title "Skipping $1 tests"
     fi
 }
 
 do_install() {
     else
         title "Skipping $1 tests"
     fi
 }
 
 do_install() {
-    if [[ -z "$skip_install" ]]
+    if [[ -z "$skip_install" || (-n "$only_install" && "$only_install" == "$1") ]]
     then
         title "Running $1 install"
         timer_reset
         if [[ "$2" == "go" ]]
         then
             go get -t "git.curoverse.com/arvados.git/$1"
     then
         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
         else
             "install_$1"
         fi
-        checkexit "$1 install"
+        checkexit $? "$1 install"
         title "End of $1 install (`timer`)"
     else
         title "Skipping $1 install"
         title "End of $1 install (`timer`)"
     else
         title "Skipping $1 install"
@@ -239,56 +583,82 @@ title () {
     printf "\n%*s%s\n\n" $((($COLUMNS-${#txt})/2)) "" "$txt"
 }
 
     printf "\n%*s%s\n\n" $((($COLUMNS-${#txt})/2)) "" "$txt"
 }
 
-install_docs() {
-    cd "$WORKSPACE/doc"
-    HOME="$GEMHOME" bundle install --no-deployment
-    rm -rf .site
-    # Make sure python-epydoc is installed or the next line won't do much good!
-    ARVADOS_API_HOST=qr1hi.arvadosapi.com
-    PYTHONPATH=$WORKSPACE/sdk/python/ HOME="$GEMHOME" bundle exec rake generate baseurl=file://$WORKSPACE/doc/.site/ arvados_workbench_host=workbench.$ARVADOS_API_HOST arvados_api_host=$ARVADOS_API_HOST
-    unset ARVADOS_API_HOST
+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
+    )
 }
 }
-do_install docs
 
 
-install_ruby_sdk() {
-    cd "$WORKSPACE/sdk/ruby" \
-        && HOME="$GEMHOME" bundle install --no-deployment \
-        && gem build arvados.gemspec \
-        && HOME="$GEMHOME" gem install --user-install --no-ri --no-rdoc `ls -t arvados-*.gem|head -n1`
+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)
 }
 }
-do_install ruby_sdk
 
 
-install_cli() {
-    cd "$WORKSPACE/sdk/cli" \
-        && gem build arvados-cli.gemspec \
-        && HOME="$GEMHOME" gem install --user-install --no-ri --no-rdoc `ls -t arvados-cli-*.gem|head -n1`
+install_ruby_sdk() {
+    install_gem arvados sdk/ruby
 }
 }
-do_install cli
+do_install sdk/ruby ruby_sdk
 
 
-install_python_sdk() {
-    # 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).
+install_perl_sdk() {
+    cd "$WORKSPACE/sdk/perl" \
+        && perl Makefile.PL INSTALL_BASE="$PERLINSTALLBASE" \
+        && make install INSTALLDIRS=perl
+}
+do_install sdk/perl perl_sdk
 
 
-    cd "$WORKSPACE/sdk/python" \
-        && python setup.py egg_info -b ".$(git log --format=format:%ct.%h -n1 .)" sdist rotate --keep=1 --match .tar.gz \
-        && pip install dist/arvados-python-client-0.1.*.tar.gz
+install_cli() {
+    install_gem arvados-cli sdk/cli
 }
 }
-do_install python_sdk
+do_install sdk/cli cli
 
 
-install_fuse() {
-    cd "$WORKSPACE/services/fuse" \
-        && python setup.py egg_info -b ".$(git log --format=format:%ct.%h -n1 .)" sdist rotate --keep=1 --match .tar.gz \
-        && pip install dist/arvados_fuse-0.1.*.tar.gz
+install_login-sync() {
+    install_gem arvados-login-sync services/login-sync
 }
 }
-do_install fuse
+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).
+declare -a pythonstuff
+pythonstuff=(
+    sdk/pam
+    sdk/python
+    sdk/cwl
+    services/fuse
+    services/nodemanager
+    tools/crunchstat-summary
+    )
+for p in "${pythonstuff[@]}"
+do
+    do_install "$p" pip
+done
+if [ -n "$PYTHON3" ]; then
+    do_install services/dockercleaner pip "$VENV3DIR/bin/"
+fi
 
 install_apiserver() {
 
 install_apiserver() {
-    cd "$WORKSPACE/services/api"
-    export RAILS_ENV=test
-    HOME="$GEMHOME" bundle install --no-deployment
+    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
 
     rm -f config/environments/test.rb
     cp config/environments/test.rb.example config/environments/test.rb
@@ -322,82 +692,106 @@ install_apiserver() {
         && git add tmp \
         && git commit -m 'initial commit'
 
         && git add tmp \
         && git commit -m 'initial commit'
 
-    # Clear out any lingering postgresql connections to arvados_test, so that we can drop it
-    # This assumes the current user is a postgresql superuser
-    psql arvados_test -c "SELECT pg_terminate_backend (pg_stat_activity.procpid::int) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'arvados_test';" 2>/dev/null
+    # 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
 
     cd "$WORKSPACE/services/api" \
 
     cd "$WORKSPACE/services/api" \
-        && HOME="$GEMHOME" bundle exec rake db:drop \
-        && HOME="$GEMHOME" bundle exec rake db:create \
-        && HOME="$GEMHOME" bundle exec rake db:setup
+        && RAILS_ENV=test bundle exec rake db:drop \
+        && RAILS_ENV=test bundle exec rake db:setup \
+        && RAILS_ENV=test bundle exec rake db:fixtures:load
 }
 }
-do_install apiserver
+do_install services/api apiserver
 
 declare -a gostuff
 gostuff=(
 
 declare -a gostuff
 gostuff=(
-    services/keepstore
-    services/keepproxy
     sdk/go/arvadosclient
     sdk/go/arvadosclient
-    sdk/go/keepclient
+    sdk/go/blockdigest
+    sdk/go/manifest
     sdk/go/streamer
     sdk/go/streamer
+    sdk/go/crunchrunner
+    services/arv-git-httpd
+    services/crunchstat
+    services/keep-web
+    services/keepstore
+    sdk/go/keepclient
+    services/keepproxy
+    services/datamanager/summary
+    services/datamanager/collection
+    services/datamanager/keep
+    services/datamanager
+    services/crunch-dispatch-local
+    services/crunch-run
+    tools/keep-rsync
     )
 for g in "${gostuff[@]}"
 do
     do_install "$g" go
 done
 
     )
 for g in "${gostuff[@]}"
 do
     do_install "$g" go
 done
 
+install_workbench() {
+    cd "$WORKSPACE/apps/workbench" \
+        && mkdir -p tmp/cache \
+        && RAILS_ENV=test bundle_install_trylocal
+}
+do_install apps/workbench workbench
+
 test_doclinkchecker() {
 test_doclinkchecker() {
-    cd "$WORKSPACE/doc"
-    HOME="$GEMHOME" bundle exec rake linkchecker baseurl=file://$WORKSPACE/doc/.site/
+    (
+        set -e
+        cd "$WORKSPACE/doc"
+        ARVADOS_API_HOST=qr1hi.arvadosapi.com
+        # Make sure python-epydoc is installed or the next line won't
+        # do much good!
+        PYTHONPATH=$WORKSPACE/sdk/python/ bundle exec rake linkchecker baseurl=file://$WORKSPACE/doc/.site/ arvados_workbench_host=https://workbench.$ARVADOS_API_HOST arvados_api_host=$ARVADOS_API_HOST
+    )
 }
 }
-do_test doclinkchecker
+do_test doc doclinkchecker
+
+stop_services
+
+test_apiserver() {
+    cd "$WORKSPACE/services/api" \
+        && RAILS_ENV=test bundle exec rake test TESTOPTS=-v ${testargs[services/api]}
+}
+do_test services/api apiserver
+
+# Shortcut for when we're only running apiserver tests. This saves a bit of time,
+# because we don't need to start up the api server for subsequent tests.
+if [ ! -z "$only" ] && [ "$only" == "services/api" ]; then
+  rotate_logfile "$WORKSPACE/services/api/log/" "test.log"
+  exit_cleanly
+fi
+
+start_api
 
 test_ruby_sdk() {
     cd "$WORKSPACE/sdk/ruby" \
 
 test_ruby_sdk() {
     cd "$WORKSPACE/sdk/ruby" \
-        && HOME="$GEMHOME" bundle install --no-deployment \
-        && HOME="$GEMHOME" bundle exec rake test
+        && bundle exec rake test TESTOPTS=-v ${testargs[sdk/ruby]}
 }
 }
-do_test ruby_sdk
+do_test sdk/ruby ruby_sdk
 
 test_cli() {
 
 test_cli() {
-    title "Starting SDK CLI tests"
     cd "$WORKSPACE/sdk/cli" \
     cd "$WORKSPACE/sdk/cli" \
-        && HOME="$GEMHOME" bundle install --no-deployment \
         && mkdir -p /tmp/keep \
         && mkdir -p /tmp/keep \
-        && KEEP_LOCAL_STORE=/tmp/keep HOME="$GEMHOME" bundle exec rake test $cli_test
+        && KEEP_LOCAL_STORE=/tmp/keep bundle exec rake test TESTOPTS=-v ${testargs[sdk/cli]}
 }
 }
-do_test cli
+do_test sdk/cli cli
 
 
-test_apiserver() {
-    cd "$WORKSPACE/services/api"
-    HOME="$GEMHOME" bundle exec rake test $apiserver_test
+test_login-sync() {
+    cd "$WORKSPACE/services/login-sync" \
+        && bundle exec rake test TESTOPTS=-v ${testargs[services/login-sync]}
 }
 }
-do_test apiserver
+do_test services/login-sync login-sync
 
 
-test_python_sdk() {
-    # Python SDK. We test this before testing keepproxy: keepproxy runs
-    # run_test_server.py, which depends on the yaml package, which is in
-    # tests_require but not install_requires, and therefore does not get
-    # installed by setuptools until we run "setup.py test" *and* install
-    # the .egg files that setup.py downloads.
-
-    cd "$WORKSPACE/sdk/python" \
-        && python setup.py test $python_sdk_test
-    r=$?
-    easy_install *.egg
-    return $r
-}
-do_test python_sdk
-
-test_fuse() {
-    # Install test dependencies here too, in case run_test_server needs them.
-    cd "$WORKSPACE/services/fuse" \
-        && python setup.py test $fuse_test
-    r=$?
-    easy_install *.egg
-    return $r
-}
-do_test fuse
+for p in "${pythonstuff[@]}"
+do
+    do_test "$p" pip
+done
+do_test services/dockercleaner pip "$VENV3DIR/bin/"
 
 for g in "${gostuff[@]}"
 do
 
 for g in "${gostuff[@]}"
 do
@@ -405,13 +799,24 @@ do
 done
 
 test_workbench() {
 done
 
 test_workbench() {
-    cd "$WORKSPACE/apps/workbench" \
-        && HOME="$GEMHOME" bundle install --no-deployment \
-        && HOME="$GEMHOME" bundle exec rake test $workbench_test
+    start_nginx_proxy_services \
+        && cd "$WORKSPACE/apps/workbench" \
+        && RAILS_ENV=test bundle exec rake test TESTOPTS=-v ${testargs[apps/workbench]}
+}
+do_test apps/workbench workbench
+
+test_workbench_benchmark() {
+    start_nginx_proxy_services \
+        && cd "$WORKSPACE/apps/workbench" \
+        && RAILS_ENV=test bundle exec rake test:benchmark ${testargs[apps/workbench_benchmark]}
 }
 }
-do_test workbench
+do_test apps/workbench_benchmark workbench_benchmark
 
 
-report_outcomes
-clear_temp
+test_workbench_profile() {
+    start_nginx_proxy_services \
+        && cd "$WORKSPACE/apps/workbench" \
+        && RAILS_ENV=test bundle exec rake test:profile ${testargs[apps/workbench_profile]}
+}
+do_test apps/workbench_profile workbench_profile
 
 
-exit ${#failures}
+exit_cleanly