testrunner.py: Add --build-only and --skip-build options.

The build stage is more expensive than the test stage
and we do it redundantly repeatedly for all variants.

Make it possible to separate the build and test stages,
which will allow us to do the build just once on buildbots.

The proper solution is to move all the build work to soong,
and we will be able to remove this code again at that point.

Bug: 188631922
Test: testrunner.py --host --build-only && \
      testrunner.py --host --skip-build
Change-Id: I542feac03acc25a853dbf7f1a2e5587a2c5d6d7a
diff --git a/test/run-test b/test/run-test
index 9352314..c968bee 100755
--- a/test/run-test
+++ b/test/run-test
@@ -39,6 +39,7 @@
 else
   tmp_dir="${TMPDIR}/${test_dir}"
 fi
+build_path="${tmp_dir}-build"
 checker="${progdir}/../tools/checker/checker.py"
 export JAVA="java"
 export JAVAC="javac -g -Xlint:-options -source 1.8 -target 1.8"
@@ -148,6 +149,7 @@
 runtime="art"
 usage="no"
 build_only="no"
+skip_build="no"
 suffix64=""
 trace="false"
 trace_stream="false"
@@ -381,11 +383,23 @@
     elif [ "x$1" = "x--build-only" ]; then
         build_only="yes"
         shift
-    elif [ "x$1" = "x--output-path" ]; then
+    elif [ "x$1" = "x--skip-build" ]; then
+        skip_build="yes"
+        shift
+    elif [ "x$1" = "x--build-path" ]; then
+        shift
+        build_path=$1
+        if [ "x$build_path" = "x" ]; then
+            echo "$0 missing argument to --build-path" 1>&2
+            usage="yes"
+            break
+        fi
+        shift
+    elif [ "x$1" = "x--temp-path" ]; then
         shift
         tmp_dir=$1
         if [ "x$tmp_dir" = "x" ]; then
-            echo "$0 missing argument to --output-path" 1>&2
+            echo "$0 missing argument to --temp-path" 1>&2
             usage="yes"
             break
         fi
@@ -796,6 +810,9 @@
         echo "                          Use the given binary as gdbserver."
         echo "    --gdb-arg             Pass an option to gdb or gdbserver."
         echo "    --build-only          Build test files only (off by default)."
+        echo "    --skip-build          Assume that test files are already built (off by default)."
+        echo "    --build-path [path]   Location where to store or expect the build files."
+        echo "    --temp-path [path]    Location where to execute the tests."
         echo "    --interpreter         Enable interpreter only mode (off by default)."
         echo "    --jit                 Enable jit (off by default)."
         echo "    --optimizing          Enable optimizing compiler (default)."
@@ -825,8 +842,6 @@
         echo "    --use-java-home       Use the JAVA_HOME environment variable"
         echo "                          to find the java compiler and runtime"
         echo "                          (if applicable) to run the test with."
-        echo "    --output-path [path]  Location where to store the build" \
-             "files."
         echo "    --64                  Run the test in 64-bit mode"
         echo "    --bionic              Use the (host, 64-bit only) linux_bionic libc runtime"
         echo "    --runtime-zipapex [file]"
@@ -885,35 +900,36 @@
     fi
 done
 
-# copy the test to a temp dir and run it
+if [ "$skip_build" = "no" ]; then
+  echo "${test_dir}: building..." 1>&2
 
-echo "${test_dir}: building..." 1>&2
+  rm -rf "$build_path"
+  cp -LRp "$test_dir" "$build_path"
+  cd "$build_path"
 
-rm -rf "$tmp_dir"
-cp -LRp "$test_dir" "$tmp_dir"
-cd "$tmp_dir"
+  if [ '!' -r "$build" ]; then
+      cp "${progdir}/etc/default-build" build
+  else
+      cp "${progdir}/etc/default-build" .
+  fi
 
-if [ '!' -r "$build" ]; then
-    cp "${progdir}/etc/default-build" build
-else
-    cp "${progdir}/etc/default-build" .
+  if [ '!' -r "$run" ]; then
+      cp "${progdir}/etc/default-run" run
+  else
+      cp "${progdir}/etc/default-run" .
+  fi
+
+  if [ '!' -r "$check_cmd" ]; then
+      cp "${progdir}/etc/default-check" check
+  else
+      cp "${progdir}/etc/default-check" .
+  fi
+
+  chmod 755 "$build"
+  chmod 755 "$run"
+  chmod 755 "$check_cmd"
 fi
-
-if [ '!' -r "$run" ]; then
-    cp "${progdir}/etc/default-run" run
-else
-    cp "${progdir}/etc/default-run" .
-fi
-
-if [ '!' -r "$check_cmd" ]; then
-    cp "${progdir}/etc/default-check" check
-else
-    cp "${progdir}/etc/default-check" .
-fi
-
-chmod 755 "$build"
-chmod 755 "$run"
-chmod 755 "$check_cmd"
+cd "$build_path"
 
 export TEST_NAME=`basename ${test_dir}`
 
@@ -965,14 +981,30 @@
   build_args="$build_args --dev"
 fi
 
+# Build needed files, and copy them to the directory that will run the tests.
+if [ "$skip_build" = "yes" ]; then
+  # Assume the directory already contains all the needed files.
+  # Load the exit code, since builds can intentionally fail.
+  build_exit=`cat ./build_exit_code`
+elif [ "$dev_mode" = "yes" ]; then
+  "./${build}" $build_args
+  build_exit="$?"
+  echo "build exit status: $build_exit" 1>&2
+  echo "$build_exit" > ./build_exit_code
+else
+  "./${build}" $build_args >"$build_stdout" 2>"$build_stderr"
+  build_exit="$?"
+  echo "$build_exit" > ./build_exit_code
+fi
+rm -rf "$tmp_dir"
+cp -LRp "$build_path" "$tmp_dir"
+cd "$tmp_dir"
+
 good="no"
 good_build="yes"
 good_run="yes"
 export TEST_RUNTIME="${runtime}"
 if [ "$dev_mode" = "yes" ]; then
-    "./${build}" $build_args
-    build_exit="$?"
-    echo "build exit status: $build_exit" 1>&2
     if [ "$build_exit" = '0' ]; then
         echo "${test_dir}: running..." 1>&2
         "./${run}" "${run_args[@]}" "$@"
@@ -996,8 +1028,6 @@
         echo "run exit status: $run_exit" 1>&2
     fi
 elif [ "$update_mode" = "yes" ]; then
-    "./${build}" $build_args >"$build_stdout" 2>"$build_stderr"
-    build_exit="$?"
     if [ "$build_exit" = '0' ]; then
         echo "${test_dir}: running..." 1>&2
         "./${run}" "${run_args[@]}" "$@" >"$test_stdout" 2>"$test_stderr"
@@ -1021,8 +1051,6 @@
     fi
 elif [ "$build_only" = "yes" ]; then
     good="yes"
-    "./${build}" $build_args >"$build_stdout" 2>"$build_stderr"
-    build_exit="$?"
     if [ "$build_exit" '!=' '0' ]; then
         cp "$build_stdout" "$test_stdout"
         diff --strip-trailing-cr -q "$expected_stdout" "$test_stdout" >/dev/null
@@ -1046,8 +1074,6 @@
       | xargs rm -rf
     exit 0
 else
-    "./${build}" $build_args >"$build_stdout" 2>"$build_stderr"
-    build_exit="$?"
     if [ "$build_exit" = '0' ]; then
         echo "${test_dir}: running..." 1>&2
         "./${run}" "${run_args[@]}" "$@" >"$test_stdout" 2>"$test_stderr"
@@ -1165,6 +1191,10 @@
 # Clean up test files.
 if [ "$always_clean" = "yes" -o "$good" = "yes" ] && [ "$never_clean" = "no" ]; then
     cd "$oldwd"
+    # Clean up build files only if we created them.
+    if [ "$skip_build" = "no" ]; then
+      rm -rf "$build_path"
+    fi
     rm -rf "$tmp_dir"
     if [ "$target_mode" = "yes" -a "$build_exit" = "0" ]; then
         adb shell rm -rf $chroot_dex_location
diff --git a/test/testrunner/env.py b/test/testrunner/env.py
index 40f750b..319e1a7 100644
--- a/test/testrunner/env.py
+++ b/test/testrunner/env.py
@@ -71,6 +71,9 @@
 # Directory used for temporary test files on the host.
 ART_HOST_TEST_DIR = tempfile.mkdtemp(prefix = 'test-art-')
 
+# Directory used to store files build by the run-test script.
+ART_TEST_RUN_TEST_BUILD_PATH = _env.get('ART_TEST_RUN_TEST_BUILD_PATH')
+
 # Keep going after encountering a test failure?
 ART_TEST_KEEP_GOING = _getEnvBoolean('ART_TEST_KEEP_GOING', True)
 
diff --git a/test/testrunner/testrunner.py b/test/testrunner/testrunner.py
index c531d2e..987cf71 100755
--- a/test/testrunner/testrunner.py
+++ b/test/testrunner/testrunner.py
@@ -194,6 +194,8 @@
 
 child_process_tracker = ChildProcessTracker()
 
+# Keep track of the already executed build scripts
+finished_build_script = {}
 
 def setup_csv_result():
   """Set up the CSV output if required."""
@@ -568,10 +570,27 @@
       if address_size == '64':
         options_test += ' --64'
 
-      # TODO(http://36039166): This is a temporary solution to
-      # fix build breakages.
-      options_test = (' --output-path %s') % (
-          tempfile.mkdtemp(dir=env.ART_HOST_TEST_DIR)) + options_test
+      # Make it possible to split the test to two passes: build only and test only.
+      # This is useful to avoid building identical files many times for the test combinations.
+      # We can remove this once we move the build script fully to soong.
+      global build_only
+      global skip_build
+      if (build_only or skip_build) and not is_test_disabled(test, variant_set):
+        assert(env.ART_TEST_RUN_TEST_BUILD_PATH)  # Persistent storage between the passes.
+        build_path = os.path.join(env.ART_TEST_RUN_TEST_BUILD_PATH, test)
+        if build_only and finished_build_script.setdefault(test, test_name) != test_name:
+          return None  # Different combination already build the needed files for this test.
+        os.makedirs(build_path, exist_ok=True)
+        if build_only:
+          options_test += ' --build-only'
+        if skip_build:
+          options_test += ' --skip-build'
+      else:
+        build_path = tempfile.mkdtemp(dir=env.ART_HOST_TEST_DIR)
+
+      # b/36039166: Note that the path lengths must kept reasonably short.
+      temp_path = tempfile.mkdtemp(dir=env.ART_HOST_TEST_DIR)
+      options_test = '--build-path {} --temp-path {} '.format(build_path, temp_path) + options_test
 
       run_test_sh = env.ANDROID_BUILD_TOP + '/art/test/run-test'
       command = ' '.join((run_test_sh, options_test, ' '.join(extra_arguments[target]), test))
@@ -594,7 +613,7 @@
 
       try:
         tests_done = 0
-        for test_future in concurrent.futures.as_completed(test_futures):
+        for test_future in concurrent.futures.as_completed(f for f in test_futures if f):
           (test, status, failure_info, test_time) = test_future.result()
           tests_done += 1
           print_test_info(tests_done, test, status, failure_info, test_time)
@@ -1091,6 +1110,8 @@
   global with_agent
   global zipapex_loc
   global csv_result
+  global build_only
+  global skip_build
 
   parser = argparse.ArgumentParser(description="Runs all or a subset of the ART test suite.")
   parser.add_argument('-t', '--test', action='append', dest='tests', help='name(s) of the test(s)')
@@ -1127,7 +1148,11 @@
                             help="""Pass an option, unaltered, to the run-test script.
                             This should be enclosed in single-quotes to allow for spaces. The option
                             will be split using shlex.split() prior to invoking run-test.
-                            Example \"--run-test-option='--with-agent libtifast.so=MethodExit'\"""")
+                            Example \"--run-test-option='--with-agent libtifast.so=MethodExit'\".""")
+  global_group.add_argument('--build-only', action='store_true', dest='build_only',
+                            help="""Only execute the build commands in the run-test script""")
+  global_group.add_argument('--skip-build', action='store_true', dest='skip_build',
+                            help="""Skip the builds command in the run-test script""")
   global_group.add_argument('--with-agent', action='append', dest='with_agent',
                             help="""Pass an agent to be attached to the runtime""")
   global_group.add_argument('--runtime-option', action='append', dest='runtime_option',
@@ -1195,6 +1220,8 @@
   with_agent = options['with_agent'];
   run_test_option = sum(map(shlex.split, options['run_test_option']), [])
   zipapex_loc = options['runtime_zipapex']
+  build_only = options['build_only']
+  skip_build = options['skip_build']
 
   timeout = options['timeout']
   if options['dex2oat_jobs']: