Convert run-test-jar from bash to python

This is a naive 1:1 conversion with intent to create minimal diff.
The code does not follow python conventions, idioms or formatting.
(this is left for follow up clean-up CLs)

I have tested it by recording and comparing the commands and their
environment executed by both version. They match.

Test: test.py -r --all-target --all-run --all-gc
Change-Id: Ie01c4a53618a425acb5e8708f172f2379da3e343
diff --git a/test/661-oat-writer-layout/run b/test/661-oat-writer-layout/run
index 087cd20..3c09690 100644
--- a/test/661-oat-writer-layout/run
+++ b/test/661-oat-writer-layout/run
@@ -19,4 +19,4 @@
 # -- we accomplish this by blocklisting other compiler variants
 # and we also have to pass the option explicitly as dex2oat
 # defaults to speed-profile if a profile is specified.
-"${RUN}" "$@" --profile -Xcompiler-option --compiler-filter=speed
+${RUN} "$@" --profile -Xcompiler-option --compiler-filter=speed
diff --git a/test/etc/apex-bootclasspath-utils.sh b/test/etc/apex-bootclasspath-utils.sh
deleted file mode 100755
index 5a0873d..0000000
--- a/test/etc/apex-bootclasspath-utils.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/bash
-#
-# Copyright (C) 2022 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# This file contains utils for constructing -Xbootclasspath and -Xbootclasspath-location
-# for dex2oat and dalvikvm from apex modules list.
-#
-# Those utils could be used outside of art/test/ to run ART in chroot setup.
-
-# Note: This must start with the CORE_IMG_JARS in Android.common_path.mk
-# because that's what we use for compiling the boot.art image.
-# It may contain additional modules from TEST_CORE_JARS.
-readonly bpath_modules="core-oj core-libart okhttp bouncycastle apache-xml core-icu4j conscrypt"
-
-# Helper function to construct paths for apex modules (for both -Xbootclasspath and
-# -Xbootclasspath-location).
-#
-#  Arguments.
-#   ${1}: path prefix.
-get_apex_bootclasspath_impl() {
-  local -r bpath_prefix="$1"
-  local bpath_separator=""
-  local bpath=""
-  local bpath_jar=""
-  for bpath_module in ${bpath_modules}; do
-    local apex_module="com.android.art"
-    case "$bpath_module" in
-      (conscrypt)  apex_module="com.android.conscrypt";;
-      (core-icu4j) apex_module="com.android.i18n";;
-      (*)          apex_module="com.android.art";;
-    esac
-    bpath_jar="/apex/${apex_module}/javalib/${bpath_module}.jar"
-    bpath+="${bpath_separator}${bpath_prefix}${bpath_jar}"
-    bpath_separator=":"
-  done
-  echo "${bpath}"
-}
-
-# Gets a -Xbootclasspath paths with the apex modules.
-#
-#  Arguments.
-#   ${1}: host (y|n).
-get_apex_bootclasspath() {
-  local -r host="${1}"
-  local bpath_prefix=""
-
-  if [[ "${host}" == "y" ]]; then
-    bpath_prefix="${ANDROID_HOST_OUT}"
-  fi
-
-  get_apex_bootclasspath_impl "${bpath_prefix}"
-}
-
-# Gets a -Xbootclasspath-location paths with the apex modules.
-#
-#  Arguments.
-#   ${1}: host (y|n).
-get_apex_bootclasspath_locations() {
-  local -r host="${1}"
-  local bpath_location_prefix=""
-
-  if [[ "${host}" == "y" ]]; then
-    if [[ "${ANDROID_HOST_OUT:0:${#ANDROID_BUILD_TOP}+1}" == "${ANDROID_BUILD_TOP}/" ]]; then
-      bpath_location_prefix="${ANDROID_HOST_OUT:${#ANDROID_BUILD_TOP}+1}"
-    else
-      error_msg "ANDROID_BUILD_TOP/ is not a prefix of ANDROID_HOST_OUT"\
-                "\nANDROID_BUILD_TOP=${ANDROID_BUILD_TOP}"\
-                "\nANDROID_HOST_OUT=${ANDROID_HOST_OUT}"
-      exit
-    fi
-  fi
-
-  get_apex_bootclasspath_impl "${bpath_location_prefix}"
-}
diff --git a/test/etc/apex_bootclasspath_utils.py b/test/etc/apex_bootclasspath_utils.py
new file mode 100755
index 0000000..0a95646
--- /dev/null
+++ b/test/etc/apex_bootclasspath_utils.py
@@ -0,0 +1,79 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file contains utils for constructing -Xbootclasspath and -Xbootclasspath-location
+# for dex2oat and dalvikvm from apex modules list.
+#
+# Those utils could be used outside of art/test/ to run ART in chroot setup.
+
+import os, sys
+
+# Note: This must start with the CORE_IMG_JARS in Android.common_path.mk
+# because that's what we use for compiling the boot.art image.
+# It may contain additional modules from TEST_CORE_JARS.
+bpath_modules="core-oj core-libart okhttp bouncycastle apache-xml core-icu4j conscrypt"
+
+ANDROID_BUILD_TOP=os.environ["ANDROID_BUILD_TOP"]
+ANDROID_HOST_OUT=os.environ["ANDROID_HOST_OUT"]
+
+# Helper function to construct paths for apex modules (for both -Xbootclasspath and
+# -Xbootclasspath-location).
+#
+#  Arguments.
+#   ${1}: path prefix.
+def get_apex_bootclasspath_impl(bpath_prefix: str):
+  bpath_separator=""
+  bpath=""
+  bpath_jar=""
+  for bpath_module in bpath_modules.split(" "):
+    apex_module="com.android.art"
+    if bpath_module == "conscrypt":
+      apex_module="com.android.conscrypt"
+    if bpath_module == "core-icu4j":
+      apex_module="com.android.i18n"
+    bpath_jar=f"/apex/{apex_module}/javalib/{bpath_module}.jar"
+    bpath+=f"{bpath_separator}{bpath_prefix}{bpath_jar}"
+    bpath_separator=":"
+  return bpath
+
+# Gets a -Xbootclasspath paths with the apex modules.
+#
+#  Arguments.
+#   ${1}: host (y|n).
+def get_apex_bootclasspath(host: str):
+  bpath_prefix=""
+
+  if host == "y":
+    bpath_prefix=ANDROID_HOST_OUT
+
+  return get_apex_bootclasspath_impl(bpath_prefix)
+
+# Gets a -Xbootclasspath-location paths with the apex modules.
+#
+#  Arguments.
+#   ${1}: host (y|n).
+def get_apex_bootclasspath_locations(host: str):
+  bpath_location_prefix=""
+
+  if host == "y":
+    if ANDROID_HOST_OUT[0:len(ANDROID_BUILD_TOP)+1] == f"{ANDROID_BUILD_TOP}/":
+      bpath_location_prefix=ANDROID_HOST_OUT[len(ANDROID_BUILD_TOP)+1:]
+    else:
+      print(f"ANDROID_BUILD_TOP/ is not a prefix of ANDROID_HOST_OUT"\
+            "\nANDROID_BUILD_TOP={ANDROID_BUILD_TOP}"\
+            "\nANDROID_HOST_OUT={ANDROID_HOST_OUT}")
+      sys.exit(1)
+
+  return get_apex_bootclasspath_impl(bpath_location_prefix)
diff --git a/test/etc/run-test-jar b/test/etc/run-test-jar
index 7f251e5..617ceff 100755
--- a/test/etc/run-test-jar
+++ b/test/etc/run-test-jar
@@ -1,36 +1,104 @@
-#!/bin/bash
+#!/usr/bin/env python3
 #
-# Runner for an individual run-test.
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
-readonly local_path=$(dirname "$0")
-source "${local_path}/apex-bootclasspath-utils.sh"
+import sys, os, shutil, shlex, re, subprocess, glob
+from apex_bootclasspath_utils import get_apex_bootclasspath, get_apex_bootclasspath_locations
+from os import path
+from os.path import isfile, isdir
+from typing import List
+from subprocess import DEVNULL, PIPE, STDOUT
 
-# Check how many colors the terminal can display.
-ncolors=$(tput colors 2>/dev/null)
+def export(env: str, value: str) -> None:
+  os.environ[env] = value
+  if env in globals():
+    globals()[env] = value
+
+# Script debugging: Record executed commands into the given directory.
+# This is useful to ensure that changes to the script don't change behaviour.
+# (the commands are appended so the directory needs to be cleared before run)
+ART_TEST_CMD_DIR = os.environ.get("ART_TEST_CMD_DIR")
+
+def run(cmdline: str, capture_output=True, check=True, save_cmd=True) -> subprocess.CompletedProcess:
+  if ART_TEST_CMD_DIR and save_cmd and cmdline != "true":
+    tmp = os.environ["DEX_LOCATION"]
+    with open(os.path.join(ART_TEST_CMD_DIR, os.environ["FULL_TEST_NAME"]), "a") as f:
+      env_ignore = ["SHLVL", "_", "ART_TOOLS_BUILD_VAR_CACHE", "PWD", "OLDPWD", "TMUX", "TMUX_PANE"]
+      env = {k: v for k, v in sorted(os.environ.items()) if k not in env_ignore}
+      # Replace DEX_LOCATION (which is randomly generated temporary directory),
+      # with a deterministic placeholder so that we can do a diff from run to run.
+      f.write("\n".join(k + ":" + v.replace(tmp, "<tmp>") for k, v in env.items()) + "\n\n")
+      f.write(re.sub(" +", "\n", cmdline).replace(tmp, "<tmp>") + "\n\n")
+  proc = subprocess.run([cmdline],
+                        shell=True,
+                        encoding="utf8",
+                        capture_output=capture_output)
+  if check and proc.returncode != 0:
+    print(proc.stdout or "", file=sys.stdout, flush=True)
+    print("$ " + cmdline + "\n", file=sys.stderr)
+    print(proc.stderr or "", file=sys.stderr, flush=True)
+    raise Exception("Command returned exit code {}".format(proc.returncode))
+  return proc
+
+class Adb():
+  def root(self) -> None:
+    run("adb root")
+  def wait_for_device(self) -> None:
+    run("adb wait-for-device")
+  def shell(self, cmdline: str, **kwargs) -> subprocess.CompletedProcess:
+    return run("adb shell " + cmdline, **kwargs)
+  def push(self, src: str, dst: str, **kwargs) -> None:
+    run(f"adb push {src} {dst}", **kwargs)
+
+adb = Adb()
+
+local_path=os.path.dirname(__file__)
 
 # Check that stdout is connected to a terminal and that we have at least 1 color.
 # This ensures that if the stdout is not connected to a terminal and instead
 # the stdout will be used for a log, it will not append the color characters.
-if [[ -t 1 && ${ncolors} && ${ncolors} -ge 1 ]]; then
-  bold_red="$(tput bold)$(tput setaf 1)"
-fi
+bold_red=""
+if sys.stdout.isatty():
+  if int(run("tput colors", save_cmd=False).stdout) >= 1:
+    bold_red=run("tput bold", save_cmd=False).stdout.strip()
+    bold_red+=run("tput setaf 1", save_cmd=False).stdout.strip()
 
-readonly bold_red
+def error_msg(msg: str):
+  print(f"{bold_red}ERROR: {msg}")
 
-error_msg() {
-  echo -e "${bold_red}ERROR: $@" 1>&2
-}
+ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP")
+ANDROID_DATA = os.environ.get("ANDROID_DATA")
+ANDROID_HOST_OUT = os.environ["ANDROID_HOST_OUT"]
+ANDROID_LOG_TAGS = os.environ.get("ANDROID_LOG_TAGS", "")
+ART_TIME_OUT_MULTIPLIER = int(os.environ.get("ART_TIME_OUT_MULTIPLIER", 1))
+DEX2OAT = os.environ.get("DEX2OAT", "")
+DEX_LOCATION = os.environ["DEX_LOCATION"]
+JAVA = os.environ.get("JAVA")
+OUT_DIR = os.environ.get("OUT_DIR")
+PATH = os.environ.get("PATH", "")
+SANITIZE_HOST = os.environ.get("SANITIZE_HOST", "")
+TEST_NAME = os.environ["TEST_NAME"]
+USE_EXRACTED_ZIPAPEX = os.environ.get("USE_EXRACTED_ZIPAPEX", "")
 
-if [[ -z "$ANDROID_BUILD_TOP" ]]; then
-  error_msg 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?'
-  exit 1
-fi
+if not ANDROID_BUILD_TOP:
+  error_msg('ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?')
+  sys.exit(1)
 
-msg() {
-    if [ "$QUIET" = "n" ]; then
-        echo "$@"
-    fi
-}
+def msg(msg: str):
+    if QUIET == "n":
+        print(msg)
 
 ANDROID_ROOT="/system"
 ANDROID_ART_ROOT="/apex/com.android.art"
@@ -38,20 +106,20 @@
 ANDROID_TZDATA_ROOT="/apex/com.android.tzdata"
 ARCHITECTURES_32="(arm|x86|none)"
 ARCHITECTURES_64="(arm64|x86_64|none)"
-ARCHITECTURES_PATTERN="${ARCHITECTURES_32}"
+ARCHITECTURES_PATTERN=ARCHITECTURES_32
 GET_DEVICE_ISA_BITNESS_FLAG="--32"
 BOOT_IMAGE=""
-CHROOT=
+CHROOT=""
 COMPILE_FLAGS=""
 DALVIKVM="dalvikvm32"
 DEBUGGER="n"
-WITH_AGENT=()
+WITH_AGENT=[]
 DEBUGGER_AGENT=""
 WRAP_DEBUGGER_AGENT="n"
 DEV_MODE="n"
 DEX2OAT_NDEBUG_BINARY="dex2oat32"
 DEX2OAT_DEBUG_BINARY="dex2oatd32"
-EXPERIMENTAL=""
+EXPERIMENTAL=[]
 FALSE_BIN="false"
 FLAGS=""
 ANDROID_FLAGS=""
@@ -75,7 +143,7 @@
 IS_JVMTI_TEST="n"
 ADD_LIBDIR_ARGUMENTS="n"
 SUFFIX64=""
-ISA=x86
+ISA="x86"
 LIBRARY_DIRECTORY="lib"
 TEST_DIRECTORY="nativetest"
 MAIN=""
@@ -85,10 +153,10 @@
 RELOCATE="n"
 SECONDARY_DEX=""
 TIME_OUT="n"  # "n" (disabled), "timeout" (use timeout), "gdb" (use gdb)
-TIMEOUT_DUMPER=signal_dumper
+TIMEOUT_DUMPER="signal_dumper"
 # Values in seconds.
 TIME_OUT_EXTRA=0
-TIME_OUT_VALUE=
+TIME_OUT_VALUE=0
 USE_GDB="n"
 USE_GDBSERVER="n"
 GDBSERVER_PORT=":5039"
@@ -119,773 +187,720 @@
 PROFILE="n"
 RANDOM_PROFILE="n"
 # The normal dex2oat timeout.
-DEX2OAT_TIMEOUT="300" # 5 mins
+DEX2OAT_TIMEOUT=300 # 5 mins
 # The *hard* timeout where we really start trying to kill the dex2oat.
-DEX2OAT_RT_TIMEOUT="360" # 6 mins
+DEX2OAT_RT_TIMEOUT=360 # 6 mins
 CREATE_RUNNER="n"
+INT_OPTS=""
+SIMPLEPERF="n"
+DEBUGGER_OPTS=""
+JVM_VERIFY_ARG=""
 
 # if "y", run 'sync' before dalvikvm to make sure all files from
 # build step (e.g. dex2oat) were finished writing.
 SYNC_BEFORE_RUN="n"
 
 # When running a debug build, we want to run with all checks.
-ANDROID_FLAGS="${ANDROID_FLAGS} -XX:SlowDebug=true"
+ANDROID_FLAGS+=" -XX:SlowDebug=true"
 # The same for dex2oatd, both prebuild and runtime-driven.
-ANDROID_FLAGS="${ANDROID_FLAGS} -Xcompiler-option --runtime-arg -Xcompiler-option -XX:SlowDebug=true"
-COMPILER_FLAGS="${COMPILER_FLAGS} --runtime-arg -XX:SlowDebug=true"
+ANDROID_FLAGS+=" -Xcompiler-option --runtime-arg -Xcompiler-option -XX:SlowDebug=true"
+COMPILER_FLAGS="  --runtime-arg -XX:SlowDebug=true"
 
 # Let the compiler and runtime know that we are running tests.
-COMPILE_FLAGS="${COMPILE_FLAGS} --compile-art-test"
-ANDROID_FLAGS="${ANDROID_FLAGS} -Xcompiler-option --compile-art-test"
+COMPILE_FLAGS+=" --compile-art-test"
+ANDROID_FLAGS+=" -Xcompiler-option --compile-art-test"
 
-while true; do
-    if [ "x$1" = "x--quiet" ]; then
+args = list(sys.argv)
+arg = ""
+def shift():
+  global arg
+  args.pop(0)
+  arg = args[0] if args else None
+shift()
+while arg:
+    if arg == "--quiet":
         QUIET="y"
-        shift
-    elif [ "x$1" = "x--dex2oat-rt-timeout" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --dex2oat-rt-timeout"
-            exit 1
-        fi
-        DEX2OAT_RT_TIMEOUT="$1"
-        shift
-    elif [ "x$1" = "x--dex2oat-timeout" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --dex2oat-timeout"
-            exit 1
-        fi
-        DEX2OAT_TIMEOUT="$1"
-        shift
-    elif [ "x$1" = "x--jvmti" ]; then
+        shift()
+    elif arg == "--dex2oat-rt-timeout":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --dex2oat-rt-timeout")
+            sys.exit(1)
+        DEX2OAT_RT_TIMEOUT=int(arg)
+        shift()
+    elif arg == "--dex2oat-timeout":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --dex2oat-timeout")
+            sys.exit(1)
+        DEX2OAT_TIMEOUT=int(arg)
+        shift()
+    elif arg == "--jvmti":
         USE_JVMTI="y"
         IS_JVMTI_TEST="y"
         # Secondary images block some tested behavior.
         SECONDARY_APP_IMAGE="n"
-        shift
-    elif [ "x$1" = "x--add-libdir-argument" ]; then
+        shift()
+    elif arg == "--add-libdir-argument":
         ADD_LIBDIR_ARGUMENTS="y"
-        shift
-    elif [ "x$1" = "x-O" ]; then
+        shift()
+    elif arg == "-O":
         TEST_IS_NDEBUG="y"
-        shift
-    elif [ "x$1" = "x--lib" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --lib"
-            exit 1
-        fi
-        LIB="$1"
-        shift
-    elif [ "x$1" = "x--gc-stress" ]; then
+        shift()
+    elif arg == "--lib":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --lib")
+            sys.exit(1)
+        LIB=arg
+        shift()
+    elif arg == "--gc-stress":
         # Give an extra 20 mins if we are gc-stress.
-        TIME_OUT_EXTRA=$((${TIME_OUT_EXTRA} + 1200))
-        shift
-    elif [ "x$1" = "x--testlib" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --testlib"
-            exit 1
-        fi
-        ARGS="${ARGS} $1"
-        shift
-    elif [ "x$1" = "x--args" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --args"
-            exit 1
-        fi
-        ARGS="${ARGS} $1"
-        shift
-    elif [ "x$1" = "x--compiler-only-option" ]; then
-        shift
-        option="$1"
-        COMPILE_FLAGS="${COMPILE_FLAGS} $option"
-        shift
-    elif [ "x$1" = "x-Xcompiler-option" ]; then
-        shift
-        option="$1"
-        FLAGS="${FLAGS} -Xcompiler-option $option"
-        COMPILE_FLAGS="${COMPILE_FLAGS} $option"
-        shift
-    elif [ "x$1" = "x--create-runner" ]; then
+        TIME_OUT_EXTRA+=1200
+        shift()
+    elif arg == "--testlib":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --testlib")
+            sys.exit(1)
+        ARGS+=f" {arg}"
+        shift()
+    elif arg == "--args":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --args")
+            sys.exit(1)
+        ARGS+=f" {arg}"
+        shift()
+    elif arg == "--compiler-only-option":
+        shift()
+        option=arg
+        COMPILE_FLAGS+=f" {option}"
+        shift()
+    elif arg == "-Xcompiler-option":
+        shift()
+        option=arg
+        FLAGS+=f" -Xcompiler-option {option}"
+        COMPILE_FLAGS+=f" {option}"
+        shift()
+    elif arg == "--create-runner":
         CREATE_RUNNER="y"
-        shift
-    elif [ "x$1" = "x--android-runtime-option" ]; then
-        shift
-        option="$1"
-        ANDROID_FLAGS="${ANDROID_FLAGS} $option"
-        shift
-    elif [ "x$1" = "x--runtime-option" ]; then
-        shift
-        option="$1"
-        FLAGS="${FLAGS} $option"
-        if [ "x$option" = "x-Xmethod-trace" ]; then
+        shift()
+    elif arg == "--android-runtime-option":
+        shift()
+        option=arg
+        ANDROID_FLAGS+=f" {option}"
+        shift()
+    elif arg == "--runtime-option":
+        shift()
+        option=arg
+        FLAGS+=f" {option}"
+        if option == "-Xmethod-trace":
             # Method tracing can slow some tests down a lot.
-            TIME_OUT_EXTRA=$((${TIME_OUT_EXTRA} + 1200))
-        fi
-        shift
-    elif [ "x$1" = "x--boot" ]; then
-        shift
-        BOOT_IMAGE="$1"
-        shift
-    elif [ "x$1" = "x--relocate" ]; then
+            TIME_OUT_EXTRA+=1200
+        shift()
+    elif arg == "--boot":
+        shift()
+        BOOT_IMAGE=arg
+        shift()
+    elif arg == "--relocate":
         RELOCATE="y"
-        shift
-    elif [ "x$1" = "x--no-relocate" ]; then
+        shift()
+    elif arg == "--no-relocate":
         RELOCATE="n"
-        shift
-    elif [ "x$1" = "x--prebuild" ]; then
+        shift()
+    elif arg == "--prebuild":
         PREBUILD="y"
-        shift
-    elif [ "x$1" = "x--compact-dex-level" ]; then
-        shift
-        COMPILE_FLAGS="${COMPILE_FLAGS} --compact-dex-level=$1"
-        shift
-    elif [ "x$1" = "x--jvmti-redefine-stress" ]; then
+        shift()
+    elif arg == "--compact-dex-level":
+        shift()
+        COMPILE_FLAGS+=f" --compact-dex-level={arg}"
+        shift()
+    elif arg == "--jvmti-redefine-stress":
         # APP_IMAGE doesn't really work with jvmti redefine stress
         USE_JVMTI="y"
         APP_IMAGE="n"
         SECONDARY_APP_IMAGE="n"
         JVMTI_STRESS="y"
         JVMTI_REDEFINE_STRESS="y"
-        shift
-    elif [ "x$1" = "x--jvmti-step-stress" ]; then
+        shift()
+    elif arg == "--jvmti-step-stress":
         USE_JVMTI="y"
         JVMTI_STRESS="y"
         JVMTI_STEP_STRESS="y"
-        shift
-    elif [ "x$1" = "x--jvmti-field-stress" ]; then
+        shift()
+    elif arg == "--jvmti-field-stress":
         USE_JVMTI="y"
         JVMTI_STRESS="y"
         JVMTI_FIELD_STRESS="y"
-        shift
-    elif [ "x$1" = "x--jvmti-trace-stress" ]; then
+        shift()
+    elif arg == "--jvmti-trace-stress":
         USE_JVMTI="y"
         JVMTI_STRESS="y"
         JVMTI_TRACE_STRESS="y"
-        shift
-    elif [ "x$1" = "x--no-app-image" ]; then
+        shift()
+    elif arg == "--no-app-image":
         APP_IMAGE="n"
-        shift
-    elif [ "x$1" = "x--no-secondary-app-image" ]; then
+        shift()
+    elif arg == "--no-secondary-app-image":
         SECONDARY_APP_IMAGE="n"
-        shift
-    elif [ "x$1" = "x--secondary-class-loader-context" ]; then
-        shift
-        SECONDARY_CLASS_LOADER_CONTEXT="$1"
-        shift
-    elif [ "x$1" = "x--no-secondary-compilation" ]; then
+        shift()
+    elif arg == "--secondary-class-loader-context":
+        shift()
+        SECONDARY_CLASS_LOADER_CONTEXT=arg
+        shift()
+    elif arg == "--no-secondary-compilation":
         SECONDARY_COMPILATION="n"
-        shift
-    elif [ "x$1" = "x--host" ]; then
+        shift()
+    elif arg == "--host":
         HOST="y"
-        ANDROID_ROOT="${ANDROID_HOST_OUT}"
-        ANDROID_ART_ROOT="${ANDROID_HOST_OUT}/com.android.art"
-        ANDROID_I18N_ROOT="${ANDROID_HOST_OUT}/com.android.i18n"
-        ANDROID_TZDATA_ROOT="${ANDROID_HOST_OUT}/com.android.tzdata"
+        ANDROID_ROOT=ANDROID_HOST_OUT
+        ANDROID_ART_ROOT=f"{ANDROID_HOST_OUT}/com.android.art"
+        ANDROID_I18N_ROOT=f"{ANDROID_HOST_OUT}/com.android.i18n"
+        ANDROID_TZDATA_ROOT=f"{ANDROID_HOST_OUT}/com.android.tzdata"
         # On host, we default to using the symlink, as the PREFER_32BIT
         # configuration is the only configuration building a 32bit version of
         # dex2oat.
         DEX2OAT_DEBUG_BINARY="dex2oatd"
         DEX2OAT_NDEBUG_BINARY="dex2oat"
-        shift
-    elif [ "x$1" = "x--bionic" ]; then
+        shift()
+    elif arg == "--bionic":
         BIONIC="y"
         # We need to create an ANDROID_ROOT because currently we cannot create
         # the frameworks/libcore with linux_bionic so we need to use the normal
         # host ones which are in a different location.
         CREATE_ANDROID_ROOT="y"
-        shift
-    elif [ "x$1" = "x--runtime-extracted-zipapex" ]; then
-        shift
+        shift()
+    elif arg == "--runtime-extracted-zipapex":
+        shift()
         USE_EXTRACTED_ZIPAPEX="y"
-        EXTRACTED_ZIPAPEX_LOC="$1"
-        shift
-    elif [ "x$1" = "x--runtime-zipapex" ]; then
-        shift
+        EXTRACTED_ZIPAPEX_LOC=arg
+        shift()
+    elif arg == "--runtime-zipapex":
+        shift()
         USE_ZIPAPEX="y"
-        ZIPAPEX_LOC="$1"
+        ZIPAPEX_LOC=arg
         # TODO (b/119942078): Currently apex does not support
         # symlink_preferred_arch so we will not have a dex2oatd to execute and
         # need to manually provide
         # dex2oatd64.
         DEX2OAT_DEBUG_BINARY="dex2oatd64"
-        shift
-    elif [ "x$1" = "x--no-prebuild" ]; then
+        shift()
+    elif arg == "--no-prebuild":
         PREBUILD="n"
-        shift
-    elif [ "x$1" = "x--no-image" ]; then
+        shift()
+    elif arg == "--no-image":
         HAVE_IMAGE="n"
-        shift
-    elif [ "x$1" = "x--secondary" ]; then
-        SECONDARY_DEX=":$DEX_LOCATION/$TEST_NAME-ex.jar"
+        shift()
+    elif arg == "--secondary":
+        SECONDARY_DEX=f":{DEX_LOCATION}/{TEST_NAME}-ex.jar"
         # Enable cfg-append to make sure we get the dump for both dex files.
         # (otherwise the runtime compilation of the secondary dex will overwrite
         # the dump of the first one).
-        FLAGS="${FLAGS} -Xcompiler-option --dump-cfg-append"
-        COMPILE_FLAGS="${COMPILE_FLAGS} --dump-cfg-append"
-        shift
-    elif [ "x$1" = "x--with-agent" ]; then
-        shift
+        FLAGS+=" -Xcompiler-option --dump-cfg-append"
+        COMPILE_FLAGS+=" --dump-cfg-append"
+        shift()
+    elif arg == "--with-agent":
+        shift()
         USE_JVMTI="y"
-        WITH_AGENT+=("$1")
-        shift
-    elif [ "x$1" = "x--debug-wrap-agent" ]; then
+        WITH_AGENT.append(arg)
+        shift()
+    elif arg == "--debug-wrap-agent":
         WRAP_DEBUGGER_AGENT="y"
-        shift
-    elif [ "x$1" = "x--debug-agent" ]; then
-        shift
+        shift()
+    elif arg == "--debug-agent":
+        shift()
         DEBUGGER="agent"
         USE_JVMTI="y"
-        DEBUGGER_AGENT="$1"
+        DEBUGGER_AGENT=arg
         TIME_OUT="n"
-        shift
-    elif [ "x$1" = "x--debug" ]; then
+        shift()
+    elif arg == "--debug":
         USE_JVMTI="y"
         DEBUGGER="y"
         TIME_OUT="n"
-        shift
-    elif [ "x$1" = "x--gdbserver-port" ]; then
-        shift
-        GDBSERVER_PORT=$1
-        shift
-    elif [ "x$1" = "x--gdbserver-bin" ]; then
-        shift
-        GDBSERVER_HOST=$1
-        GDBSERVER_DEVICE=$1
-        shift
-    elif [ "x$1" = "x--gdbserver" ]; then
+        shift()
+    elif arg == "--gdbserver-port":
+        shift()
+        GDBSERVER_PORT=arg
+        shift()
+    elif arg == "--gdbserver-bin":
+        shift()
+        GDBSERVER_HOST=arg
+        GDBSERVER_DEVICE=arg
+        shift()
+    elif arg == "--gdbserver":
         USE_GDBSERVER="y"
         DEV_MODE="y"
         TIME_OUT="n"
-        shift
-    elif [ "x$1" = "x--gdb" ]; then
+        shift()
+    elif arg == "--gdb":
         USE_GDB="y"
         DEV_MODE="y"
         TIME_OUT="n"
-        shift
-    elif [ "x$1" = "x--gdb-arg" ]; then
-        shift
-        gdb_arg="$1"
-        GDB_ARGS="${GDB_ARGS} $gdb_arg"
-        shift
-    elif [ "x$1" = "x--gdb-dex2oat" ]; then
+        shift()
+    elif arg == "--gdb-arg":
+        shift()
+        gdb_arg=arg
+        GDB_ARGS+=f" {gdb_arg}"
+        shift()
+    elif arg == "--gdb-dex2oat":
         USE_GDB_DEX2OAT="y"
         DEV_MODE="y"
         TIME_OUT="n"
-        shift
-    elif [ "x$1" = "x--gdb-dex2oat-args" ]; then
-        shift
-        for arg in $(echo $1 | tr ";" " "); do
-          GDB_DEX2OAT_ARGS+="$arg "
-        done
-        shift
-    elif [ "x$1" = "x--zygote" ]; then
+        shift()
+    elif arg == "--gdb-dex2oat-args":
+        shift()
+        for arg in arg.split(";"):
+          GDB_DEX2OAT_ARGS+=f"{arg} "
+        shift()
+    elif arg == "--zygote":
         ZYGOTE="-Xzygote"
-        msg "Spawning from zygote"
-        shift
-    elif [ "x$1" = "x--dev" ]; then
+        msg("Spawning from zygote")
+        shift()
+    elif arg == "--dev":
         DEV_MODE="y"
-        shift
-    elif [ "x$1" = "x--interpreter" ]; then
+        shift()
+    elif arg == "--interpreter":
         INTERPRETER="y"
-        shift
-    elif [ "x$1" = "x--jit" ]; then
+        shift()
+    elif arg == "--jit":
         JIT="y"
-        shift
-    elif [ "x$1" = "x--baseline" ]; then
-        FLAGS="${FLAGS} -Xcompiler-option --baseline"
-        COMPILE_FLAGS="${COMPILE_FLAGS} --baseline"
-        shift
-    elif [ "x$1" = "x--jvm" ]; then
+        shift()
+    elif arg == "--baseline":
+        FLAGS+=" -Xcompiler-option --baseline"
+        COMPILE_FLAGS+=" --baseline"
+        shift()
+    elif arg == "--jvm":
         USE_JVM="y"
-        shift
-    elif [ "x$1" = "x--invoke-with" ]; then
-        shift
-        if [ "x$1" = "x" ]; then
-            error_msg "$0 missing argument to --invoke-with"
-            exit 1
-        fi
-        if [ "x$INVOKE_WITH" = "x" ]; then
-            INVOKE_WITH="$1"
-        else
-            INVOKE_WITH="$INVOKE_WITH $1"
-        fi
-        shift
-    elif [ "x$1" = "x--no-verify" ]; then
+        shift()
+    elif arg == "--invoke-with":
+        shift()
+        if arg == "":
+            error_msg("missing argument to --invoke-with")
+            sys.exit(1)
+        if INVOKE_WITH == "":
+            INVOKE_WITH=arg
+        else:
+            INVOKE_WITH+=f" {arg}"
+        shift()
+    elif arg == "--no-verify":
         VERIFY="n"
-        shift
-    elif [ "x$1" = "x--verify-soft-fail" ]; then
+        shift()
+    elif arg == "--verify-soft-fail":
         VERIFY="s"
-        shift
-    elif [ "x$1" = "x--no-optimize" ]; then
+        shift()
+    elif arg == "--no-optimize":
         OPTIMIZE="n"
-        shift
-    elif [ "x$1" = "x--chroot" ]; then
-        shift
-        CHROOT="$1"
-        shift
-    elif [ "x$1" = "x--simpleperf" ]; then
+        shift()
+    elif arg == "--chroot":
+        shift()
+        CHROOT=arg
+        shift()
+    elif arg == "--simpleperf":
         SIMPLEPERF="yes"
-        shift
-    elif [ "x$1" = "x--android-root" ]; then
-        shift
-        ANDROID_ROOT="$1"
-        shift
-    elif [ "x$1" = "x--android-i18n-root" ]; then
-        shift
-        ANDROID_I18N_ROOT="$1"
-        shift
-    elif [ "x$1" = "x--android-art-root" ]; then
-        shift
-        ANDROID_ART_ROOT="$1"
-        shift
-    elif [ "x$1" = "x--android-tzdata-root" ]; then
-        shift
-        ANDROID_TZDATA_ROOT="$1"
-        shift
-    elif [ "x$1" = "x--instruction-set-features" ]; then
-        shift
-        INSTRUCTION_SET_FEATURES="$1"
-        shift
-    elif [ "x$1" = "x--timeout" ]; then
-        shift
-        TIME_OUT_VALUE="$1"
-        shift
-    elif [ "x$1" = "x--" ]; then
-        shift
+        shift()
+    elif arg == "--android-root":
+        shift()
+        ANDROID_ROOT=arg
+        shift()
+    elif arg == "--android-i18n-root":
+        shift()
+        ANDROID_I18N_ROOT=arg
+        shift()
+    elif arg == "--android-art-root":
+        shift()
+        ANDROID_ART_ROOT=arg
+        shift()
+    elif arg == "--android-tzdata-root":
+        shift()
+        ANDROID_TZDATA_ROOT=arg
+        shift()
+    elif arg == "--instruction-set-features":
+        shift()
+        INSTRUCTION_SET_FEATURES=arg
+        shift()
+    elif arg == "--timeout":
+        shift()
+        TIME_OUT_VALUE=int(arg)
+        shift()
+    elif arg == "--":
+        shift()
         break
-    elif [ "x$1" = "x--64" ]; then
+    elif arg == "--64":
         SUFFIX64="64"
         ISA="x86_64"
         GDBSERVER_DEVICE="gdbserver64"
         DALVIKVM="dalvikvm64"
         LIBRARY_DIRECTORY="lib64"
         TEST_DIRECTORY="nativetest64"
-        ARCHITECTURES_PATTERN="${ARCHITECTURES_64}"
+        ARCHITECTURES_PATTERN=ARCHITECTURES_64
         GET_DEVICE_ISA_BITNESS_FLAG="--64"
         DEX2OAT_NDEBUG_BINARY="dex2oat64"
         DEX2OAT_DEBUG_BINARY="dex2oatd64"
-        shift
-    elif [ "x$1" = "x--experimental" ]; then
-        if [ "$#" -lt 2 ]; then
-            error_msg "missing --experimental option"
-            exit 1
-        fi
-        EXPERIMENTAL="$EXPERIMENTAL $2"
-        shift 2
-    elif [ "x$1" = "x--external-log-tags" ]; then
+        shift()
+    elif arg == "--experimental":
+        if len(args) < 2:
+            error_msg("missing --experimental option")
+            sys.exit(1)
+        shift()
+        EXPERIMENTAL.append(arg)
+        shift()
+    elif arg == "--external-log-tags":
         EXTERNAL_LOG_TAGS="y"
-        shift
-    elif [ "x$1" = "x--dry-run" ]; then
+        shift()
+    elif arg == "--dry-run":
         DRY_RUN="y"
-        shift
-    elif [ "x$1" = "x--vdex" ]; then
+        shift()
+    elif arg == "--vdex":
         TEST_VDEX="y"
-        shift
-    elif [ "x$1" = "x--dex2oat-dm" ]; then
+        shift()
+    elif arg == "--dex2oat-dm":
         TEST_DEX2OAT_DM="y"
-        shift
-    elif [ "x$1" = "x--runtime-dm" ]; then
+        shift()
+    elif arg == "--runtime-dm":
         TEST_RUNTIME_DM="y"
-        shift
-    elif [ "x$1" = "x--vdex-filter" ]; then
-        shift
-        option="$1"
-        VDEX_ARGS="${VDEX_ARGS} --compiler-filter=$option"
-        shift
-    elif [ "x$1" = "x--vdex-arg" ]; then
-        shift
-        VDEX_ARGS="${VDEX_ARGS} $1"
-        shift
-    elif [ "x$1" = "x--sync" ]; then
+        shift()
+    elif arg == "--vdex-filter":
+        shift()
+        option=arg
+        VDEX_ARGS+=f" --compiler-filter={option}"
+        shift()
+    elif arg == "--vdex-arg":
+        shift()
+        VDEX_ARGS+=f" {arg}"
+        shift()
+    elif arg == "--sync":
         SYNC_BEFORE_RUN="y"
-        shift
-    elif [ "x$1" = "x--profile" ]; then
+        shift()
+    elif arg == "--profile":
         PROFILE="y"
-        shift
-    elif [ "x$1" = "x--random-profile" ]; then
+        shift()
+    elif arg == "--random-profile":
         RANDOM_PROFILE="y"
-        shift
-    elif expr "x$1" : "x--" >/dev/null 2>&1; then
-        error_msg "unknown $0 option: $1"
-        exit 1
-    else
+        shift()
+    elif arg.startswith("--"):
+        error_msg(f"unknown option: {arg}")
+        sys.exit(1)
+    else:
         break
-    fi
-done
 
 # HACK: Force the use of `signal_dumper` on host.
-if [[ "$HOST" = "y" ]]; then
+if HOST == "y":
   TIME_OUT="timeout"
-fi
 
 # If you change this, update the timeout in testrunner.py as well.
-if [ -z "$TIME_OUT_VALUE" ] ; then
+if not TIME_OUT_VALUE:
   # 10 minutes is the default.
   TIME_OUT_VALUE=600
 
   # For sanitized builds use a larger base.
   # TODO: Consider sanitized target builds?
-  if [ "x$SANITIZE_HOST" != "x" ] ; then
+  if SANITIZE_HOST != "":
     TIME_OUT_VALUE=1500  # 25 minutes.
-  fi
 
-  TIME_OUT_VALUE=$((${TIME_OUT_VALUE} + ${TIME_OUT_EXTRA}))
-fi
+  TIME_OUT_VALUE+=TIME_OUT_EXTRA
 
 # Escape hatch for slow hosts or devices. Accept an environment variable as a timeout factor.
-if [ ! -z "$ART_TIME_OUT_MULTIPLIER" ] ; then
-  TIME_OUT_VALUE=$((${TIME_OUT_VALUE} * ${ART_TIME_OUT_MULTIPLIER}))
-fi
+if ART_TIME_OUT_MULTIPLIER:
+  TIME_OUT_VALUE*=ART_TIME_OUT_MULTIPLIER
 
 # The DEX_LOCATION with the chroot prefix, if any.
-CHROOT_DEX_LOCATION="$CHROOT$DEX_LOCATION"
+CHROOT_DEX_LOCATION=f"{CHROOT}{DEX_LOCATION}"
 
 # If running on device, determine the ISA of the device.
-if [ "$HOST" = "n" -a "$USE_JVM" = "n" ]; then
-  ISA=$("$ANDROID_BUILD_TOP/art/test/utils/get-device-isa" "$GET_DEVICE_ISA_BITNESS_FLAG")
-fi
+if HOST == "n" and USE_JVM == "n":
+  ISA=run(f"{ANDROID_BUILD_TOP}/art/test/utils/get-device-isa {GET_DEVICE_ISA_BITNESS_FLAG}",
+          save_cmd=False).stdout.strip()
 
-if [ "$USE_JVM" = "n" ]; then
-    FLAGS="${FLAGS} ${ANDROID_FLAGS}"
+if USE_JVM == "n":
+    FLAGS+=f" {ANDROID_FLAGS}"
     # we don't want to be trying to get adbconnections since the plugin might
     # not have been built.
-    FLAGS="${FLAGS} -XjdwpProvider:none"
-    for feature in ${EXPERIMENTAL}; do
-        FLAGS="${FLAGS} -Xexperimental:${feature} -Xcompiler-option --runtime-arg -Xcompiler-option -Xexperimental:${feature}"
-        COMPILE_FLAGS="${COMPILE_FLAGS} --runtime-arg -Xexperimental:${feature}"
-    done
-fi
+    FLAGS+=" -XjdwpProvider:none"
+    for feature in EXPERIMENTAL:
+        FLAGS+=f" -Xexperimental:{feature} -Xcompiler-option --runtime-arg -Xcompiler-option -Xexperimental:{feature}"
+        COMPILE_FLAGS=f"{COMPILE_FLAGS} --runtime-arg -Xexperimental:{feature}"
 
-if [ "$CREATE_ANDROID_ROOT" = "y" ]; then
-    ANDROID_ROOT=$DEX_LOCATION/android-root
-fi
+if CREATE_ANDROID_ROOT == "y":
+    ANDROID_ROOT=f"{DEX_LOCATION}/android-root"
 
-if [ "x$1" = "x" ] ; then
+if not arg:
   MAIN="Main"
-else
-  MAIN="$1"
-  shift
-fi
+else:
+  MAIN=arg
+  shift()
 
-if [ "$ZYGOTE" = "" ]; then
-    if [ "$OPTIMIZE" = "y" ]; then
-        if [ "$VERIFY" = "y" ]; then
+test_args = (" " + " ".join(args)) if args else ""
+
+if ZYGOTE == "":
+    if OPTIMIZE == "y":
+        if VERIFY == "y":
             DEX_OPTIMIZE="-Xdexopt:verified"
-        else
+        else:
             DEX_OPTIMIZE="-Xdexopt:all"
-        fi
-        msg "Performing optimizations"
-    else
+        msg("Performing optimizations")
+    else:
         DEX_OPTIMIZE="-Xdexopt:none"
-        msg "Skipping optimizations"
-    fi
+        msg("Skipping optimizations")
 
-    if [ "$VERIFY" = "y" ]; then
+    if VERIFY == "y":
         JVM_VERIFY_ARG="-Xverify:all"
-        msg "Performing verification"
-    elif [ "$VERIFY" = "s" ]; then
+        msg("Performing verification")
+    elif VERIFY == "s":
         JVM_VERIFY_ARG="Xverify:all"
         DEX_VERIFY="-Xverify:softfail"
-        msg "Forcing verification to be soft fail"
-    else # VERIFY = "n"
+        msg("Forcing verification to be soft fail")
+    else: # VERIFY == "n"
         DEX_VERIFY="-Xverify:none"
         JVM_VERIFY_ARG="-Xverify:none"
-        msg "Skipping verification"
-    fi
-fi
+        msg("Skipping verification")
 
-msg "------------------------------"
+msg("------------------------------")
 
-if [ "$DEBUGGER" = "y" ]; then
+if DEBUGGER == "y":
   # Use this instead for ddms and connect by running 'ddms':
   # DEBUGGER_OPTS="-XjdwpOptions=server=y,suspend=y -XjdwpProvider:adbconnection"
   # TODO: add a separate --ddms option?
 
   PORT=12345
-  msg "Waiting for jdb to connect:"
-  if [ "$HOST" = "n" ]; then
-    msg "    adb forward tcp:$PORT tcp:$PORT"
-  fi
-  msg "    jdb -attach localhost:$PORT"
-  if [ "$USE_JVM" = "n" ]; then
+  msg("Waiting for jdb to connect:")
+  if HOST == "n":
+    msg(f"    adb forward tcp:{PORT} tcp:{PORT}")
+  msg(f"    jdb -attach localhost:{PORT}")
+  if USE_JVM == "n":
     # Use the default libjdwp agent. Use --debug-agent to use a custom one.
-    DEBUGGER_OPTS="-agentpath:libjdwp.so=transport=dt_socket,address=$PORT,server=y,suspend=y -XjdwpProvider:internal"
-  else
-    DEBUGGER_OPTS="-agentlib:jdwp=transport=dt_socket,address=$PORT,server=y,suspend=y"
-  fi
-elif [ "$DEBUGGER" = "agent" ]; then
+    DEBUGGER_OPTS=f"-agentpath:libjdwp.so=transport=dt_socket,address={PORT},server=y,suspend=y -XjdwpProvider:internal"
+  else:
+    DEBUGGER_OPTS=f"-agentlib:jdwp=transport=dt_socket,address={PORT},server=y,suspend=y"
+elif DEBUGGER == "agent":
   PORT=12345
   # TODO Support ddms connection and support target.
-  if [ "$HOST" = "n" ]; then
-    error_msg "--debug-agent not supported yet for target!"
-    exit 1
-  fi
-  AGENTPATH=${DEBUGGER_AGENT}
-  if [ "$WRAP_DEBUGGER_AGENT" = "y" ]; then
-    WRAPPROPS="${ANDROID_ROOT}/${LIBRARY_DIRECTORY}/libwrapagentpropertiesd.so"
-    if [ "$TEST_IS_NDEBUG" = "y" ]; then
-      WRAPPROPS="${ANDROID_ROOT}/${LIBRARY_DIRECTORY}/libwrapagentproperties.so"
-    fi
-    AGENTPATH="${WRAPPROPS}=${ANDROID_BUILD_TOP}/art/tools/libjdwp-compat.props,${AGENTPATH}"
-  fi
-  msg "Connect to localhost:$PORT"
-  DEBUGGER_OPTS="-agentpath:${AGENTPATH}=transport=dt_socket,address=$PORT,server=y,suspend=y"
-fi
+  if HOST == "n":
+    error_msg("--debug-agent not supported yet for target!")
+    sys.exit(1)
+  AGENTPATH=DEBUGGER_AGENT
+  if WRAP_DEBUGGER_AGENT == "y":
+    WRAPPROPS=f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}/libwrapagentpropertiesd.so"
+    if TEST_IS_NDEBUG == "y":
+      WRAPPROPS=f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}/libwrapagentproperties.so"
+    AGENTPATH=f"{WRAPPROPS}={ANDROID_BUILD_TOP}/art/tools/libjdwp-compat.props,{AGENTPATH}"
+  msg(f"Connect to localhost:{PORT}")
+  DEBUGGER_OPTS=f"-agentpath:{AGENTPATH}=transport=dt_socket,address={PORT},server=y,suspend=y"
 
-for agent in "${WITH_AGENT[@]}"; do
-  FLAGS="${FLAGS} -agentpath:${agent}"
-done
+for agent in WITH_AGENT:
+  FLAGS+=f" -agentpath:{agent}"
 
-if [ "$USE_JVMTI" = "y" ]; then
-  if [ "$USE_JVM" = "n" ]; then
-    plugin=libopenjdkjvmtid.so
-    if  [[ "$TEST_IS_NDEBUG" = "y" ]]; then
-      plugin=libopenjdkjvmti.so
-    fi
+if USE_JVMTI == "y":
+  if USE_JVM == "n":
+    plugin="libopenjdkjvmtid.so"
+    if TEST_IS_NDEBUG == "y":
+      plugin="libopenjdkjvmti.so"
     # We used to add flags here that made the runtime debuggable but that is not
     # needed anymore since the plugin can do it for us now.
-    FLAGS="${FLAGS} -Xplugin:${plugin}"
+    FLAGS+=f" -Xplugin:{plugin}"
 
     # For jvmti tests, set the threshold of compilation to 0, so we jit early to
     # provide better test coverage for jvmti + jit. This means we won't run
     # the default --jit configuration but it is not too important test scenario for
     # jvmti tests. This is art specific flag, so don't use it with jvm.
-    FLAGS="${FLAGS} -Xjitthreshold:0"
-  fi
-fi
+    FLAGS+=" -Xjitthreshold:0"
 
 # Add the libdir to the argv passed to the main function.
-if [ "$ADD_LIBDIR_ARGUMENTS" = "y" ]; then
-  if [[ "$HOST" = "y" ]]; then
-    ARGS="${ARGS} ${ANDROID_HOST_OUT}/${TEST_DIRECTORY}/"
-  else
-    ARGS="${ARGS} /data/${TEST_DIRECTORY}/art/${ISA}/"
-  fi
-fi
-if [ "$IS_JVMTI_TEST" = "y" ]; then
-  agent=libtiagentd.so
-  lib=tiagentd
-  if  [[ "$TEST_IS_NDEBUG" = "y" ]]; then
-    agent=libtiagent.so
-    lib=tiagent
-  fi
+if ADD_LIBDIR_ARGUMENTS == "y":
+  if HOST == "y":
+    ARGS+=f" {ANDROID_HOST_OUT}/{TEST_DIRECTORY}/"
+  else:
+    ARGS+=f" /data/{TEST_DIRECTORY}/art/{ISA}/"
+if IS_JVMTI_TEST == "y":
+  agent="libtiagentd.so"
+  lib="tiagentd"
+  if TEST_IS_NDEBUG == "y":
+    agent="libtiagent.so"
+    lib="tiagent"
 
-  ARGS="${ARGS} ${lib}"
-  if [[ "$USE_JVM" = "y" ]]; then
-    FLAGS="${FLAGS} -agentpath:${ANDROID_HOST_OUT}/nativetest64/${agent}=${TEST_NAME},jvm"
-  else
-    FLAGS="${FLAGS} -agentpath:${agent}=${TEST_NAME},art"
-  fi
-fi
+  ARGS+=f" {lib}"
+  if USE_JVM == "y":
+    FLAGS+=f" -agentpath:{ANDROID_HOST_OUT}/nativetest64/{agent}={TEST_NAME},jvm"
+  else:
+    FLAGS+=f" -agentpath:{agent}={TEST_NAME},art"
 
-if [[ "$JVMTI_STRESS" = "y" ]]; then
-  agent=libtistressd.so
-  if  [[ "$TEST_IS_NDEBUG" = "y" ]]; then
-    agent=libtistress.so
-  fi
+if JVMTI_STRESS == "y":
+  agent="libtistressd.so"
+  if TEST_IS_NDEBUG == "y":
+    agent="libtistress.so"
 
   # Just give it a default start so we can always add ',' to it.
   agent_args="jvmti-stress"
-  if [[ "$JVMTI_REDEFINE_STRESS" = "y" ]]; then
+  if JVMTI_REDEFINE_STRESS == "y":
     # We really cannot do this on RI so don't both passing it in that case.
-    if [[ "$USE_JVM" = "n" ]]; then
-      agent_args="${agent_args},redefine"
-    fi
-  fi
-  if [[ "$JVMTI_FIELD_STRESS" = "y" ]]; then
-    agent_args="${agent_args},field"
-  fi
-  if [[ "$JVMTI_STEP_STRESS" = "y" ]]; then
-    agent_args="${agent_args},step"
-  fi
-  if [[ "$JVMTI_TRACE_STRESS" = "y" ]]; then
-    agent_args="${agent_args},trace"
-  fi
+    if USE_JVM == "n":
+      agent_args=f"{agent_args},redefine"
+  if JVMTI_FIELD_STRESS == "y":
+    agent_args=f"{agent_args},field"
+  if JVMTI_STEP_STRESS == "y":
+    agent_args=f"{agent_args},step"
+  if JVMTI_TRACE_STRESS == "y":
+    agent_args=f"{agent_args},trace"
   # In the future add onto this;
-  if [[ "$USE_JVM" = "y" ]]; then
-    FLAGS="${FLAGS} -agentpath:${ANDROID_HOST_OUT}/nativetest64/${agent}=${agent_args}"
-  else
-    FLAGS="${FLAGS} -agentpath:${agent}=${agent_args}"
-  fi
-fi
+  if USE_JVM == "y":
+    FLAGS+=f" -agentpath:{ANDROID_HOST_OUT}/nativetest64/{agent}={agent_args}"
+  else:
+    FLAGS+=f" -agentpath:{agent}={agent_args}"
 
-if [ "$USE_JVM" = "y" ]; then
-  export LD_LIBRARY_PATH=${ANDROID_HOST_OUT}/lib64
+if USE_JVM == "y":
+  export(f"LD_LIBRARY_PATH", f"{ANDROID_HOST_OUT}/lib64")
   # Some jvmti tests are flaky without -Xint on the RI.
-  if [ "$IS_JVMTI_TEST" = "y" ]; then
-    FLAGS="${FLAGS} -Xint"
-  fi
+  if IS_JVMTI_TEST == "y":
+    FLAGS+=" -Xint"
   # Xmx is necessary since we don't pass down the ART flags to JVM.
   # We pass the classes2 path whether it's used (src-multidex) or not.
-  cmdline="${JAVA} ${DEBUGGER_OPTS} ${JVM_VERIFY_ARG} -Xmx256m -classpath classes:classes2 ${FLAGS} $MAIN $@ ${ARGS}"
-  if [ "$DEV_MODE" = "y" ]; then
-    echo $cmdline
-  fi
-  if [ "$CREATE_RUNNER" = "y" ]; then
-    echo "#!/bin/bash" > runit.sh
-    echo "export LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH\""
-    echo $cmdline >> runit.sh
-    chmod u+x runit.sh
-    echo "Runnable test script written to $PWD/runit.sh"
-  else
-    $cmdline
-  fi
-  exit
-fi
+  cmdline=f"{JAVA} {DEBUGGER_OPTS} {JVM_VERIFY_ARG} -Xmx256m -classpath classes:classes2 {FLAGS} {MAIN} {test_args} {ARGS}"
+  if DEV_MODE == "y":
+    print(cmdline)
+  if CREATE_RUNNER == "y":
+    with open("runit.sh", "w") as f:
+      f.write("#!/bin/bash")
+      print(f"export LD_LIBRARY_PATH=\"{LD_LIBRARY_PATH}\"")
+      f.write(cmdline)
+    os.chmod("runit.sh", 0o777)
+    pwd = os.getcwd()
+    print(f"Runnable test script written to {pwd}/runit.sh")
+    sys.exit(0)
+  else:
+    exit_value=run(cmdline, check=False, capture_output=False).returncode
+    sys.exit(exit_value)
 
-readonly b_path=$(get_apex_bootclasspath ${HOST})
-readonly b_path_locations=$(get_apex_bootclasspath_locations ${HOST})
+b_path=get_apex_bootclasspath(HOST)
+b_path_locations=get_apex_bootclasspath_locations(HOST)
 
-BCPEX=
-if [ -f "$TEST_NAME-bcpex.jar" ] ; then
-  BCPEX=":$DEX_LOCATION/$TEST_NAME-bcpex.jar"
-fi
+BCPEX=""
+if isfile(f"{TEST_NAME}-bcpex.jar"):
+  BCPEX=f":{DEX_LOCATION}/{TEST_NAME}-bcpex.jar"
 
 # Pass down the bootclasspath
-FLAGS="${FLAGS} -Xbootclasspath:${b_path}${BCPEX}"
-FLAGS="${FLAGS} -Xbootclasspath-locations:${b_path_locations}${BCPEX}"
-COMPILE_FLAGS="${COMPILE_FLAGS} --runtime-arg -Xbootclasspath:${b_path}"
-COMPILE_FLAGS="${COMPILE_FLAGS} --runtime-arg -Xbootclasspath-locations:${b_path_locations}"
+FLAGS+=f" -Xbootclasspath:{b_path}{BCPEX}"
+FLAGS+=f" -Xbootclasspath-locations:{b_path_locations}{BCPEX}"
+COMPILE_FLAGS+=f" --runtime-arg -Xbootclasspath:{b_path}"
+COMPILE_FLAGS+=f" --runtime-arg -Xbootclasspath-locations:{b_path_locations}"
 
-if [ "$HAVE_IMAGE" = "n" ]; then
+if HAVE_IMAGE == "n":
     # Disable image dex2oat - this will forbid the runtime to patch or compile an image.
-    FLAGS="${FLAGS} -Xnoimage-dex2oat"
+    FLAGS+=" -Xnoimage-dex2oat"
 
     # We'll abuse a second flag here to test different behavior. If --relocate, use the
     # existing image - relocation will fail as patching is disallowed. If --no-relocate,
     # pass a non-existent image - compilation will fail as dex2oat is disallowed.
-    if [ "${RELOCATE}" = "n" ] ; then
+    if RELOCATE == "n":
       BOOT_IMAGE="/system/non-existent/boot.art"
-    fi
     # App images cannot be generated without a boot image.
     APP_IMAGE="n"
-fi
-DALVIKVM_BOOT_OPT="-Ximage:${BOOT_IMAGE}"
+DALVIKVM_BOOT_OPT=f"-Ximage:{BOOT_IMAGE}"
 
-if [ "$USE_GDB_DEX2OAT" = "y" ]; then
-  if [ "$HOST" = "n" ]; then
-    echo "The --gdb-dex2oat option is not yet implemented for target." >&2
-    exit 1
-  fi
-fi
+if USE_GDB_DEX2OAT == "y":
+  if HOST == "n":
+    print("The --gdb-dex2oat option is not yet implemented for target.", file=sys.stderr)
+    sys.exit(1)
 
-if [ "$USE_GDB" = "y" ]; then
-  if [ "$USE_GDBSERVER" = "y" ]; then
-    error_msg "Cannot pass both --gdb and --gdbserver at the same time!"
-    exit 1
-  elif [ "$HOST" = "n" ]; then
+if USE_GDB == "y":
+  if USE_GDBSERVER == "y":
+    error_msg("Cannot pass both --gdb and --gdbserver at the same time!")
+    sys.exit(1)
+  elif HOST == "n":
     # We might not have any hostname resolution if we are using a chroot.
-    GDB="$GDBSERVER_DEVICE --no-startup-with-shell 127.0.0.1$GDBSERVER_PORT"
-  else
-    if [ `uname` = "Darwin" ]; then
-        GDB=lldb
-        GDB_ARGS="$GDB_ARGS -- $DALVIKVM"
-        DALVIKVM=
-    else
-        GDB=gdb
-        GDB_ARGS="$GDB_ARGS --args $DALVIKVM"
+    GDB=f"{GDBSERVER_DEVICE} --no-startup-with-shell 127.0.0.1{GDBSERVER_PORT}"
+  else:
+    if run("uname").stdout.strip() == "Darwin":
+        GDB="lldb"
+        GDB_ARGS+=f" -- {DALVIKVM}"
+        DALVIKVM=""
+    else:
+        GDB="gdb"
+        GDB_ARGS+=f" --args {DALVIKVM}"
         # Enable for Emacs "M-x gdb" support. TODO: allow extra gdb arguments on command line.
-        # gdbargs="--annotate=3 $gdbargs"
-    fi
-  fi
-elif [ "$USE_GDBSERVER" = "y" ]; then
-  if [ "$HOST" = "n" ]; then
+        # gdbargs=f"--annotate=3 {gdbargs}"
+elif USE_GDBSERVER == "y":
+  if HOST == "n":
     # We might not have any hostname resolution if we are using a chroot.
-    GDB="$GDBSERVER_DEVICE --no-startup-with-shell 127.0.0.1$GDBSERVER_PORT"
-  else
-    GDB="$GDBSERVER_HOST $GDBSERVER_PORT"
-  fi
-fi
+    GDB=f"{GDBSERVER_DEVICE} --no-startup-with-shell 127.0.0.1{GDBSERVER_PORT}"
+  else:
+    GDB=f"{GDBSERVER_HOST} {GDBSERVER_PORT}"
 
-if [ "$INTERPRETER" = "y" ]; then
-    INT_OPTS="${INT_OPTS} -Xint"
-fi
+if INTERPRETER == "y":
+    INT_OPTS+=" -Xint"
 
-if [ "$JIT" = "y" ]; then
-    INT_OPTS="${INT_OPTS} -Xusejit:true"
-else
-    INT_OPTS="${INT_OPTS} -Xusejit:false"
-fi
+if JIT == "y":
+    INT_OPTS+=" -Xusejit:true"
+else:
+    INT_OPTS+=" -Xusejit:false"
 
-if [ "$INTERPRETER" = "y" ] || [ "$JIT" = "y" ]; then
-  if [ "$VERIFY" = "y" ] ; then
-    INT_OPTS="${INT_OPTS} -Xcompiler-option --compiler-filter=verify"
-    COMPILE_FLAGS="${COMPILE_FLAGS} --compiler-filter=verify"
-  elif [ "$VERIFY" = "s" ]; then
-    INT_OPTS="${INT_OPTS} -Xcompiler-option --compiler-filter=extract"
-    COMPILE_FLAGS="${COMPILE_FLAGS} --compiler-filter=extract"
-    DEX_VERIFY="${DEX_VERIFY} -Xverify:softfail"
-  else # VERIFY = "n"
-    INT_OPTS="${INT_OPTS} -Xcompiler-option --compiler-filter=assume-verified"
-    COMPILE_FLAGS="${COMPILE_FLAGS} --compiler-filter=assume-verified"
-    DEX_VERIFY="${DEX_VERIFY} -Xverify:none"
-  fi
-fi
+if INTERPRETER == "y" or JIT == "y":
+  if VERIFY == "y":
+    INT_OPTS+=" -Xcompiler-option --compiler-filter=verify"
+    COMPILE_FLAGS+=" --compiler-filter=verify"
+  elif VERIFY == "s":
+    INT_OPTS+=" -Xcompiler-option --compiler-filter=extract"
+    COMPILE_FLAGS+=" --compiler-filter=extract"
+    DEX_VERIFY=f"{DEX_VERIFY} -Xverify:softfail"
+  else: # VERIFY == "n"
+    INT_OPTS+=" -Xcompiler-option --compiler-filter=assume-verified"
+    COMPILE_FLAGS+=" --compiler-filter=assume-verified"
+    DEX_VERIFY=f"{DEX_VERIFY} -Xverify:none"
 
 JNI_OPTS="-Xjnigreflimit:512 -Xcheck:jni"
 
-COMPILE_FLAGS="${COMPILE_FLAGS} --runtime-arg -Xnorelocate"
-if [ "$RELOCATE" = "y" ]; then
-    FLAGS="${FLAGS} -Xrelocate"
-else
-    FLAGS="$FLAGS -Xnorelocate"
-fi
+COMPILE_FLAGS+=" --runtime-arg -Xnorelocate"
+if RELOCATE == "y":
+    FLAGS+=" -Xrelocate"
+else:
+    FLAGS+=" -Xnorelocate"
 
-if [ "$BIONIC" = "y" ]; then
+if BIONIC == "y":
   # This is the location that soong drops linux_bionic builds. Despite being
   # called linux_bionic-x86 the build is actually amd64 (x86_64) only.
-  if [ ! -e "$OUT_DIR/soong/host/linux_bionic-x86" ]; then
-    error_msg "linux_bionic-x86 target doesn't seem to have been built!"
-    exit 1
-  fi
+  if not path.exists(f"{OUT_DIR}/soong/host/linux_bionic-x86"):
+    error_msg("linux_bionic-x86 target doesn't seem to have been built!")
+    sys.exit(1)
   # Set TIMEOUT_DUMPER manually so it works even with apex's
-  TIMEOUT_DUMPER=$OUT_DIR/soong/host/linux_bionic-x86/bin/signal_dumper
-fi
+  TIMEOUT_DUMPER=f"{OUT_DIR}/soong/host/linux_bionic-x86/bin/signal_dumper"
 
 # Prevent test from silently falling back to interpreter in no-prebuild mode. This happens
 # when DEX_LOCATION path is too long, because vdex/odex filename is constructed by taking
 # full path to dex, stripping leading '/', appending '@classes.vdex' and changing every
 # remaining '/' into '@'.
-if [ "$HOST" = "y" ]; then
-  max_filename_size=$(getconf NAME_MAX $DEX_LOCATION)
-else
+if HOST == "y":
+  max_filename_size=int(run(f"getconf NAME_MAX {DEX_LOCATION}", save_cmd=False).stdout)
+else:
   # There is no getconf on device, fallback to standard value.
   # See NAME_MAX in kernel <linux/limits.h>
   max_filename_size=255
-fi
 # Compute VDEX_NAME.
-DEX_LOCATION_STRIPPED="${DEX_LOCATION#/}"
-VDEX_NAME="${DEX_LOCATION_STRIPPED//\//@}@$TEST_NAME.jar@classes.vdex"
-if [ ${#VDEX_NAME} -gt $max_filename_size ]; then
-    echo "Dex location path too long:"
-    error_msg "$VDEX_NAME is ${#VDEX_NAME} character long, and the limit is $max_filename_size."
-    exit 1
-fi
+DEX_LOCATION_STRIPPED=DEX_LOCATION.lstrip("/")
+VDEX_NAME=f"{DEX_LOCATION_STRIPPED}@{TEST_NAME}.jar@classes.vdex".replace("/", "@")
+if len(VDEX_NAME) > max_filename_size:
+    print("Dex location path too long:")
+    error_msg(f"{VDEX_NAME} is {len(VDEX_NAME)} character long, and the limit is {max_filename_size}.")
+    sys.exit(1)
 
-if [ "$HOST" = "y" ]; then
+if HOST == "y":
   # On host, run binaries (`dex2oat(d)`, `dalvikvm`, `profman`) from the `bin`
   # directory under the "Android Root" (usually `out/host/linux-x86`).
   #
   # TODO(b/130295968): Adjust this if/when ART host artifacts are installed
   # under the ART root (usually `out/host/linux-x86/com.android.art`).
-  ANDROID_ART_BIN_DIR=$ANDROID_ROOT/bin
-else
+  ANDROID_ART_BIN_DIR=f"{ANDROID_ROOT}/bin"
+else:
   # On target, run binaries (`dex2oat(d)`, `dalvikvm`, `profman`) from the ART
   # APEX's `bin` directory. This means the linker will observe the ART APEX
   # linker configuration file (`/apex/com.android.art/etc/ld.config.txt`) for
   # these binaries.
-  ANDROID_ART_BIN_DIR=$ANDROID_ART_ROOT/bin
-fi
+  ANDROID_ART_BIN_DIR=f"{ANDROID_ART_ROOT}/bin"
 
 profman_cmdline="true"
 dex2oat_cmdline="true"
 vdex_cmdline="true"
 dm_cmdline="true"
-mkdir_locations="${DEX_LOCATION}/dalvik-cache/$ISA"
+mkdir_locations=f"{DEX_LOCATION}/dalvik-cache/{ISA}"
 strip_cmdline="true"
 sync_cmdline="true"
 linkroot_cmdline="true"
@@ -894,165 +909,145 @@
 installapex_cmdline="true"
 installapex_test_cmdline="true"
 
-linkdirs() {
-  find "$1" -maxdepth 1 -mindepth 1 -type d | xargs -i ln -sf '{}' "$2"
+def linkdirs(host_out: str, root: str):
+  dirs = list(filter(os.path.isdir, glob.glob(os.path.join(host_out, "*"))))
   # Also create a link for the boot image.
-  ln -sf $ANDROID_HOST_OUT/apex/art_boot_images "$2"
-}
+  dirs.append(f"{ANDROID_HOST_OUT}/apex/art_boot_images")
+  return " && ".join(f"ln -sf {dir} {root}" for dir in dirs)
 
-if [ "$CREATE_ANDROID_ROOT" = "y" ]; then
-  mkdir_locations="${mkdir_locations} ${ANDROID_ROOT}"
-  linkroot_cmdline="linkdirs ${ANDROID_HOST_OUT} ${ANDROID_ROOT}"
-  if [ "${BIONIC}" = "y" ]; then
+if CREATE_ANDROID_ROOT == "y":
+  mkdir_locations+=f" {ANDROID_ROOT}"
+  linkroot_cmdline=linkdirs(ANDROID_HOST_OUT, ANDROID_ROOT)
+  if BIONIC == "y":
     # TODO Make this overlay more generic.
-    linkroot_overlay_cmdline="linkdirs $OUT_DIR/soong/host/linux_bionic-x86 ${ANDROID_ROOT}"
-  fi
+    linkroot_overlay_cmdline=linkdirs(f"{OUT_DIR}/soong/host/linux_bionic-x86", ANDROID_ROOT)
   # Replace the boot image to a location expected by the runtime.
-  DALVIKVM_BOOT_OPT="-Ximage:${ANDROID_ROOT}/art_boot_images/javalib/boot.art"
-fi
+  DALVIKVM_BOOT_OPT=f"-Ximage:{ANDROID_ROOT}/art_boot_images/javalib/boot.art"
 
-if [ "$USE_ZIPAPEX" = "y" ]; then
+if USE_ZIPAPEX == "y":
   # TODO Currently this only works for linux_bionic zipapexes because those are
   # stripped and so small enough that the ulimit doesn't kill us.
-  mkdir_locations="${mkdir_locations} $DEX_LOCATION/zipapex"
+  mkdir_locations+=f" {DEX_LOCATION}/zipapex"
   zip_options="-qq"
-  if [ "$DEV_MODE" = "y" ]; then
+  if DEV_MODE == "y":
     zip_options=""
-  fi
-  setupapex_cmdline="unzip -o -u ${zip_options} ${ZIPAPEX_LOC} apex_payload.zip -d ${DEX_LOCATION}"
-  installapex_cmdline="unzip -o -u ${zip_options} ${DEX_LOCATION}/apex_payload.zip -d ${DEX_LOCATION}/zipapex"
-  ANDROID_ART_BIN_DIR=$DEX_LOCATION/zipapex/bin
-elif [ "$USE_EXTRACTED_ZIPAPEX" = "y" ]; then
+  setupapex_cmdline=f"unzip -o -u {zip_options} {ZIPAPEX_LOC} apex_payload.zip -d {DEX_LOCATION}"
+  installapex_cmdline=f"unzip -o -u {zip_options} {DEX_LOCATION}/apex_payload.zip -d {DEX_LOCATION}/zipapex"
+  ANDROID_ART_BIN_DIR=f"{DEX_LOCATION}/zipapex/bin"
+elif USE_EXTRACTED_ZIPAPEX == "y":
   # Just symlink the zipapex binaries
-  ANDROID_ART_BIN_DIR=$DEX_LOCATION/zipapex/bin
+  ANDROID_ART_BIN_DIR=f"{DEX_LOCATION}/zipapex/bin"
   # Force since some tests manually run this file twice.
   ln_options=""
-  if [ "$DEV_MODE" = "y" ]; then
+  if DEV_MODE == "y":
     ln_options="--verbose"
-  fi
-  # If the ${RUN} is executed multiple times we don't need to recreate the link
-  installapex_test_cmdline="test -L ${DEX_LOCATION}/zipapex"
-  installapex_cmdline="ln -s -f ${ln_options} ${EXTRACTED_ZIPAPEX_LOC} ${DEX_LOCATION}/zipapex"
-fi
+  # If the {RUN} is executed multiple times we don't need to recreate the link
+  installapex_test_cmdline=f"test -L {DEX_LOCATION}/zipapex"
+  installapex_cmdline=f"ln -s -f {ln_options} {EXTRACTED_ZIPAPEX_LOC} {DEX_LOCATION}/zipapex"
 
 # PROFILE takes precedence over RANDOM_PROFILE, since PROFILE tests require a
 # specific profile to run properly.
-if [ "$PROFILE" = "y" ] || [ "$RANDOM_PROFILE" = "y" ]; then
-  profman_cmdline="$ANDROID_ART_BIN_DIR/profman  \
-    --apk=$DEX_LOCATION/$TEST_NAME.jar \
-    --dex-location=$DEX_LOCATION/$TEST_NAME.jar"
-  if [ -f "$TEST_NAME-ex.jar" ] && [ "$SECONDARY_COMPILATION" = "y" ] ; then
-    profman_cmdline="${profman_cmdline} \
-      --apk=$DEX_LOCATION/$TEST_NAME-ex.jar \
-      --dex-location=$DEX_LOCATION/$TEST_NAME-ex.jar"
-  fi
-  COMPILE_FLAGS="${COMPILE_FLAGS} --profile-file=$DEX_LOCATION/$TEST_NAME.prof"
-  FLAGS="${FLAGS} -Xcompiler-option --profile-file=$DEX_LOCATION/$TEST_NAME.prof"
-  if [ "$PROFILE" = "y" ]; then
-    profman_cmdline="${profman_cmdline} --create-profile-from=$DEX_LOCATION/profile \
-        --reference-profile-file=$DEX_LOCATION/$TEST_NAME.prof"
-  else
-    profman_cmdline="${profman_cmdline} --generate-test-profile=$DEX_LOCATION/$TEST_NAME.prof \
+if PROFILE == "y" or RANDOM_PROFILE == "y":
+  profman_cmdline=f"{ANDROID_ART_BIN_DIR}/profman  \
+    --apk={DEX_LOCATION}/{TEST_NAME}.jar \
+    --dex-location={DEX_LOCATION}/{TEST_NAME}.jar"
+  if isfile(f"{TEST_NAME}-ex.jar") and SECONDARY_COMPILATION == "y":
+    profman_cmdline=f"{profman_cmdline} \
+      --apk={DEX_LOCATION}/{TEST_NAME}-ex.jar \
+      --dex-location={DEX_LOCATION}/{TEST_NAME}-ex.jar"
+  COMPILE_FLAGS=f"{COMPILE_FLAGS} --profile-file={DEX_LOCATION}/{TEST_NAME}.prof"
+  FLAGS=f"{FLAGS} -Xcompiler-option --profile-file={DEX_LOCATION}/{TEST_NAME}.prof"
+  if PROFILE == "y":
+    profman_cmdline=f"{profman_cmdline} --create-profile-from={DEX_LOCATION}/profile \
+        --reference-profile-file={DEX_LOCATION}/{TEST_NAME}.prof"
+  else:
+    profman_cmdline=f"{profman_cmdline} --generate-test-profile={DEX_LOCATION}/{TEST_NAME}.prof \
         --generate-test-profile-seed=0"
-  fi
-fi
 
-function get_prebuilt_lldb_path {
-  local CLANG_BASE="prebuilts/clang/host"
-  local CLANG_VERSION="$("$ANDROID_BUILD_TOP/build/soong/scripts/get_clang_version.py")"
-  case "$(uname -s)" in
-    Darwin)
-      local PREBUILT_NAME="darwin-x86"
-      ;;
-    Linux)
-      local PREBUILT_NAME="linux-x86"
-      ;;
-    *)
-      >&2 echo "Unknown host $(uname -s). Unsupported for debugging dex2oat with LLDB."
+def get_prebuilt_lldb_path():
+  CLANG_BASE="prebuilts/clang/host"
+  CLANG_VERSION=run(f"{ANDROID_BUILD_TOP}/build/soong/scripts/get_clang_version.py").stdout.strip()
+  uname = run("uname -s").stdout.strip()
+  if uname == "Darwin":
+      PREBUILT_NAME="darwin-x86"
+  elif uname == "Linux":
+      PREBUILT_NAME="linux-x86"
+  else:
+      print("Unknown host $(uname -s). Unsupported for debugging dex2oat with LLDB.", file=sys.stderr)
       return
-      ;;
-  esac
-  local CLANG_PREBUILT_HOST_PATH="$ANDROID_BUILD_TOP/$CLANG_BASE/$PREBUILT_NAME/$CLANG_VERSION"
+  CLANG_PREBUILT_HOST_PATH=f"{ANDROID_BUILD_TOP}/{CLANG_BASE}/{PREBUILT_NAME}/{CLANG_VERSION}"
   # If the clang prebuilt directory exists and the reported clang version
   # string does not, then it is likely that the clang version reported by the
   # get_clang_version.py script does not match the expected directory name.
-  if [ -d "${ANDROID_BUILD_TOP}/${CLANG_BASE}/${PREBUILT_NAME}" ] && \
-     [ ! -d "${CLANG_PREBUILT_HOST_PATH}" ]; then
-    error_msg "The prebuilt clang directory exists, but the specific clang"\
+  if (isdir(f"{ANDROID_BUILD_TOP}/{CLANG_BASE}/{PREBUILT_NAME}") and
+      not isdir(CLANG_PREBUILT_HOST_PATH)):
+    error_msg("The prebuilt clang directory exists, but the specific clang"\
     "\nversion reported by get_clang_version.py does not exist in that path."\
     "\nPlease make sure that the reported clang version resides in the"\
-    "\nprebuilt clang directory!"
-    exit 1
-  fi
+    "\nprebuilt clang directory!")
+    sys.exit(1)
 
   # The lldb-server binary is a dependency of lldb.
-  export LLDB_DEBUGSERVER_PATH="${CLANG_PREBUILT_HOST_PATH}/runtimes_ndk_cxx/x86_64/lldb-server"
+  export("LLDB_DEBUGSERVER_PATH", f"{CLANG_PREBUILT_HOST_PATH}/runtimes_ndk_cxx/x86_64/lldb-server")
 
   # Set the current terminfo directory to TERMINFO so that LLDB can read the
   # termcap database.
-  local terminfo_regexp_path='\/.*\/*terminfo\/'
-  if [[ $(infocmp) =~ $terminfo_regexp_path ]] ; then
-    export TERMINFO="${BASH_REMATCH[0]}"
-  fi
+  terminfo = re.search('/.*/terminfo/', run("infocmp", save_cmd=False).stdout)
+  if terminfo:
+    export("TERMINFO", terminfo[0])
 
-  prebuilt_lldb_path="$CLANG_PREBUILT_HOST_PATH/bin/lldb.sh"
-}
+  return f"{CLANG_PREBUILT_HOST_PATH}/bin/lldb.sh"
 
-function write_dex2oat_cmdlines {
-  local name="$1"
+def write_dex2oat_cmdlines(name: str):
+  global dex2oat_cmdline, dm_cmdline, vdex_cmdline
 
-  local class_loader_context=""
-  local enable_app_image=false
-  if [ "$APP_IMAGE" = "y" ]; then
-    enable_app_image=true
-  fi
+  class_loader_context=""
+  enable_app_image=False
+  if APP_IMAGE == "y":
+    enable_app_image=True
 
   # If the name ends in -ex then this is a secondary dex file
-  if [ "${name:${#name}-3}" = "-ex" ]; then
+  if name.endswith("-ex"):
     # Lazily realize the default value in case DEX_LOCATION/TEST_NAME change
-    if [ "x$SECONDARY_CLASS_LOADER_CONTEXT" = "x" ]; then
-      if [ "x$SECONDARY_DEX" = "x" ]; then
+    global SECONDARY_CLASS_LOADER_CONTEXT
+    if SECONDARY_CLASS_LOADER_CONTEXT == "":
+      if SECONDARY_DEX == "":
         # Tests without `--secondary` load the "-ex" jar in a separate PathClassLoader
         # that is a child of the main PathClassLoader. If the class loader is constructed
         # in any other way, the test needs to specify the secondary CLC explicitly.
-        SECONDARY_CLASS_LOADER_CONTEXT="PCL[];PCL[$DEX_LOCATION/$TEST_NAME.jar]"
-      else
+        SECONDARY_CLASS_LOADER_CONTEXT=f"PCL[];PCL[{DEX_LOCATION}/{TEST_NAME}.jar]"
+      else:
         # Tests with `--secondary` load the `-ex` jar a part of the main PathClassLoader.
-        SECONDARY_CLASS_LOADER_CONTEXT="PCL[$DEX_LOCATION/$TEST_NAME.jar]"
-      fi
-    fi
-    class_loader_context="'--class-loader-context=$SECONDARY_CLASS_LOADER_CONTEXT'"
-    $enable_app_image && [ "$SECONDARY_APP_IMAGE" = "y" ] || enable_app_image=false
-  fi
+        SECONDARY_CLASS_LOADER_CONTEXT=f"PCL[{DEX_LOCATION}/{TEST_NAME}.jar]"
+    class_loader_context=f"'--class-loader-context={SECONDARY_CLASS_LOADER_CONTEXT}'"
+    enable_app_image = enable_app_image and SECONDARY_APP_IMAGE == "y"
 
-  local app_image=""
-  $enable_app_image && app_image="--app-image-file=$DEX_LOCATION/oat/$ISA/$name.art --resolve-startup-const-strings=true"
+  app_image=""
+  if enable_app_image:
+    app_image=f"--app-image-file={DEX_LOCATION}/oat/{ISA}/{name}.art --resolve-startup-const-strings=true"
 
-  if [ "$USE_GDB_DEX2OAT" = "y" ]; then
-    get_prebuilt_lldb_path
-    GDB_DEX2OAT="$prebuilt_lldb_path -f"
+  global GDB_DEX2OAT, GDB_DEX2OAT_ARGS
+  if USE_GDB_DEX2OAT == "y":
+    prebuilt_lldb_path=get_prebuilt_lldb_path()
+    GDB_DEX2OAT=f"{prebuilt_lldb_path} -f"
     GDB_DEX2OAT_ARGS+=" -- "
-  fi
 
-  local dex2oat_binary
-  dex2oat_binary=${DEX2OAT_DEBUG_BINARY}
-  if  [[ "$TEST_IS_NDEBUG" = "y" ]]; then
-    dex2oat_binary=${DEX2OAT_NDEBUG_BINARY}
-  fi
-  dex2oat_cmdline="$INVOKE_WITH $GDB_DEX2OAT \
-                      $ANDROID_ART_BIN_DIR/$dex2oat_binary \
-                      $GDB_DEX2OAT_ARGS \
-                      $COMPILE_FLAGS \
-                      --boot-image=${BOOT_IMAGE} \
-                      --dex-file=$DEX_LOCATION/$name.jar \
-                      --oat-file=$DEX_LOCATION/oat/$ISA/$name.odex \
-                      "$app_image" \
+  dex2oat_binary=DEX2OAT_DEBUG_BINARY
+  if TEST_IS_NDEBUG == "y":
+    dex2oat_binary=DEX2OAT_NDEBUG_BINARY
+  dex2oat_cmdline=f"{INVOKE_WITH} {GDB_DEX2OAT} \
+                      {ANDROID_ART_BIN_DIR}/{dex2oat_binary} \
+                      {GDB_DEX2OAT_ARGS} \
+                      {COMPILE_FLAGS} \
+                      --boot-image={BOOT_IMAGE} \
+                      --dex-file={DEX_LOCATION}/{name}.jar \
+                      --oat-file={DEX_LOCATION}/oat/{ISA}/{name}.odex \
+                      {app_image} \
                       --generate-mini-debug-info \
-                      --instruction-set=$ISA \
-                      $class_loader_context"
-  if [ "x$INSTRUCTION_SET_FEATURES" != "x" ] ; then
-    dex2oat_cmdline="${dex2oat_cmdline} --instruction-set-features=${INSTRUCTION_SET_FEATURES}"
-  fi
+                      --instruction-set={ISA} \
+                      {class_loader_context}"
+  if INSTRUCTION_SET_FEATURES != "":
+    dex2oat_cmdline+=f" --instruction-set-features={INSTRUCTION_SET_FEATURES}"
 
   # Add in a timeout. This is important for testing the compilation/verification time of
   # pathological cases. We do not append a timeout when debugging dex2oat because we
@@ -1061,262 +1056,231 @@
   #       now. We should try to improve this.
   #       The current value is rather arbitrary. run-tests should compile quickly.
   # Watchdog timeout is in milliseconds so add 3 '0's to the dex2oat timeout.
-  if [ "$HOST" != "n" ] && [ "$USE_GDB_DEX2OAT" != "y" ]; then
+  if HOST != "n" and USE_GDB_DEX2OAT != "y":
     # Use SIGRTMIN+2 to try to dump threads.
     # Use -k 1m to SIGKILL it a minute later if it hasn't ended.
-    dex2oat_cmdline="timeout -k ${DEX2OAT_TIMEOUT}s -s SIGRTMIN+2 ${DEX2OAT_RT_TIMEOUT}s ${dex2oat_cmdline} --watchdog-timeout=${DEX2OAT_TIMEOUT}000"
-  fi
-  if [ "$PROFILE" = "y" ] || [ "$RANDOM_PROFILE" = "y" ]; then
-    vdex_cmdline="${dex2oat_cmdline} ${VDEX_ARGS} --input-vdex=$DEX_LOCATION/oat/$ISA/$name.vdex --output-vdex=$DEX_LOCATION/oat/$ISA/$name.vdex"
-  elif [ "$TEST_VDEX" = "y" ]; then
-    if [ "$VDEX_ARGS" = "" ]; then
+    dex2oat_cmdline=f"timeout -k {DEX2OAT_TIMEOUT}s -s SIGRTMIN+2 {DEX2OAT_RT_TIMEOUT}s {dex2oat_cmdline} --watchdog-timeout={DEX2OAT_TIMEOUT}000"
+  if PROFILE == "y" or RANDOM_PROFILE == "y":
+    vdex_cmdline=f"{dex2oat_cmdline} {VDEX_ARGS} --input-vdex={DEX_LOCATION}/oat/{ISA}/{name}.vdex --output-vdex={DEX_LOCATION}/oat/{ISA}/{name}.vdex"
+  elif TEST_VDEX == "y":
+    if VDEX_ARGS == "":
       # If no arguments need to be passed, just delete the odex file so that the runtime only picks up the vdex file.
-      vdex_cmdline="rm $DEX_LOCATION/oat/$ISA/$name.odex"
-    else
-      vdex_cmdline="${dex2oat_cmdline} ${VDEX_ARGS} --input-vdex=$DEX_LOCATION/oat/$ISA/$name.vdex"
-    fi
-  elif [ "$TEST_DEX2OAT_DM" = "y" ]; then
-    vdex_cmdline="${dex2oat_cmdline} ${VDEX_ARGS} --dump-timings --dm-file=$DEX_LOCATION/oat/$ISA/$name.dm"
-    dex2oat_cmdline="${dex2oat_cmdline} --copy-dex-files=false --output-vdex=$DEX_LOCATION/oat/$ISA/primary.vdex"
-    dm_cmdline="zip -qj $DEX_LOCATION/oat/$ISA/$name.dm $DEX_LOCATION/oat/$ISA/primary.vdex"
-  elif [ "$TEST_RUNTIME_DM" = "y" ]; then
-    dex2oat_cmdline="${dex2oat_cmdline} --copy-dex-files=false --output-vdex=$DEX_LOCATION/oat/$ISA/primary.vdex"
-    dm_cmdline="zip -qj $DEX_LOCATION/$name.dm $DEX_LOCATION/oat/$ISA/primary.vdex"
-  fi
-}
+      vdex_cmdline=f"rm {DEX_LOCATION}/oat/{ISA}/{name}.odex"
+    else:
+      vdex_cmdline=f"{dex2oat_cmdline} {VDEX_ARGS} --input-vdex={DEX_LOCATION}/oat/{ISA}/{name}.vdex"
+  elif TEST_DEX2OAT_DM == "y":
+    vdex_cmdline=f"{dex2oat_cmdline} {VDEX_ARGS} --dump-timings --dm-file={DEX_LOCATION}/oat/{ISA}/{name}.dm"
+    dex2oat_cmdline=f"{dex2oat_cmdline} --copy-dex-files=false --output-vdex={DEX_LOCATION}/oat/{ISA}/primary.vdex"
+    dm_cmdline=f"zip -qj {DEX_LOCATION}/oat/{ISA}/{name}.dm {DEX_LOCATION}/oat/{ISA}/primary.vdex"
+  elif TEST_RUNTIME_DM == "y":
+    dex2oat_cmdline=f"{dex2oat_cmdline} --copy-dex-files=false --output-vdex={DEX_LOCATION}/oat/{ISA}/primary.vdex"
+    dm_cmdline=f"zip -qj {DEX_LOCATION}/{name}.dm {DEX_LOCATION}/oat/{ISA}/primary.vdex"
 
 # Enable mini-debug-info for JIT (if JIT is used).
-FLAGS="$FLAGS -Xcompiler-option --generate-mini-debug-info"
+FLAGS+=" -Xcompiler-option --generate-mini-debug-info"
 
-if [ "$PREBUILD" = "y" ]; then
-  mkdir_locations="${mkdir_locations} ${DEX_LOCATION}/oat/$ISA"
+if PREBUILD == "y":
+  mkdir_locations+=f" {DEX_LOCATION}/oat/{ISA}"
 
   # "Primary".
-  write_dex2oat_cmdlines "$TEST_NAME"
-  dex2oat_cmdline=$(echo $dex2oat_cmdline)
-  dm_cmdline=$(echo $dm_cmdline)
-  vdex_cmdline=$(echo $vdex_cmdline)
+  write_dex2oat_cmdlines(TEST_NAME)
+  dex2oat_cmdline=re.sub(" +", " ", dex2oat_cmdline)
+  dm_cmdline=re.sub(" +", " ", dm_cmdline)
+  vdex_cmdline=re.sub(" +", " ", vdex_cmdline)
 
   # Enable mini-debug-info for JIT (if JIT is used).
-  FLAGS="$FLAGS -Xcompiler-option --generate-mini-debug-info"
+  FLAGS+=" -Xcompiler-option --generate-mini-debug-info"
 
-  if [ -f "$TEST_NAME-ex.jar" ] && [ "$SECONDARY_COMPILATION" = "y" ] ; then
+  if isfile(f"{TEST_NAME}-ex.jar") and SECONDARY_COMPILATION == "y":
     # "Secondary" for test coverage.
 
     # Store primary values.
-    base_dex2oat_cmdline="$dex2oat_cmdline"
-    base_dm_cmdline="$dm_cmdline"
-    base_vdex_cmdline="$vdex_cmdline"
+    base_dex2oat_cmdline=dex2oat_cmdline
+    base_dm_cmdline=dm_cmdline
+    base_vdex_cmdline=vdex_cmdline
 
-    write_dex2oat_cmdlines "$TEST_NAME-ex"
-    dex2oat_cmdline=$(echo $dex2oat_cmdline)
-    dm_cmdline=$(echo $dm_cmdline)
-    vdex_cmdline=$(echo $vdex_cmdline)
+    write_dex2oat_cmdlines(f"{TEST_NAME}-ex")
+    dex2oat_cmdline=re.sub(" +", " ", dex2oat_cmdline)
+    dm_cmdline=re.sub(" +", " ", dm_cmdline)
+    vdex_cmdline=re.sub(" +", " ", vdex_cmdline)
 
     # Concatenate.
-    dex2oat_cmdline="$base_dex2oat_cmdline && $dex2oat_cmdline"
-    dm_cmdline="$base_dm_cmdline" # Only use primary dm.
-    vdex_cmdline="$base_vdex_cmdline && $vdex_cmdline"
-  fi
-fi
+    dex2oat_cmdline=f"{base_dex2oat_cmdline} && {dex2oat_cmdline}"
+    dm_cmdline=base_dm_cmdline # Only use primary dm.
+    vdex_cmdline=f"{base_vdex_cmdline} && {vdex_cmdline}"
 
-if [ "$SYNC_BEFORE_RUN" = "y" ]; then
+if SYNC_BEFORE_RUN == "y":
   sync_cmdline="sync"
-fi
 
 DALVIKVM_ISA_FEATURES_ARGS=""
-if [ "x$INSTRUCTION_SET_FEATURES" != "x" ] ; then
-  DALVIKVM_ISA_FEATURES_ARGS="-Xcompiler-option --instruction-set-features=${INSTRUCTION_SET_FEATURES}"
-fi
+if INSTRUCTION_SET_FEATURES != "":
+  DALVIKVM_ISA_FEATURES_ARGS=f"-Xcompiler-option --instruction-set-features={INSTRUCTION_SET_FEATURES}"
 
 # java.io.tmpdir can only be set at launch time.
 TMP_DIR_OPTION=""
-if [ "$HOST" = "n" ]; then
+if HOST == "n":
   TMP_DIR_OPTION="-Djava.io.tmpdir=/data/local/tmp"
-fi
 
 # The build servers have an ancient version of bash so we cannot use @Q.
-if [ "$USE_GDBSERVER" == "y" ]; then
-  printf -v QUOTED_DALVIKVM_BOOT_OPT "%q" "$DALVIKVM_BOOT_OPT"
-else
-  QUOTED_DALVIKVM_BOOT_OPT="$DALVIKVM_BOOT_OPT"
-fi
+QUOTED_DALVIKVM_BOOT_OPT=shlex.quote(DALVIKVM_BOOT_OPT)
 
-DALVIKVM_CLASSPATH=$DEX_LOCATION/$TEST_NAME.jar
-if [ -f "$TEST_NAME-aotex.jar" ] ; then
-  DALVIKVM_CLASSPATH=$DALVIKVM_CLASSPATH:$DEX_LOCATION/$TEST_NAME-aotex.jar
-fi
-DALVIKVM_CLASSPATH=$DALVIKVM_CLASSPATH$SECONDARY_DEX
+DALVIKVM_CLASSPATH=f"{DEX_LOCATION}/{TEST_NAME}.jar"
+if isfile(f"{TEST_NAME}-aotex.jar"):
+  DALVIKVM_CLASSPATH=f"{DALVIKVM_CLASSPATH}:{DEX_LOCATION}/{TEST_NAME}-aotex.jar"
+DALVIKVM_CLASSPATH=f"{DALVIKVM_CLASSPATH}{SECONDARY_DEX}"
 
 # We set DumpNativeStackOnSigQuit to false to avoid stressing libunwind.
 # b/27185632
 # b/24664297
 
-dalvikvm_cmdline="$INVOKE_WITH $GDB $ANDROID_ART_BIN_DIR/$DALVIKVM \
-                  $GDB_ARGS \
-                  $FLAGS \
-                  $DEX_VERIFY \
-                  -XXlib:$LIB \
-                  $DEX2OAT \
-                  $DALVIKVM_ISA_FEATURES_ARGS \
-                  $ZYGOTE \
-                  $JNI_OPTS \
-                  $INT_OPTS \
-                  $DEBUGGER_OPTS \
-                  ${QUOTED_DALVIKVM_BOOT_OPT} \
-                  $TMP_DIR_OPTION \
+dalvikvm_cmdline=f"{INVOKE_WITH} {GDB} {ANDROID_ART_BIN_DIR}/{DALVIKVM} \
+                  {GDB_ARGS} \
+                  {FLAGS} \
+                  {DEX_VERIFY} \
+                  -XXlib:{LIB} \
+                  {DEX2OAT} \
+                  {DALVIKVM_ISA_FEATURES_ARGS} \
+                  {ZYGOTE} \
+                  {JNI_OPTS} \
+                  {INT_OPTS} \
+                  {DEBUGGER_OPTS} \
+                  {QUOTED_DALVIKVM_BOOT_OPT} \
+                  {TMP_DIR_OPTION} \
                   -XX:DumpNativeStackOnSigQuit:false \
-                  -cp $DALVIKVM_CLASSPATH $MAIN $ARGS"
+                  -cp {DALVIKVM_CLASSPATH} {MAIN} {ARGS}"
 
-if [ "x$SIMPLEPERF" == xyes ]; then
-  dalvikvm_cmdline="simpleperf record ${dalvikvm_cmdline} && simpleperf report"
-fi
+if SIMPLEPERF == "yes":
+  dalvikvm_cmdline=f"simpleperf record {dalvikvm_cmdline} && simpleperf report"
 
-sanitize_dex2oat_cmdline() {
-  local args=()
-  for arg in "$@"; do
-    if [ "$arg" = "--class-loader-context=&" ]; then
+def sanitize_dex2oat_cmdline(cmdline: str) -> str:
+  args = []
+  for arg in cmdline.split(" "):
+    if arg == "--class-loader-context=&":
       arg="--class-loader-context=\&"
-    fi
-    args+=("$arg")
-  done
-  echo -n "${args[@]}"
-}
+    args.append(arg)
+  return " ".join(args)
 
 # Remove whitespace.
-dex2oat_cmdline=$(sanitize_dex2oat_cmdline $(echo $dex2oat_cmdline))
-dalvikvm_cmdline=$(echo $dalvikvm_cmdline)
-dm_cmdline=$(echo $dm_cmdline)
-vdex_cmdline=$(sanitize_dex2oat_cmdline $(echo $vdex_cmdline))
-profman_cmdline=$(echo $profman_cmdline)
+dex2oat_cmdline=sanitize_dex2oat_cmdline(dex2oat_cmdline)
+dalvikvm_cmdline=re.sub(" +", " ", dalvikvm_cmdline)
+dm_cmdline=re.sub(" +", " ", dm_cmdline)
+vdex_cmdline=sanitize_dex2oat_cmdline(vdex_cmdline)
+profman_cmdline=re.sub(" +", " ", profman_cmdline)
 
 # Use an empty ASAN_OPTIONS to enable defaults.
 # Note: this is required as envsetup right now exports detect_leaks=0.
 RUN_TEST_ASAN_OPTIONS=""
 
 # Multiple shutdown leaks. b/38341789
-if [ "x$RUN_TEST_ASAN_OPTIONS" != "x" ] ; then
-  RUN_TEST_ASAN_OPTIONS="${RUN_TEST_ASAN_OPTIONS}:"
-fi
-RUN_TEST_ASAN_OPTIONS="${RUN_TEST_ASAN_OPTIONS}detect_leaks=0"
+if RUN_TEST_ASAN_OPTIONS != "":
+  RUN_TEST_ASAN_OPTIONS=f"{RUN_TEST_ASAN_OPTIONS}:"
+RUN_TEST_ASAN_OPTIONS=f"{RUN_TEST_ASAN_OPTIONS}detect_leaks=0"
 
 # For running, we must turn off logging when dex2oat is missing. Otherwise we use
 # the same defaults as for prebuilt: everything when --dev, otherwise errors and above only.
-if [ "$EXTERNAL_LOG_TAGS" = "n" ]; then
-  if [ "$DEV_MODE" = "y" ]; then
-      export ANDROID_LOG_TAGS='*:d'
-  elif [ "$HAVE_IMAGE" = "n" ]; then
+if EXTERNAL_LOG_TAGS == "n":
+  if DEV_MODE == "y":
+      export("ANDROID_LOG_TAGS", '*:d')
+  elif HAVE_IMAGE == "n":
       # All tests would log the error of missing image. Be silent here and only log fatal
       # events.
-      export ANDROID_LOG_TAGS='*:s'
-  else
+      export("ANDROID_LOG_TAGS", '*:s')
+  else:
       # We are interested in LOG(ERROR) output.
-      export ANDROID_LOG_TAGS='*:e'
-  fi
-fi
+      export("ANDROID_LOG_TAGS", '*:e')
 
-if [ "$HOST" = "n" ]; then
-    adb root > /dev/null
-    adb wait-for-device
-    if [ "$QUIET" = "n" ]; then
-      adb shell rm -rf $CHROOT_DEX_LOCATION
-      adb shell mkdir -p $CHROOT_DEX_LOCATION
-      adb push $TEST_NAME.jar $CHROOT_DEX_LOCATION
-      adb push $TEST_NAME-ex.jar $CHROOT_DEX_LOCATION
-      adb push $TEST_NAME-aotex.jar $CHROOT_DEX_LOCATION
-      adb push $TEST_NAME-bcpex.jar $CHROOT_DEX_LOCATION
-      if [ "$PROFILE" = "y" ] || [ "$RANDOM_PROFILE" = "y" ]; then
-        adb push profile $CHROOT_DEX_LOCATION
-      fi
+if HOST == "n":
+    adb.root()
+    adb.wait_for_device()
+    if QUIET == "n":
+      adb.shell(f"rm -rf {CHROOT_DEX_LOCATION}", capture_output=False)
+      adb.shell(f"mkdir -p {CHROOT_DEX_LOCATION}", capture_output=False)
+      adb.push(f"{TEST_NAME}.jar", CHROOT_DEX_LOCATION, capture_output=False)
+      adb.push(f"{TEST_NAME}-ex.jar", CHROOT_DEX_LOCATION, check=False, capture_output=False)
+      adb.push(f"{TEST_NAME}-aotex.jar", CHROOT_DEX_LOCATION, check=False, capture_output=False)
+      adb.push(f"{TEST_NAME}-bcpex.jar", CHROOT_DEX_LOCATION, check=False, capture_output=False)
+      if PROFILE == "y" or RANDOM_PROFILE == "y":
+        adb.push("profile", CHROOT_DEX_LOCATION, check=False, capture_output=False)
       # Copy resource folder
-      if [ -d res ]; then
-        adb push res $CHROOT_DEX_LOCATION
-      fi
-    else
-      adb shell rm -rf $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      adb shell mkdir -p $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      adb push $TEST_NAME.jar $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      adb push $TEST_NAME-ex.jar $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      adb push $TEST_NAME-aotex.jar $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      adb push $TEST_NAME-bcpex.jar $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      if [ "$PROFILE" = "y" ] || [ "$RANDOM_PROFILE" = "y" ]; then
-        adb push profile $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      fi
+      if isdir("res"):
+        adb.push("res", CHROOT_DEX_LOCATION, capture_output=False)
+    else:
+      adb.shell(f"rm -rf {CHROOT_DEX_LOCATION}")
+      adb.shell(f"mkdir -p {CHROOT_DEX_LOCATION}")
+      adb.push(f"{TEST_NAME}.jar", CHROOT_DEX_LOCATION)
+      adb.push(f"{TEST_NAME}-ex.jar", CHROOT_DEX_LOCATION, check=False)
+      adb.push(f"{TEST_NAME}-aotex.jar", CHROOT_DEX_LOCATION, check=False)
+      adb.push(f"{TEST_NAME}-bcpex.jar", CHROOT_DEX_LOCATION, check=False)
+      if PROFILE == "y" or RANDOM_PROFILE == "y":
+        adb.push("profile", CHROOT_DEX_LOCATION, check=False)
       # Copy resource folder
-      if [ -d res ]; then
-        adb push res $CHROOT_DEX_LOCATION >/dev/null 2>&1
-      fi
-    fi
+      if isdir("res"):
+        adb.push("res", CHROOT_DEX_LOCATION)
 
     # Populate LD_LIBRARY_PATH.
-    LD_LIBRARY_PATH=
-    if [ "$ANDROID_ROOT" != "/system" ]; then
+    LD_LIBRARY_PATH=""
+    if ANDROID_ROOT != "/system":
       # Current default installation is dalvikvm 64bits and dex2oat 32bits,
       # so we can only use LD_LIBRARY_PATH when testing on a local
       # installation.
-      LD_LIBRARY_PATH="$ANDROID_ROOT/$LIBRARY_DIRECTORY"
-    fi
+      LD_LIBRARY_PATH=f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}"
 
     # This adds libarttest(d).so to the default linker namespace when dalvikvm
     # is run from /apex/com.android.art/bin. Since that namespace is essentially
     # an alias for the com_android_art namespace, that gives libarttest(d).so
     # full access to the internal ART libraries.
-    LD_LIBRARY_PATH="/data/$TEST_DIRECTORY/com.android.art/lib${SUFFIX64}:$LD_LIBRARY_PATH"
-    if [ "$TEST_IS_NDEBUG" = "y" ]; then dlib=""; else dlib="d"; fi
-    art_test_internal_libraries=(
-      libartagent${dlib}.so
-      libarttest${dlib}.so
-      libtiagent${dlib}.so
-      libtistress${dlib}.so
-    )
-    art_test_internal_libraries="${art_test_internal_libraries[*]}"
-    NATIVELOADER_DEFAULT_NAMESPACE_LIBS="${art_test_internal_libraries// /:}"
-    dlib=
-    art_test_internal_libraries=
+    LD_LIBRARY_PATH=f"/data/{TEST_DIRECTORY}/com.android.art/lib{SUFFIX64}:{LD_LIBRARY_PATH}"
+    dlib=("" if TEST_IS_NDEBUG == "y" else "d")
+    art_test_internal_libraries=[
+      f"libartagent{dlib}.so",
+      f"libarttest{dlib}.so",
+      f"libtiagent{dlib}.so",
+      f"libtistress{dlib}.so",
+    ]
+    NATIVELOADER_DEFAULT_NAMESPACE_LIBS=":".join(art_test_internal_libraries)
+    dlib=""
+    art_test_internal_libraries=[]
 
     # Needed to access the test's Odex files.
-    LD_LIBRARY_PATH="$DEX_LOCATION/oat/$ISA:$LD_LIBRARY_PATH"
+    LD_LIBRARY_PATH=f"{DEX_LOCATION}/oat/{ISA}:{LD_LIBRARY_PATH}"
     # Needed to access the test's native libraries (see e.g. 674-hiddenapi,
-    # which generates `libhiddenapitest_*.so` libraries in `$DEX_LOCATION`).
-    LD_LIBRARY_PATH="$DEX_LOCATION:$LD_LIBRARY_PATH"
+    # which generates `libhiddenapitest_*.so` libraries in `{DEX_LOCATION}`).
+    LD_LIBRARY_PATH=f"{DEX_LOCATION}:{LD_LIBRARY_PATH}"
 
     # Prepend directories to the path on device.
-    PREPEND_TARGET_PATH=$ANDROID_ART_BIN_DIR
-    if [ "$ANDROID_ROOT" != "/system" ]; then
-      PREPEND_TARGET_PATH="$PREPEND_TARGET_PATH:$ANDROID_ROOT/bin"
-    fi
+    PREPEND_TARGET_PATH=ANDROID_ART_BIN_DIR
+    if ANDROID_ROOT != "/system":
+      PREPEND_TARGET_PATH=f"{PREPEND_TARGET_PATH}:{ANDROID_ROOT}/bin"
 
-    timeout_dumper_cmd=
+    timeout_dumper_cmd=""
 
     # Check whether signal_dumper is available.
-    if [ "$TIMEOUT_DUMPER" = signal_dumper ] ; then
+    if TIMEOUT_DUMPER == "signal_dumper":
       # Chroot? Use as prefix for tests.
-      TIMEOUT_DUMPER_PATH_PREFIX=
-      if [ -n "$CHROOT" ]; then
-        TIMEOUT_DUMPER_PATH_PREFIX="$CHROOT/"
-      fi
+      TIMEOUT_DUMPER_PATH_PREFIX=""
+      if CHROOT:
+        TIMEOUT_DUMPER_PATH_PREFIX=f"{CHROOT}/"
 
       # Testing APEX?
-      if adb shell "test -x ${TIMEOUT_DUMPER_PATH_PREFIX}/apex/com.android.art/bin/signal_dumper" ; then
+      if adb.shell(f"test -x {TIMEOUT_DUMPER_PATH_PREFIX}/apex/com.android.art/bin/signal_dumper",
+                   check=False, save_cmd=False).returncode:
         TIMEOUT_DUMPER="/apex/com.android.art/bin/signal_dumper"
       # Is it in /system/bin?
-      elif adb shell "test -x ${TIMEOUT_DUMPER_PATH_PREFIX}/system/bin/signal_dumper" ; then
+      elif adb.shell(f"test -x {TIMEOUT_DUMPER_PATH_PREFIX}/system/bin/signal_dumper",
+                     check=False, save_cmd=False).returncode:
         TIMEOUT_DUMPER="/system/bin/signal_dumper"
-      else
-        TIMEOUT_DUMPER=
-      fi
-    else
-      TIMEOUT_DUMPER=
-    fi
+      else:
+        TIMEOUT_DUMPER=""
+    else:
+      TIMEOUT_DUMPER=""
 
-    if [ ! -z "$TIMEOUT_DUMPER" ] ; then
+    if TIMEOUT_DUMPER:
       # Use "-l" to dump to logcat. That is convenience for the build bot crash symbolization.
       # Use exit code 124 for toybox timeout (b/141007616).
-      timeout_dumper_cmd="${TIMEOUT_DUMPER} -l -s 15 -e 124"
-    fi
+      timeout_dumper_cmd=f"{TIMEOUT_DUMPER} -l -s 15 -e 124"
 
-    timeout_prefix=
-    if [ "$TIME_OUT" = "timeout" ]; then
+    timeout_prefix=""
+    if TIME_OUT == "timeout":
       # Add timeout command if time out is desired.
       #
       # Note: We first send SIGTERM (the timeout default, signal 15) to the signal dumper, which
@@ -1324,106 +1288,100 @@
       #       dumping do not lead to a deadlock, we also use the "-k" option to definitely kill the
       #       child.
       # Note: Using "--foreground" to not propagate the signal to children, i.e., the runtime.
-      timeout_prefix="timeout --foreground -k 120s ${TIME_OUT_VALUE}s ${timeout_dumper_cmd} $cmdline"
-    fi
+      timeout_prefix=f"timeout --foreground -k 120s {TIME_OUT_VALUE}s {timeout_dumper_cmd} {cmdline}"
 
     # Create a script with the command. The command can get longer than the longest
     # allowed adb command and there is no way to get the exit status from a adb shell
     # command. Dalvik cache is cleaned before running to make subsequent executions
     # of the script follow the same runtime path.
-    cmdline="cd $DEX_LOCATION && \
-             export ASAN_OPTIONS=$RUN_TEST_ASAN_OPTIONS && \
-             export ANDROID_DATA=$DEX_LOCATION && \
-             export DEX_LOCATION=$DEX_LOCATION && \
-             export ANDROID_ROOT=$ANDROID_ROOT && \
-             export ANDROID_I18N_ROOT=$ANDROID_I18N_ROOT && \
-             export ANDROID_ART_ROOT=$ANDROID_ART_ROOT && \
-             export ANDROID_TZDATA_ROOT=$ANDROID_TZDATA_ROOT && \
-             export ANDROID_LOG_TAGS=$ANDROID_LOG_TAGS && \
-             rm -rf ${DEX_LOCATION}/dalvik-cache/ && \
-             mkdir -p ${mkdir_locations} && \
-             export LD_LIBRARY_PATH=$LD_LIBRARY_PATH && \
-             export NATIVELOADER_DEFAULT_NAMESPACE_LIBS=$NATIVELOADER_DEFAULT_NAMESPACE_LIBS && \
-             export PATH=$PREPEND_TARGET_PATH:\$PATH && \
-             $profman_cmdline && \
-             $dex2oat_cmdline && \
-             $dm_cmdline && \
-             $vdex_cmdline && \
-             $strip_cmdline && \
-             $sync_cmdline && \
-             $timeout_prefix $dalvikvm_cmdline"
+    cmdline=f"cd {DEX_LOCATION} && \
+             export ASAN_OPTIONS={RUN_TEST_ASAN_OPTIONS} && \
+             export ANDROID_DATA={DEX_LOCATION} && \
+             export DEX_LOCATION={DEX_LOCATION} && \
+             export ANDROID_ROOT={ANDROID_ROOT} && \
+             export ANDROID_I18N_ROOT={ANDROID_I18N_ROOT} && \
+             export ANDROID_ART_ROOT={ANDROID_ART_ROOT} && \
+             export ANDROID_TZDATA_ROOT={ANDROID_TZDATA_ROOT} && \
+             export ANDROID_LOG_TAGS={ANDROID_LOG_TAGS} && \
+             rm -rf {DEX_LOCATION}/dalvik-cache/ && \
+             mkdir -p {mkdir_locations} && \
+             export LD_LIBRARY_PATH={LD_LIBRARY_PATH} && \
+             export NATIVELOADER_DEFAULT_NAMESPACE_LIBS={NATIVELOADER_DEFAULT_NAMESPACE_LIBS} && \
+             export PATH={PREPEND_TARGET_PATH}:$PATH && \
+             {profman_cmdline} && \
+             {dex2oat_cmdline} && \
+             {dm_cmdline} && \
+             {vdex_cmdline} && \
+             {strip_cmdline} && \
+             {sync_cmdline} && \
+             {timeout_prefix} {dalvikvm_cmdline}"
 
-    cmdfile=$(mktemp cmd-XXXX --suffix "-$TEST_NAME")
-    echo "$cmdline" >> $cmdfile
+    cmdfile=run(f'mktemp cmd-XXXX --suffix "-{TEST_NAME}"', save_cmd=False).stdout.strip()
+    with open(cmdfile, "w") as f:
+      f.write(cmdline)
 
-    if [ "$DEV_MODE" = "y" ]; then
-      echo $cmdline
-      if [ "$USE_GDB" = "y" ] || [ "$USE_GDBSERVER" = "y" ]; then
-        echo "Forward ${GDBSERVER_PORT} to local port and connect GDB"
-      fi
-    fi
+    run('echo cmdline.sh "' + cmdline.replace('"', '\\"') + '"')
 
-    if [ "$QUIET" = "n" ]; then
-      adb push $cmdfile $CHROOT_DEX_LOCATION/cmdline.sh
-    else
-      adb push $cmdfile $CHROOT_DEX_LOCATION/cmdline.sh >/dev/null 2>&1
-    fi
+    if DEV_MODE == "y":
+      print(cmdline)
+      if USE_GDB == "y" or USE_GDBSERVER == "y":
+        print(f"Forward {GDBSERVER_PORT} to local port and connect GDB")
+
+    if QUIET == "n":
+      adb.push(cmdfile, f"{CHROOT_DEX_LOCATION}/cmdline.sh", save_cmd=False, capture_output=False)
+    else:
+      adb.push(cmdfile, f"{CHROOT_DEX_LOCATION}/cmdline.sh", save_cmd=False)
 
     exit_status=0
-    if [ "$DRY_RUN" != "y" ]; then
-      if [ -n "$CHROOT" ]; then
-        adb shell chroot "$CHROOT" sh $DEX_LOCATION/cmdline.sh
-      else
-        adb shell sh $DEX_LOCATION/cmdline.sh
-      fi
-      exit_status=$?
-    fi
+    if DRY_RUN != "y":
+      if CHROOT:
+        exit_status=adb.shell(f"chroot {CHROOT} sh {DEX_LOCATION}/cmdline.sh",
+                              check=False, capture_output=False).returncode
+      else:
+        exit_status=adb.shell(f"sh {DEX_LOCATION}/cmdline.sh",
+                              check=False ,capture_output=False).returncode
 
-    rm -f $cmdfile
-    exit $exit_status
-else
+    run(f'rm -f {cmdfile}', save_cmd=False)
+    sys.exit(exit_status)
+else:
     # Host run.
-    export ANDROID_PRINTF_LOG=brief
+    export("ANDROID_PRINTF_LOG", "brief")
 
-    export ANDROID_DATA="$DEX_LOCATION"
-    export ANDROID_ROOT="${ANDROID_ROOT}"
-    export ANDROID_I18N_ROOT="${ANDROID_I18N_ROOT}"
-    export ANDROID_ART_ROOT="${ANDROID_ART_ROOT}"
-    export ANDROID_TZDATA_ROOT="${ANDROID_TZDATA_ROOT}"
-    if [ "$USE_ZIPAPEX" = "y" ] || [ "$USE_EXRACTED_ZIPAPEX" = "y" ]; then
+    export("ANDROID_DATA", DEX_LOCATION)
+    export("ANDROID_ROOT", ANDROID_ROOT)
+    export("ANDROID_I18N_ROOT", ANDROID_I18N_ROOT)
+    export("ANDROID_ART_ROOT", ANDROID_ART_ROOT)
+    export("ANDROID_TZDATA_ROOT", ANDROID_TZDATA_ROOT)
+    if USE_ZIPAPEX == "y" or USE_EXRACTED_ZIPAPEX == "y":
       # Put the zipapex files in front of the ld-library-path
-      export LD_LIBRARY_PATH="${ANDROID_DATA}/zipapex/${LIBRARY_DIRECTORY}:${ANDROID_ROOT}/${TEST_DIRECTORY}"
-      export DYLD_LIBRARY_PATH="${ANDROID_DATA}/zipapex/${LIBRARY_DIRECTORY}:${ANDROID_ROOT}/${TEST_DIRECTORY}"
-    else
-      export LD_LIBRARY_PATH="${ANDROID_ROOT}/${LIBRARY_DIRECTORY}:${ANDROID_ROOT}/${TEST_DIRECTORY}"
-      export DYLD_LIBRARY_PATH="${ANDROID_ROOT}/${LIBRARY_DIRECTORY}:${ANDROID_ROOT}/${TEST_DIRECTORY}"
-    fi
-    export PATH="$PATH:$ANDROID_ART_BIN_DIR"
+      export("LD_LIBRARY_PATH", f"{ANDROID_DATA}/zipapex/{LIBRARY_DIRECTORY}:{ANDROID_ROOT}/{TEST_DIRECTORY}")
+      export("DYLD_LIBRARY_PATH", f"{ANDROID_DATA}/zipapex/{LIBRARY_DIRECTORY}:{ANDROID_ROOT}/{TEST_DIRECTORY}")
+    else:
+      export("LD_LIBRARY_PATH", f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}:{ANDROID_ROOT}/{TEST_DIRECTORY}")
+      export("DYLD_LIBRARY_PATH", f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}:{ANDROID_ROOT}/{TEST_DIRECTORY}")
+    export("PATH", f"{PATH}:{ANDROID_ART_BIN_DIR}")
 
     # Temporarily disable address space layout randomization (ASLR).
     # This is needed on the host so that the linker loads core.oat at the necessary address.
-    export LD_USE_LOAD_BIAS=1
+    export("LD_USE_LOAD_BIAS", "1")
 
-    cmdline="$dalvikvm_cmdline"
+    cmdline=dalvikvm_cmdline
 
-    if [ "$TIME_OUT" = "gdb" ]; then
-      if [ `uname` = "Darwin" ]; then
+    if TIME_OUT == "gdb":
+      if run("uname").stdout.strip() == "Darwin":
         # Fall back to timeout on Mac.
         TIME_OUT="timeout"
-      elif [ "$ISA" = "x86" ]; then
+      elif ISA == "x86":
         # prctl call may fail in 32-bit on an older (3.2) 64-bit Linux kernel. Fall back to timeout.
         TIME_OUT="timeout"
-      else
+      else:
         # Check if gdb is available.
-        gdb --eval-command="quit" > /dev/null 2>&1
-        if [ $? != 0 ]; then
+        proc = run('gdb --eval-command="quit"', check=False, save_cmd=False, capture_output=True)
+        if proc.returncode != 0:
           # gdb isn't available. Fall back to timeout.
           TIME_OUT="timeout"
-        fi
-      fi
-    fi
 
-    if [ "$TIME_OUT" = "timeout" ]; then
+    if TIME_OUT == "timeout":
       # Add timeout command if time out is desired.
       #
       # Note: We first send SIGTERM (the timeout default, signal 15) to the signal dumper, which
@@ -1431,116 +1389,104 @@
       #       dumping do not lead to a deadlock, we also use the "-k" option to definitely kill the
       #       child.
       # Note: Using "--foreground" to not propagate the signal to children, i.e., the runtime.
-      cmdline="timeout --foreground -k 120s ${TIME_OUT_VALUE}s ${TIMEOUT_DUMPER} -s 15 $cmdline"
-    fi
+      cmdline=f"timeout --foreground -k 120s {TIME_OUT_VALUE}s {TIMEOUT_DUMPER} -s 15 {cmdline}"
 
-    if [ "$DEV_MODE" = "y" ]; then
-      for var in ANDROID_PRINTF_LOG ANDROID_DATA ANDROID_ROOT ANDROID_I18N_ROOT ANDROID_TZDATA_ROOT ANDROID_ART_ROOT LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH LD_USE_LOAD_BIAS; do
-        echo EXPORT $var=${!var}
-      done
-      echo "$(declare -f linkdirs)"
-      echo "mkdir -p ${mkdir_locations} && $setupapex_cmdline && ( $installapex_test_cmdline || $installapex_cmdline ) && $linkroot_cmdline && $linkroot_overlay_cmdline && $profman_cmdline && $dex2oat_cmdline && $dm_cmdline && $vdex_cmdline && $strip_cmdline && $sync_cmdline && $cmdline"
-    fi
+    if DEV_MODE == "y":
+      for var in "ANDROID_PRINTF_LOG ANDROID_DATA ANDROID_ROOT ANDROID_I18N_ROOT ANDROID_TZDATA_ROOT ANDROID_ART_ROOT LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH LD_USE_LOAD_BIAS".split(" "):
+        value = os.environ.get(var, "")
+        print(f"echo EXPORT {var}={value}")
+      print("$(declare -f linkdirs)")
+      print(f"mkdir -p {mkdir_locations} && {setupapex_cmdline} && ( {installapex_test_cmdline} || {installapex_cmdline} ) && {linkroot_cmdline} && {linkroot_overlay_cmdline} && {profman_cmdline} && {dex2oat_cmdline} && {dm_cmdline} && {vdex_cmdline} && {strip_cmdline} && {sync_cmdline} && {cmdline}")
 
-    cd $ANDROID_BUILD_TOP
+    os.chdir(ANDROID_BUILD_TOP)
 
     # Make sure we delete any existing compiler artifacts.
     # This enables tests to call the RUN script multiple times in a row
     # without worrying about interference.
-    rm -rf ${DEX_LOCATION}/oat
-    rm -rf ${DEX_LOCATION}/dalvik-cache/
+    shutil.rmtree(f"{DEX_LOCATION}/oat", ignore_errors=True)
+    shutil.rmtree(f"{DEX_LOCATION}/dalvik-cache/", ignore_errors=True)
 
-    export ASAN_OPTIONS=$RUN_TEST_ASAN_OPTIONS
+    export("ASAN_OPTIONS", RUN_TEST_ASAN_OPTIONS)
 
-    mkdir -p ${mkdir_locations} || exit 1
-    $setupapex_cmdline || { echo "zipapex extraction failed." >&2 ; exit 2; }
-    $installapex_test_cmdline || $installapex_cmdline || { echo "zipapex install failed. cmd was: ${installapex_test_cmdline} || ${installapex_cmdline}." >&2; find ${mkdir_locations} -type f >&2; exit 2; }
-    $linkroot_cmdline || { echo "create symlink android-root failed." >&2 ; exit 2; }
-    $linkroot_overlay_cmdline || { echo "overlay android-root failed." >&2 ; exit 2; }
-    $profman_cmdline || { echo "Profman failed." >&2 ; exit 2; }
-    eval "$dex2oat_cmdline" || { echo "Dex2oat failed." >&2 ; exit 2; }
-    eval "$dm_cmdline" || { echo "Dex2oat failed." >&2 ; exit 2; }
-    eval "$vdex_cmdline" || { echo "Dex2oat failed." >&2 ; exit 2; }
-    $strip_cmdline || { echo "Strip failed." >&2 ; exit 3; }
-    $sync_cmdline || { echo "Sync failed." >&2 ; exit 4; }
+    run(f"mkdir -p {mkdir_locations}", save_cmd=False)
+    run(setupapex_cmdline)
+    if run(installapex_test_cmdline, check=False).returncode != 0:
+      run(installapex_cmdline)
+    run(linkroot_cmdline)
+    run(linkroot_overlay_cmdline)
+    run(profman_cmdline)
+    run(dex2oat_cmdline)
+    run(dm_cmdline)
+    run(vdex_cmdline)
+    run(strip_cmdline)
+    run(sync_cmdline)
 
-    if [ "$CREATE_RUNNER" = "y" ]; then
-      echo "#!/bin/bash" > ${DEX_LOCATION}/runit.sh
-      for var in ANDROID_PRINTF_LOG ANDROID_DATA ANDROID_ROOT ANDROID_I18N_ROOT ANDROID_TZDATA_ROOT ANDROID_ART_ROOT LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH LD_USE_LOAD_BIAS; do
-        echo export $var="${!var}" >> ${DEX_LOCATION}/runit.sh
-      done
-      if [ "$DEV_MODE" = "y" ]; then
-        echo $cmdline >> ${DEX_LOCATION}/runit.sh
-      else
-        echo 'STDERR=$(mktemp)' >> ${DEX_LOCATION}/runit.sh
-        echo 'STDOUT=$(mktemp)' >> ${DEX_LOCATION}/runit.sh
-        echo $cmdline '>${STDOUT} 2>${STDERR}' >> ${DEX_LOCATION}/runit.sh
-        echo 'if diff ${STDOUT} $ANDROID_DATA/expected-stdout.txt; then' \
-          >> ${DEX_LOCATION}/runit.sh
-        echo '  rm -f ${STDOUT} ${STDERR}' >> ${DEX_LOCATION}/runit.sh
-        echo '  exit 0' >> ${DEX_LOCATION}/runit.sh
-        echo 'elif diff ${STDERR} $ANDROID_DATA/expected-stderr.txt; then' \
-          >> ${DEX_LOCATION}/runit.sh
-        echo '  rm -f ${STDOUT} ${STDERR}' >> ${DEX_LOCATION}/runit.sh
-        echo '  exit 0' >> ${DEX_LOCATION}/runit.sh
-        echo 'else' >> ${DEX_LOCATION}/runit.sh
-        echo '  echo  STDOUT:' >> ${DEX_LOCATION}/runit.sh
-        echo '  cat ${STDOUT}' >> ${DEX_LOCATION}/runit.sh
-        echo '  echo  STDERR:' >> ${DEX_LOCATION}/runit.sh
-        echo '  cat ${STDERR}' >> ${DEX_LOCATION}/runit.sh
-        echo '  rm -f ${STDOUT} ${STDERR}' >> ${DEX_LOCATION}/runit.sh
-        echo '  exit 1' >> ${DEX_LOCATION}/runit.sh
-        echo 'fi' >> ${DEX_LOCATION}/runit.sh
-      fi
-      chmod u+x $DEX_LOCATION/runit.sh
-      echo "Runnable test script written to ${DEX_LOCATION}/runit.sh"
-    fi
-    if [ "$DRY_RUN" = "y" ]; then
-      exit 0
-    fi
+    if CREATE_RUNNER == "y":
+      with open(f"{DEX_LOCATION}/runit.sh", "w") as f:
+        f.write("#!/bin/bash")
+        for var in "ANDROID_PRINTF_LOG ANDROID_DATA ANDROID_ROOT ANDROID_I18N_ROOT ANDROID_TZDATA_ROOT ANDROID_ART_ROOT LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH LD_USE_LOAD_BIAS".split(" "):
+          value = os.environ.get(var, "")
+          f.write(f'export {var}="{value}"')
+        if DEV_MODE == "y":
+          f.write(cmdline)
+        else:
+          f.writelines([
+            'STDERR=$(mktemp)',
+            'STDOUT=$(mktemp)',
+            cmdline + ' >${STDOUT} 2>${STDERR}',
+            'if diff ${STDOUT} {ANDROID_DATA}/expected-stdout.txt; then',
+            '  rm -f ${STDOUT} ${STDERR}',
+            '  exit 0',
+            'elif diff ${STDERR} {ANDROID_DATA}/expected-stderr.txt; then',
+            '  rm -f ${STDOUT} ${STDERR}',
+            '  exit 0',
+            'else',
+            '  echo  STDOUT:',
+            '  cat ${STDOUT}',
+            '  echo  STDERR:',
+            '  cat ${STDERR}',
+            '  rm -f ${STDOUT} ${STDERR}',
+            '  exit 1',
+            'fi',
+          ])
+      os.chmod("{DEX_LOCATION}/runit.sh", 0o777)
+      print(f"Runnable test script written to {DEX_LOCATION}/runit.sh")
+    if DRY_RUN == "y":
+      sys.exit(0)
 
-    if [ "$USE_GDB" = "y" ]; then
+    if USE_GDB == "y":
       # When running under gdb, we cannot do piping and grepping...
-      $cmdline "$@"
-    elif [ "$USE_GDBSERVER" = "y" ]; then
-      echo "Connect to $GDBSERVER_PORT"
+      subprocess.run(cmdline + test_args, shell=True)
+    elif USE_GDBSERVER == "y":
+      print("Connect to {GDBSERVER_PORT}")
       # When running under gdb, we cannot do piping and grepping...
-      $cmdline "$@"
-    else
-      if [ "$TIME_OUT" != "gdb" ]; then
-        trap 'kill -INT -$pid' INT
-        $cmdline "$@" & pid=$!
-        wait $pid
-        exit_value=$?
+      subprocess.run(cmdline + test_args, shell=True)
+    else:
+      if TIME_OUT != "gdb":
+        proc = run(cmdline + test_args, check=False, capture_output=False)
+        exit_value=proc.returncode
         # Add extra detail if time out is enabled.
-        if [ $exit_value = 124 ] && [ "$TIME_OUT" = "timeout" ]; then
-          echo -e "\e[91mTEST TIMED OUT!\e[0m" >&2
-        fi
-        exit $exit_value
-      else
+        if exit_value == 124 and TIME_OUT == "timeout":
+          print("\e[91mTEST TIMED OUT!\e[0m", file=sys.stderr)
+        sys.exit(exit_value)
+      else:
         # With a thread dump that uses gdb if a timeout.
-        trap 'kill -INT -$pid' INT
-        $cmdline "$@" & pid=$!
-        # Spawn a watcher process.
-        ( sleep $TIME_OUT_VALUE && \
-          echo "##### Thread dump using gdb on test timeout" && \
-          ( gdb -q -p $pid --eval-command="info thread" --eval-command="thread apply all bt" \
-                           --eval-command="call exit(124)" --eval-command=quit || \
-            kill $pid )) 2> /dev/null & watcher=$!
-        wait $pid
-        test_exit_status=$?
-        pkill -P $watcher 2> /dev/null # kill the sleep which will in turn end the watcher as well
-        if [ $test_exit_status = 0 ]; then
+        proc = run(cmdline + test_args, check=False)
+        # TODO: Spawn a watcher process.
+        raise Exception("Not implemented")
+        # ( sleep {TIME_OUT_VALUE} && \
+        #   echo "##### Thread dump using gdb on test timeout" && \
+        #   ( gdb -q -p {pid} --eval-command="info thread" --eval-command="thread apply all bt" \
+        #                    --eval-command="call exit(124)" --eval-command=quit || \
+        #     kill {pid} )) 2> /dev/null & watcher=$!
+        test_exit_status=proc.returncode
+        # pkill -P {watcher} 2> /dev/null # kill the sleep which will in turn end the watcher as well
+        if test_exit_status == 0:
           # The test finished normally.
-          exit 0
-        else
+          sys.exit(0)
+        else:
           # The test failed or timed out.
-          if [ $test_exit_status = 124 ]; then
+          if test_exit_status == 124:
             # The test timed out.
-            echo -e "\e[91mTEST TIMED OUT!\e[0m" >&2
-          fi
-          exit $test_exit_status
-        fi
-      fi
-    fi
-fi
+            print("\e[91mTEST TIMED OUT!\e[0m", file=sys.stderr)
+          sys.exit(test_exit_status)
diff --git a/test/run-test b/test/run-test
index dccc9f6..a14ad95 100755
--- a/test/run-test
+++ b/test/run-test
@@ -40,16 +40,18 @@
   tmp_dir="${TMPDIR}/${test_dir}"
 fi
 checker="${progdir}/../tools/checker/checker.py"
-export JAVA="java"
-export JAVAC="javac -g -Xlint:-options -source 1.8 -target 1.8"
-export RUN="${progdir}/etc/run-test-jar"
-export DEX_LOCATION=/data/run-test/${test_dir}
 
 # ANDROID_BUILD_TOP is not set in a build environment.
 if [ -z "$ANDROID_BUILD_TOP" ]; then
     export ANDROID_BUILD_TOP=$oldwd
 fi
 
+export JAVA="java"
+export JAVAC="javac -g -Xlint:-options -source 1.8 -target 1.8"
+export PYTHON3="${ANDROID_BUILD_TOP}/prebuilts/build-tools/path/linux-x86/python3"
+export RUN="${PYTHON3} ${progdir}/etc/run-test-jar"
+export DEX_LOCATION=/data/run-test/${test_dir}
+
 # OUT_DIR defaults to out, and may be relative to $ANDROID_BUILD_TOP.
 # Convert it to an absolute path, since we cd into the tmp_dir to run the tests.
 export OUT_DIR=${OUT_DIR:-out}
diff --git a/test/testrunner/testrunner.py b/test/testrunner/testrunner.py
index de0cd62..9ab4ba7 100755
--- a/test/testrunner/testrunner.py
+++ b/test/testrunner/testrunner.py
@@ -665,9 +665,12 @@
       test_start_time = time.monotonic()
       if verbose:
         print_text("Starting %s at %s\n" % (test_name, test_start_time))
+      env = dict(os.environ)
+      env["FULL_TEST_NAME"] = test_name
       if gdb or gdb_dex2oat:
         proc = _popen(
           args=command.split(),
+          env=env,
           stderr=subprocess.STDOUT,
           universal_newlines=True,
           start_new_session=True
@@ -675,6 +678,7 @@
       else:
         proc = _popen(
           args=command.split(),
+          env=env,
           stderr=subprocess.STDOUT,
           stdout = subprocess.PIPE,
           universal_newlines=True,