Convert per-test run scripts to python.

Test: test.py -r --all-target
Test: diff emitted test commands before and after
Change-Id: I05003b5cde0e39ffc7d037ef875a45f13dd83755
diff --git a/test/000-nop/run b/test/000-nop/run
index 210296b..9239ae8 100644
--- a/test/000-nop/run
+++ b/test/000-nop/run
@@ -1,3 +1,5 @@
 #!/bin/sh
 
-echo "Blort."
+
+def run(ctx, args):
+  ctx.echo("Blort.")
diff --git a/test/004-ThreadStress/run b/test/004-ThreadStress/run
index 87dfb27..a34b1a4 100755
--- a/test/004-ThreadStress/run
+++ b/test/004-ThreadStress/run
@@ -14,15 +14,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Enable lock contention logging.
-${RUN} "${@}" --runtime-option -Xlockprofthreshold:10
-return_status1=$?
+import sys
 
-# Run locks-only mode with stack-dump lock profiling. Reduce the number of total operations from
-# the default 1000 to 100.
-${RUN} "${@}" --runtime-option -Xlockprofthreshold:10 \
-  --runtime-option -Xstackdumplockprofthreshold:20 -- Main --locks-only -o 100
-return_status2=$?
 
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2)
+def run(ctx, args):
+  # Enable lock contention logging.
+  ctx.default_run(args, runtime_option=["-Xlockprofthreshold:10"])
+
+  # Run locks-only mode with stack-dump lock profiling. Reduce the number of total operations from
+  # the default 1000 to 100.
+  ctx.default_run(
+      args,
+      test_args=["--locks-only -o 100"],
+      runtime_option=[
+          "-Xlockprofthreshold:10", "-Xstackdumplockprofthreshold:20"
+      ])
diff --git a/test/030-bad-finalizer/run b/test/030-bad-finalizer/run
index 54747ee..a849d79 100755
--- a/test/030-bad-finalizer/run
+++ b/test/030-bad-finalizer/run
@@ -14,9 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The test logs error messages which is expected, discard them.
-export ANDROID_LOG_TAGS='*:f'
 
-# Squash the exit status and put it in expected
-./default-run --external-log-tags "${@}"
-echo "exit status:" $?
+def run(ctx, args):
+  # The test logs error messages which is expected, discard them.
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+
+  ctx.default_run(args, external_log_tags=True, expected_exit_code=2)
diff --git a/test/034-call-null/run b/test/034-call-null/run
index 7a0d0d0..80a8a33 100755
--- a/test/034-call-null/run
+++ b/test/034-call-null/run
@@ -14,6 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Squash the exit status and put it in expected
-./default-run "$@"
-echo "exit status:" $?
+
+def run(ctx, args):
+  ctx.default_run(args, expected_exit_code=1)
diff --git a/test/038-inner-null/run b/test/038-inner-null/run
index 7a0d0d0..80a8a33 100755
--- a/test/038-inner-null/run
+++ b/test/038-inner-null/run
@@ -14,6 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Squash the exit status and put it in expected
-./default-run "$@"
-echo "exit status:" $?
+
+def run(ctx, args):
+  ctx.default_run(args, expected_exit_code=1)
diff --git a/test/044-proxy/run b/test/044-proxy/run
index 4a322f3..1784fc5 100644
--- a/test/044-proxy/run
+++ b/test/044-proxy/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a smaller heap so it's easier to fill up.
-exec ${RUN} $@ --runtime-option -Xmx4m
+
+def run(ctx, args):
+  # Use a smaller heap so it's easier to fill up.
+  ctx.default_run(args, runtime_option=["-Xmx4m"])
diff --git a/test/054-uncaught/run b/test/054-uncaught/run
index 7a0d0d0..80a8a33 100755
--- a/test/054-uncaught/run
+++ b/test/054-uncaught/run
@@ -14,6 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Squash the exit status and put it in expected
-./default-run "$@"
-echo "exit status:" $?
+
+def run(ctx, args):
+  ctx.default_run(args, expected_exit_code=1)
diff --git a/test/055-enum-performance/run b/test/055-enum-performance/run
index e27a622..dbb09fd 100755
--- a/test/055-enum-performance/run
+++ b/test/055-enum-performance/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# As this is a performance test we always use the non-debug build.
-exec ${RUN} "${@/#libartd.so/libart.so}"
+
+def run(ctx, args):
+  # As this is a performance test we always use the non-debug build.
+  ctx.default_run(args, lib="libart.so")
diff --git a/test/059-finalizer-throw/run b/test/059-finalizer-throw/run
index dda4159..0d9274e 100644
--- a/test/059-finalizer-throw/run
+++ b/test/059-finalizer-throw/run
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The test logs error messages which is expected, discard them.
-export ANDROID_LOG_TAGS='*:f'
-exec ${RUN} --external-log-tags "${@}"
+
+def run(ctx, args):
+  # The test logs error messages which is expected, discard them.
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+  ctx.default_run(args, external_log_tags=True)
diff --git a/test/064-field-access/run b/test/064-field-access/run
index 4a322f3..1784fc5 100644
--- a/test/064-field-access/run
+++ b/test/064-field-access/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a smaller heap so it's easier to fill up.
-exec ${RUN} $@ --runtime-option -Xmx4m
+
+def run(ctx, args):
+  # Use a smaller heap so it's easier to fill up.
+  ctx.default_run(args, runtime_option=["-Xmx4m"])
diff --git a/test/071-dexfile-map-clean/run b/test/071-dexfile-map-clean/run
index afa2ff7..eda60fa 100755
--- a/test/071-dexfile-map-clean/run
+++ b/test/071-dexfile-map-clean/run
@@ -14,12 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Make sure we call 'sync'
-# before executing dalvikvm because otherwise
-# it's highly likely the pushed JAR files haven't
-# been committed to permanent storage yet,
-# and when we mmap them the kernel will think
-# the memory is dirty (despite being file-backed).
-# (Note: this was reproducible 100% of the time on
-# a target angler device).
-./default-run "$@" --sync
+
+def run(ctx, args):
+  # Make sure we call 'sync'
+  # before executing dalvikvm because otherwise
+  # it's highly likely the pushed JAR files haven't
+  # been committed to permanent storage yet,
+  # and when we mmap them the kernel will think
+  # the memory is dirty (despite being file-backed).
+  # (Note: this was reproducible 100% of the time on
+  # a target angler device).
+  ctx.default_run(args, sync=True)
diff --git a/test/080-oom-throw/run b/test/080-oom-throw/run
index 08db73b..fb10464 100644
--- a/test/080-oom-throw/run
+++ b/test/080-oom-throw/run
@@ -14,18 +14,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ensure the minimum log severity is at least 'WARNING' to display the
-# stack trace shown before exception
-#
-#   "java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying
-#   to throw OutOfMemoryError; no stack trace available"
-#
-# is set, to try to understand a recurring crash in this test (b/77567088).
-case "$ANDROID_LOG_TAGS" in
-  # Lower the minimum log severity to WARNING if it was initialy set
-  # to a higher level ('ERROR', 'FATAL' or 'SILENT' -- see
-  # https://developer.android.com/studio/command-line/logcat#filteringOutput).
-  (\*:[efs]) export ANDROID_LOG_TAGS='*:w';;
-esac
 
-exec ${RUN} $@ --runtime-option -Xmx16m
+def run(ctx, args):
+  # Ensure the minimum log severity is at least 'WARNING' to display the
+  # stack trace shown before exception
+  #
+  #   "java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying
+  #   to throw OutOfMemoryError; no stack trace available"
+  #
+  # is set, to try to understand a recurring crash in this test (b/77567088).
+  if ctx.env.ANDROID_LOG_TAGS in ["*:e", "*:f", "*:s"]:
+    # Lower the minimum log severity to WARNING if it was initialy set
+    # to a higher level ('ERROR', 'FATAL' or 'SILENT' -- see
+    # https://developer.android.com/studio/command-line/logcat#filteringOutput).
+    ctx.env.ANDROID_LOG_TAGS = "*:w"
+
+  ctx.default_run(args, runtime_option=["-Xmx16m"])
diff --git a/test/089-many-methods/run b/test/089-many-methods/run
index 867dedc..ba0f83a 100644
--- a/test/089-many-methods/run
+++ b/test/089-many-methods/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Do nothing - the build intentionally failed.
+
+def run(ctx, args):
+  # Do nothing - the build intentionally failed.
+  pass
diff --git a/test/091-override-package-private-method/run b/test/091-override-package-private-method/run
index d8c3c79..2746221 100755
--- a/test/091-override-package-private-method/run
+++ b/test/091-override-package-private-method/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use secondary switch to add secondary dex file to class path.
-exec ${RUN} "${@}" --secondary
+
+def run(ctx, args):
+  # Use secondary switch to add secondary dex file to class path.
+  ctx.default_run(args, secondary=True)
diff --git a/test/etc/default-run b/test/099-vmdebug/run
similarity index 77%
rename from test/etc/default-run
rename to test/099-vmdebug/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/099-vmdebug/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/1001-app-image-regions/run b/test/1001-app-image-regions/run
index 128aa2e..d529700 100644
--- a/test/1001-app-image-regions/run
+++ b/test/1001-app-image-regions/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/etc/default-run b/test/1002-notify-startup/run
similarity index 77%
copy from test/etc/default-run
copy to test/1002-notify-startup/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/1002-notify-startup/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/1003-metadata-section-strings/run b/test/1003-metadata-section-strings/run
index 9762aba..7fc97aa 100644
--- a/test/1003-metadata-section-strings/run
+++ b/test/1003-metadata-section-strings/run
@@ -14,4 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile -Xcompiler-option --resolve-startup-const-strings=true
+
+def run(ctx, args):
+  ctx.default_run(
+      args,
+      profile=True,
+      Xcompiler_option=[
+          "--compiler-filter=speed-profile",
+          "--resolve-startup-const-strings=true"
+      ])
diff --git a/test/1004-checker-volatile-ref-load/run b/test/1004-checker-volatile-ref-load/run
index bdaa71b..97dc42a 100644
--- a/test/1004-checker-volatile-ref-load/run
+++ b/test/1004-checker-volatile-ref-load/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Limit the managed heap to 16 MiB to force more garbage collections.
-exec ${RUN} $@ --runtime-option -Xmx16m
+
+def run(ctx, args):
+  # Limit the managed heap to 16 MiB to force more garbage collections.
+  ctx.default_run(args, runtime_option=["-Xmx16m"])
diff --git a/test/115-native-bridge/run b/test/115-native-bridge/run
index 1cb03e7..19b0443 100644
--- a/test/115-native-bridge/run
+++ b/test/115-native-bridge/run
@@ -14,27 +14,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-ARGS=${@}
+import re
 
-BRIDGE_SO=libnativebridgetestd.so
-if echo ${ARGS} | grep -q " -O"; then
-  BRIDGE_SO=libnativebridgetest.so
-fi
 
-# Use libnativebridgetest as a native bridge, start NativeBridgeMain (Main is JniTest main file).
-LIBPATH=$(echo ${ARGS} | sed -r 's/.*Djava.library.path=([^ ]*) .*/\1/')
-# Trim all but the last entry in LIBPATH, which will be nativetest[64]
-LIBPATH=${LIBPATH##*:}
-ln -sf ${LIBPATH}/$BRIDGE_SO .
-touch libarttest.so
-touch libarttestd.so
-touch libinvalid.so
-ln -sf ${LIBPATH}/libarttest.so libarttest2.so
-ln -sf ${LIBPATH}/libarttestd.so libarttestd2.so
+def run(ctx, args):
+  bridge_so = "libnativebridgetest.so" if args.O else "libnativebridgetestd.so"
+  test_dir = ctx.env.DEX_LOCATION
 
-# pwd likely has /, so it's a pain to put that into a sed rule.
-LEFT=$(echo ${ARGS} | sed -r 's/-Djava.library.path.*//')
-RIGHT=$(echo ${ARGS} | sed -r 's/.*Djava.library.path[^ ]* //')
-MODARGS="${LEFT} -Djava.library.path=`pwd` ${RIGHT}"
-exec ${RUN} ${MODARGS} --runtime-option -Xforce-nb-testing \
-  --runtime-option -XX:NativeBridge=$BRIDGE_SO NativeBridgeMain
+  # Use libnativebridgetest as a native bridge, start NativeBridgeMain (Main is JniTest main file).
+  for i, opt in enumerate(args.runtime_option):
+    if opt.startswith("-Djava.library.path="):
+      libpath = opt.split(":")[-1]  # last entry in libpath is nativetest[64]
+      args.runtime_option[i] = "-Djava.library.path=" + test_dir
+
+  assert libpath
+  ctx.bash(f"ln -sf {libpath}/{bridge_so} {test_dir}/.")
+  ctx.bash(
+      f"touch {test_dir}/libarttest.so {test_dir}/libarttestd.so {test_dir}/libinvalid.so"
+  )
+  ctx.bash(f"ln -sf {libpath}/libarttest.so {test_dir}/libarttest2.so")
+  ctx.bash(f"ln -sf {libpath}/libarttestd.so {test_dir}/libarttestd2.so")
+
+  ctx.default_run(
+      args,
+      runtime_option=["-Xforce-nb-testing", f"-XX:NativeBridge={bridge_so}"],
+      main="NativeBridgeMain")
diff --git a/test/116-nodex2oat/run b/test/116-nodex2oat/run
index 9063685..5b38316 100755
--- a/test/116-nodex2oat/run
+++ b/test/116-nodex2oat/run
@@ -14,14 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-flags="${@}"
 
-# This test is supposed to test without oat files, so doesn't work for prebuild. Make sure that
-# flag isn't set, or complain.
-# Note: prebuild is the default.
-if [[ "${flags}" == *--prebuild* || "${flags}" != *--no-prebuild* ]] ; then
-  echo "Test 116-nodex2oat cannot run in prebuild mode."
-  exit 1
-fi
+def run(ctx, args):
+  # This test is supposed to test without oat files, so doesn't work for prebuild.
+  assert not args.prebuild
 
-${RUN} ${flags}
+  ctx.default_run(args)
diff --git a/test/118-noimage-dex2oat/run b/test/118-noimage-dex2oat/run
index 1b6251d..d4a71e9 100644
--- a/test/118-noimage-dex2oat/run
+++ b/test/118-noimage-dex2oat/run
@@ -14,38 +14,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-flags="$@"
 
-# This test is supposed to test without oat files, so doesn't work for prebuild. Make sure that
-# flag isn't set, or complain.
-# Note: prebuild is the default.
-if [[ "${flags}" == *--prebuild* || "${flags}" != *--no-prebuild* ]] ; then
-  echo "Test 118-noimage-dex2oat cannot run in prebuild mode."
-  exit 1
-fi
+def run(ctx, args):
+  # This test is supposed to test without oat files, so doesn't work for prebuild.
+  assert not args.prebuild
 
-# Force relocation otherwise we will just use the already created core.oat/art pair.
-# Note: relocate is the default.
-if [[ "${flags}" == *--no-relocate* ]] ; then
-  echo "Test 118-noimage-dex2oat is not intended to run in no-relocate mode."
-  exit 1
-fi
+  # Force relocation otherwise we will just use the already created core.oat/art pair.
+  assert args.relocate
 
+  # Make sure we can run without an oat file.
+  ctx.echo("Run -Xnoimage-dex2oat")
+  ctx.default_run(args, runtime_option=["-Xnoimage-dex2oat"])
 
-# Make sure we can run without an oat file.
-echo "Run -Xnoimage-dex2oat"
-${RUN} ${flags} --runtime-option -Xnoimage-dex2oat
-return_status1=$?
+  # Make sure we can run with the oat file.
+  ctx.echo("Run -Ximage-dex2oat")
+  ctx.default_run(args, runtime_option=["-Ximage-dex2oat"])
 
-# Make sure we can run with the oat file.
-echo "Run -Ximage-dex2oat"
-${RUN} ${flags} --runtime-option -Ximage-dex2oat
-return_status2=$?
-
-# Make sure we can run with the default settings.
-echo "Run default"
-${RUN} ${flags}
-return_status3=$?
-
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2) && (exit $return_status3)
+  # Make sure we can run with the default settings.
+  ctx.echo("Run default")
+  ctx.default_run(args)
diff --git a/test/126-miranda-multidex/run b/test/126-miranda-multidex/run
index abd63cb..1896315 100755
--- a/test/126-miranda-multidex/run
+++ b/test/126-miranda-multidex/run
@@ -14,13 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-${RUN} $@
-return_status1=$?
+import sys
 
-# The problem was first exposed in a no-verify setting, as that changes the resolution path
-# taken. Make sure we also test in that environment.
-${RUN} --no-verify ${@}
-return_status2=$?
 
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2)
+def run(ctx, args):
+  ctx.default_run(args)
+
+  # The problem was first exposed in a no-verify setting, as that changes the resolution path
+  # taken. Make sure we also test in that environment.
+  ctx.default_run(args, no_verify=True)
diff --git a/test/127-checker-secondarydex/run b/test/127-checker-secondarydex/run
index d8c3c79..2746221 100755
--- a/test/127-checker-secondarydex/run
+++ b/test/127-checker-secondarydex/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use secondary switch to add secondary dex file to class path.
-exec ${RUN} "${@}" --secondary
+
+def run(ctx, args):
+  # Use secondary switch to add secondary dex file to class path.
+  ctx.default_run(args, secondary=True)
diff --git a/test/130-hprof/run b/test/130-hprof/run
index 73a984e..c3fda2f 100644
--- a/test/130-hprof/run
+++ b/test/130-hprof/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Currently app images aren't unloaded when dex files are unloaded.
-exec ${RUN} $@ --no-secondary-app-image
+
+def run(ctx, args):
+  # Currently app images aren't unloaded when dex files are unloaded.
+  ctx.default_run(args, secondary_app_image=False)
diff --git a/test/133-static-invoke-super/run b/test/133-static-invoke-super/run
index e27a622..dbb09fd 100755
--- a/test/133-static-invoke-super/run
+++ b/test/133-static-invoke-super/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# As this is a performance test we always use the non-debug build.
-exec ${RUN} "${@/#libartd.so/libart.so}"
+
+def run(ctx, args):
+  # As this is a performance test we always use the non-debug build.
+  ctx.default_run(args, lib="libart.so")
diff --git a/test/1336-short-finalizer-timeout/run b/test/1336-short-finalizer-timeout/run
index 4a07034..e4aed7b 100755
--- a/test/1336-short-finalizer-timeout/run
+++ b/test/1336-short-finalizer-timeout/run
@@ -14,9 +14,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The test logs error messages which is expected, discard them.
-export ANDROID_LOG_TAGS='*:f'
 
-# Squash the exit status and put it in expected
-./default-run --external-log-tags "$@" --runtime-option -XX:FinalizerTimeoutMs=500
-echo "exit status:" $?
+def run(ctx, args):
+  # The test logs error messages which is expected, discard them.
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+
+  ctx.default_run(
+      args,
+      external_log_tags=True,
+      runtime_option=["-XX:FinalizerTimeoutMs=500"],
+      expected_exit_code=2)
diff --git a/test/etc/default-run b/test/1337-gc-coverage/run
similarity index 77%
copy from test/etc/default-run
copy to test/1337-gc-coverage/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/1337-gc-coverage/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/1338-gc-no-los/run b/test/1338-gc-no-los/run
index f3c43fb..1c64d11 100755
--- a/test/1338-gc-no-los/run
+++ b/test/1338-gc-no-los/run
@@ -13,4 +13,7 @@
 # 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.
-./default-run "$@" --runtime-option -XX:LargeObjectSpace=disabled
+
+
+def run(ctx, args):
+  ctx.default_run(args, runtime_option=["-XX:LargeObjectSpace=disabled"])
diff --git a/test/etc/default-run b/test/1339-dead-reference-safe/run
similarity index 77%
copy from test/etc/default-run
copy to test/1339-dead-reference-safe/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/1339-dead-reference-safe/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/137-cfi/run b/test/137-cfi/run
index f09765f..987d147 100755
--- a/test/137-cfi/run
+++ b/test/137-cfi/run
@@ -14,26 +14,30 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Test with full DWARF debugging information.
-# Check full signatures of methods.
-${RUN} "$@" -Xcompiler-option --generate-debug-info \
-  --args --test-local --args --test-remote
-return_status1=$?
+import sys
 
-# The option jitthreshold:0 ensures that if we run the test in JIT mode,
-# there will be JITed frames on the callstack (it synchronously JITs on first use).
-${RUN} "$@" -Xcompiler-option --generate-debug-info \
-  --runtime-option -Xjitthreshold:0 \
-  --args --test-local --args --test-remote
-return_status2=$?
 
-# Test with minimal compressed debugging information.
-# Check only method names (parameters are omitted to save space).
-# Check only remote unwinding since decompression is disabled in local unwinds (b/27391690).
-${RUN} "$@" -Xcompiler-option --generate-mini-debug-info \
-  --runtime-option -Xjitthreshold:0 \
-  --args --test-remote
-return_status3=$?
+def run(ctx, args):
+  # Test with full DWARF debugging information.
+  # Check full signatures of methods.
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--generate-debug-info"],
+      test_args=["--test-local", "--test-remote"])
 
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2) && (exit $return_status3)
+  # The option jitthreshold:0 ensures that if we run the test in JIT mode,
+  # there will be JITed frames on the callstack (it synchronously JITs on first use).
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--generate-debug-info"],
+      runtime_option=["-Xjitthreshold:0"],
+      test_args=["--test-local", "--test-remote"])
+
+  # Test with minimal compressed debugging information.
+  # Check only method names (parameters are omitted to save space).
+  # Check only remote unwinding since decompression is disabled in local unwinds (b/27391690).
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--generate-mini-debug-info"],
+      runtime_option=["-Xjitthreshold:0"],
+      test_args=["--test-remote"])
diff --git a/test/etc/default-run b/test/139-register-natives/run
similarity index 77%
copy from test/etc/default-run
copy to test/139-register-natives/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/139-register-natives/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/141-class-unload/run b/test/141-class-unload/run
index 73a984e..c3fda2f 100644
--- a/test/141-class-unload/run
+++ b/test/141-class-unload/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Currently app images aren't unloaded when dex files are unloaded.
-exec ${RUN} $@ --no-secondary-app-image
+
+def run(ctx, args):
+  # Currently app images aren't unloaded when dex files are unloaded.
+  ctx.default_run(args, secondary_app_image=False)
diff --git a/test/etc/default-run b/test/143-string-value/run
similarity index 77%
copy from test/etc/default-run
copy to test/143-string-value/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/143-string-value/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/146-bad-interface/run b/test/146-bad-interface/run
index 2b4bbb0..913dac5 100755
--- a/test/146-bad-interface/run
+++ b/test/146-bad-interface/run
@@ -14,8 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use the `--secondary-class-loader-context` switch to compile the secondary dex
-# file with the right class loader context. Do not use `--secondary` as we're
-# loading the *-ex.jar file in a separate class loader.
-exec ${RUN} "${@}" \
-    --secondary-class-loader-context "PCL[$DEX_LOCATION/$TEST_NAME.jar]"
+
+def run(ctx, args):
+  # Use the `--secondary-class-loader-context` switch to compile the secondary dex
+  # file with the right class loader context. Do not use `--secondary` as we're
+  # loading the *-ex.jar file in a separate class loader.
+  pcl = f"PCL[{ctx.env.DEX_LOCATION}/{ctx.env.TEST_NAME}.jar]"
+  ctx.default_run(args, secondary_class_loader_context=pcl)
diff --git a/test/etc/default-run b/test/148-multithread-gc-annotations/run
similarity index 77%
copy from test/etc/default-run
copy to test/148-multithread-gc-annotations/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/148-multithread-gc-annotations/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/etc/default-run b/test/149-suspend-all-stress/run
similarity index 77%
copy from test/etc/default-run
copy to test/149-suspend-all-stress/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/149-suspend-all-stress/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/151-OpenFileLimit/run b/test/151-OpenFileLimit/run
index 6faeb0d..5e9f05a 100755
--- a/test/151-OpenFileLimit/run
+++ b/test/151-OpenFileLimit/run
@@ -14,11 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Filter out expected error messages, which happen on device.
-export ANDROID_LOG_TAGS='*:f'
+import resource
 
-flags="$@"
 
-# Reduce the file descriptor limit so the test will reach the limit sooner.
-ulimit -n 512
-${RUN} --external-log-tags ${flags}
+def run(ctx, args):
+  # Filter out expected error messages, which happen on device.
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+
+  # Reduce the file descriptor limit so the test will reach the limit sooner.
+  resource.setrlimit(resource.RLIMIT_NOFILE, (512, 512))
+
+  ctx.default_run(args, external_log_tags=True)
diff --git a/test/157-void-class/run b/test/157-void-class/run
index 8c6159f..c0cc3e9 100755
--- a/test/157-void-class/run
+++ b/test/157-void-class/run
@@ -14,9 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Let the test build its own core image with --no-image and use verify,
-# so that the compiler does not try to initialize classes. This leaves the
-# java.lang.Void compile-time verified but uninitialized.
-./default-run "$@" --no-image \
-    --runtime-option -Ximage-compiler-option \
-    --runtime-option --compiler-filter=verify
+
+def run(ctx, args):
+  # Let the test build its own core image with --no-image and use verify,
+  # so that the compiler does not try to initialize classes. This leaves the
+  # java.lang.Void compile-time verified but uninitialized.
+  ctx.default_run(
+      args,
+      image=False,
+      runtime_option=["-Ximage-compiler-option", "--compiler-filter=verify"])
diff --git a/test/158-app-image-class-table/run b/test/158-app-image-class-table/run
index 146e180..7678899 100644
--- a/test/158-app-image-class-table/run
+++ b/test/158-app-image-class-table/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/159-app-image-fields/run b/test/159-app-image-fields/run
index 74facb0..ae99f54 100644
--- a/test/159-app-image-fields/run
+++ b/test/159-app-image-fields/run
@@ -14,8 +14,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a profile to put specific classes in the app image.
-# Also run the compiler with -j1 to ensure specific class verification order.
-# And limit the managed heap to 16 MiB to speed up GCs.
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile \
-    -Xcompiler-option -j1 --runtime-option -Xmx16m
+
+def run(ctx, args):
+  # Use a profile to put specific classes in the app image.
+  # Also run the compiler with -j1 to ensure specific class verification order.
+  # And limit the managed heap to 16 MiB to speed up GCs.
+  ctx.default_run(
+      args,
+      profile=True,
+      Xcompiler_option=["--compiler-filter=speed-profile", "-j1"],
+      runtime_option=["-Xmx16m"])
diff --git a/test/160-read-barrier-stress/run b/test/160-read-barrier-stress/run
index ab82229..fb006ea 100644
--- a/test/160-read-barrier-stress/run
+++ b/test/160-read-barrier-stress/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Limit the Java heap to 16MiB to force more GCs.
-exec ${RUN} $@ --runtime-option -Xmx16m
+
+def run(ctx, args):
+  # Limit the Java heap to 16MiB to force more GCs.
+  ctx.default_run(args, runtime_option=["-Xmx16m"])
diff --git a/test/163-app-image-methods/run b/test/163-app-image-methods/run
index 74facb0..ae99f54 100644
--- a/test/163-app-image-methods/run
+++ b/test/163-app-image-methods/run
@@ -14,8 +14,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a profile to put specific classes in the app image.
-# Also run the compiler with -j1 to ensure specific class verification order.
-# And limit the managed heap to 16 MiB to speed up GCs.
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile \
-    -Xcompiler-option -j1 --runtime-option -Xmx16m
+
+def run(ctx, args):
+  # Use a profile to put specific classes in the app image.
+  # Also run the compiler with -j1 to ensure specific class verification order.
+  # And limit the managed heap to 16 MiB to speed up GCs.
+  ctx.default_run(
+      args,
+      profile=True,
+      Xcompiler_option=["--compiler-filter=speed-profile", "-j1"],
+      runtime_option=["-Xmx16m"])
diff --git a/test/164-resolution-trampoline-dex-cache/run b/test/164-resolution-trampoline-dex-cache/run
index 33a6f01..6e7b777 100644
--- a/test/164-resolution-trampoline-dex-cache/run
+++ b/test/164-resolution-trampoline-dex-cache/run
@@ -14,8 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Make sure we compile the required method using speed-profile compiler filter.
-# Enable JIT through runtime option to avoid the compiler filter set by --jit.
-exec ${RUN} "${@}" \
-    -Xcompiler-option --compiler-filter=speed-profile --profile \
-    --runtime-option -Xusejit:true
+
+def run(ctx, args):
+  # Make sure we compile the required method using speed-profile compiler filter.
+  # Enable JIT through runtime option to avoid the compiler filter set by --jit.
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--compiler-filter=speed-profile"],
+      profile=True,
+      runtime_option=["-Xusejit:true"])
diff --git a/test/165-lock-owner-proxy/run b/test/165-lock-owner-proxy/run
index 9365411..266f747 100644
--- a/test/165-lock-owner-proxy/run
+++ b/test/165-lock-owner-proxy/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a smaller heap so it's easier to potentially fill up.
-exec ${RUN} $@ --runtime-option -Xmx2m
+
+def run(ctx, args):
+  # Use a smaller heap so it's easier to potentially fill up.
+  ctx.default_run(args, runtime_option=["-Xmx2m"])
diff --git a/test/167-visit-locks/run b/test/167-visit-locks/run
index 9365411..266f747 100644
--- a/test/167-visit-locks/run
+++ b/test/167-visit-locks/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a smaller heap so it's easier to potentially fill up.
-exec ${RUN} $@ --runtime-option -Xmx2m
+
+def run(ctx, args):
+  # Use a smaller heap so it's easier to potentially fill up.
+  ctx.default_run(args, runtime_option=["-Xmx2m"])
diff --git a/test/168-vmstack-annotated/run b/test/168-vmstack-annotated/run
index 9365411..266f747 100644
--- a/test/168-vmstack-annotated/run
+++ b/test/168-vmstack-annotated/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a smaller heap so it's easier to potentially fill up.
-exec ${RUN} $@ --runtime-option -Xmx2m
+
+def run(ctx, args):
+  # Use a smaller heap so it's easier to potentially fill up.
+  ctx.default_run(args, runtime_option=["-Xmx2m"])
diff --git a/test/172-app-image-twice/run b/test/172-app-image-twice/run
index 2987b4b..866c822 100644
--- a/test/172-app-image-twice/run
+++ b/test/172-app-image-twice/run
@@ -14,6 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Build an app image with TestClass (specified by profile).
 
-${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+def run(ctx, args):
+  # Build an app image with TestClass (specified by profile).
+
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/176-app-image-string/run b/test/176-app-image-string/run
index 52d2b5f..0f3a0ea 100644
--- a/test/176-app-image-string/run
+++ b/test/176-app-image-string/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile
+
+def run(ctx, args):
+  ctx.default_run(args, profile=True)
diff --git a/test/178-app-image-native-method/run b/test/178-app-image-native-method/run
index 7cd0d57..c56a810 100644
--- a/test/178-app-image-native-method/run
+++ b/test/178-app-image-native-method/run
@@ -14,14 +14,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a profile to put specific classes in the app image. Increase the large
-# method threshold to compile Main.$noinline$opt$testCriticalSignatures().
-${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile \
-    -Xcompiler-option --large-method-max=2000
-return_status1=$?
 
-# Also run with the verify filter to avoid compiling JNI stubs.
-${RUN} ${@} --profile -Xcompiler-option --compiler-filter=verify
-return_status2=$?
+def run(ctx, args):
+  # Use a profile to put specific classes in the app image. Increase the large
+  # method threshold to compile Main.$noinline$opt$testCriticalSignatures().
+  ctx.default_run(
+      args,
+      profile=True,
+      Xcompiler_option=[
+          "--compiler-filter=speed-profile", "--large-method-max=2000"
+      ])
 
-(exit ${return_status1}) && (exit ${return_status2})
+  # Also run with the verify filter to avoid compiling JNI stubs.
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=verify"])
diff --git a/test/1900-track-alloc/run b/test/1900-track-alloc/run
index c6e62ae..4796039 100755
--- a/test/1900-track-alloc/run
+++ b/test/1900-track-alloc/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1901-get-bytecodes/run b/test/1901-get-bytecodes/run
index e92b873..b596886 100755
--- a/test/1901-get-bytecodes/run
+++ b/test/1901-get-bytecodes/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1902-suspend/run b/test/1902-suspend/run
index e92b873..b596886 100755
--- a/test/1902-suspend/run
+++ b/test/1902-suspend/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1903-suspend-self/run b/test/1903-suspend-self/run
index 97f077b..6f8d830 100755
--- a/test/1903-suspend-self/run
+++ b/test/1903-suspend-self/run
@@ -14,14 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
 
-return_status1=$?
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
 
-#Also do one run with --no-image to have one test coverage with no-image.
-./default-run "$@" --jvmti --no-image
-
-return_status2=$?
-
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2)
+  #Also do one run with --no-image to have one test coverage with no-image.
+  ctx.default_run(args, jvmti=True, image=False)
diff --git a/test/1904-double-suspend/run b/test/1904-double-suspend/run
index e92b873..b596886 100755
--- a/test/1904-double-suspend/run
+++ b/test/1904-double-suspend/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1905-suspend-native/run b/test/1905-suspend-native/run
index e92b873..b596886 100755
--- a/test/1905-suspend-native/run
+++ b/test/1905-suspend-native/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1906-suspend-list-me-first/run b/test/1906-suspend-list-me-first/run
index e92b873..b596886 100755
--- a/test/1906-suspend-list-me-first/run
+++ b/test/1906-suspend-list-me-first/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1907-suspend-list-self-twice/run b/test/1907-suspend-list-self-twice/run
index e92b873..b596886 100755
--- a/test/1907-suspend-list-self-twice/run
+++ b/test/1907-suspend-list-self-twice/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1908-suspend-native-resume-self/run b/test/1908-suspend-native-resume-self/run
index e92b873..b596886 100755
--- a/test/1908-suspend-native-resume-self/run
+++ b/test/1908-suspend-native-resume-self/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1909-per-agent-tls/run b/test/1909-per-agent-tls/run
index c6e62ae..4796039 100755
--- a/test/1909-per-agent-tls/run
+++ b/test/1909-per-agent-tls/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1910-transform-with-default/run b/test/1910-transform-with-default/run
index c6e62ae..4796039 100755
--- a/test/1910-transform-with-default/run
+++ b/test/1910-transform-with-default/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1911-get-local-var-table/run b/test/1911-get-local-var-table/run
index 51875a7..ce3a55a 100755
--- a/test/1911-get-local-var-table/run
+++ b/test/1911-get-local-var-table/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1912-get-set-local-primitive/run b/test/1912-get-set-local-primitive/run
index 51875a7..ce3a55a 100755
--- a/test/1912-get-set-local-primitive/run
+++ b/test/1912-get-set-local-primitive/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1913-get-set-local-objects/run b/test/1913-get-set-local-objects/run
index 51875a7..ce3a55a 100755
--- a/test/1913-get-set-local-objects/run
+++ b/test/1913-get-set-local-objects/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1914-get-local-instance/run b/test/1914-get-local-instance/run
index 51875a7..ce3a55a 100755
--- a/test/1914-get-local-instance/run
+++ b/test/1914-get-local-instance/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1915-get-set-local-current-thread/run b/test/1915-get-set-local-current-thread/run
index 51875a7..ce3a55a 100755
--- a/test/1915-get-set-local-current-thread/run
+++ b/test/1915-get-set-local-current-thread/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1916-get-set-current-frame/run b/test/1916-get-set-current-frame/run
index 51875a7..ce3a55a 100755
--- a/test/1916-get-set-current-frame/run
+++ b/test/1916-get-set-current-frame/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1917-get-stack-frame/run b/test/1917-get-stack-frame/run
index 51875a7..ce3a55a 100755
--- a/test/1917-get-stack-frame/run
+++ b/test/1917-get-stack-frame/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1919-vminit-thread-start-timing/run b/test/1919-vminit-thread-start-timing/run
index c6e62ae..4796039 100755
--- a/test/1919-vminit-thread-start-timing/run
+++ b/test/1919-vminit-thread-start-timing/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1920-suspend-native-monitor/run b/test/1920-suspend-native-monitor/run
index e92b873..b596886 100755
--- a/test/1920-suspend-native-monitor/run
+++ b/test/1920-suspend-native-monitor/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1921-suspend-native-recursive-monitor/run b/test/1921-suspend-native-recursive-monitor/run
index e92b873..b596886 100755
--- a/test/1921-suspend-native-recursive-monitor/run
+++ b/test/1921-suspend-native-recursive-monitor/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1922-owned-monitors-info/run b/test/1922-owned-monitors-info/run
index e92b873..b596886 100755
--- a/test/1922-owned-monitors-info/run
+++ b/test/1922-owned-monitors-info/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1923-frame-pop/run b/test/1923-frame-pop/run
index 51875a7..ce3a55a 100755
--- a/test/1923-frame-pop/run
+++ b/test/1923-frame-pop/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1924-frame-pop-toggle/run b/test/1924-frame-pop-toggle/run
index 51875a7..ce3a55a 100755
--- a/test/1924-frame-pop-toggle/run
+++ b/test/1924-frame-pop-toggle/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1925-self-frame-pop/run b/test/1925-self-frame-pop/run
index 51875a7..ce3a55a 100755
--- a/test/1925-self-frame-pop/run
+++ b/test/1925-self-frame-pop/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1926-missed-frame-pop/run b/test/1926-missed-frame-pop/run
index 51875a7..ce3a55a 100755
--- a/test/1926-missed-frame-pop/run
+++ b/test/1926-missed-frame-pop/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1927-exception-event/run b/test/1927-exception-event/run
index 51875a7..ce3a55a 100755
--- a/test/1927-exception-event/run
+++ b/test/1927-exception-event/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1928-exception-event-exception/run b/test/1928-exception-event-exception/run
index 51875a7..ce3a55a 100755
--- a/test/1928-exception-event-exception/run
+++ b/test/1928-exception-event-exception/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1929-exception-catch-exception/run b/test/1929-exception-catch-exception/run
index 51875a7..ce3a55a 100755
--- a/test/1929-exception-catch-exception/run
+++ b/test/1929-exception-catch-exception/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1930-monitor-info/run b/test/1930-monitor-info/run
index e92b873..b596886 100755
--- a/test/1930-monitor-info/run
+++ b/test/1930-monitor-info/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1931-monitor-events/run b/test/1931-monitor-events/run
index e92b873..b596886 100755
--- a/test/1931-monitor-events/run
+++ b/test/1931-monitor-events/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1932-monitor-events-misc/run b/test/1932-monitor-events-misc/run
index e92b873..b596886 100755
--- a/test/1932-monitor-events-misc/run
+++ b/test/1932-monitor-events-misc/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1933-monitor-current-contended/run b/test/1933-monitor-current-contended/run
index e92b873..b596886 100755
--- a/test/1933-monitor-current-contended/run
+++ b/test/1933-monitor-current-contended/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1934-jvmti-signal-thread/run b/test/1934-jvmti-signal-thread/run
index e92b873..b596886 100755
--- a/test/1934-jvmti-signal-thread/run
+++ b/test/1934-jvmti-signal-thread/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1935-get-set-current-frame-jit/run b/test/1935-get-set-current-frame-jit/run
index e569d08..fb36023 100755
--- a/test/1935-get-set-current-frame-jit/run
+++ b/test/1935-get-set-current-frame-jit/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ensure the test is not subject to code collection
-./default-run "$@" --jvmti --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # Ensure the test is not subject to code collection
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xjitinitialsize:32M"])
diff --git a/test/1936-thread-end-events/run b/test/1936-thread-end-events/run
index 51875a7..ce3a55a 100755
--- a/test/1936-thread-end-events/run
+++ b/test/1936-thread-end-events/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1937-transform-soft-fail/run b/test/1937-transform-soft-fail/run
index c6e62ae..4796039 100755
--- a/test/1937-transform-soft-fail/run
+++ b/test/1937-transform-soft-fail/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1938-transform-abstract-single-impl/run b/test/1938-transform-abstract-single-impl/run
index adb1a1c..23b8dc3 100755
--- a/test/1938-transform-abstract-single-impl/run
+++ b/test/1938-transform-abstract-single-impl/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/1939-proxy-frames/run b/test/1939-proxy-frames/run
index 51875a7..ce3a55a 100755
--- a/test/1939-proxy-frames/run
+++ b/test/1939-proxy-frames/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1940-ddms-ext/run b/test/1940-ddms-ext/run
index c6e62ae..4796039 100755
--- a/test/1940-ddms-ext/run
+++ b/test/1940-ddms-ext/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1941-dispose-stress/run b/test/1941-dispose-stress/run
index 51875a7..ce3a55a 100755
--- a/test/1941-dispose-stress/run
+++ b/test/1941-dispose-stress/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1942-suspend-raw-monitor-exit/run b/test/1942-suspend-raw-monitor-exit/run
index e92b873..b596886 100755
--- a/test/1942-suspend-raw-monitor-exit/run
+++ b/test/1942-suspend-raw-monitor-exit/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1943-suspend-raw-monitor-wait/run b/test/1943-suspend-raw-monitor-wait/run
index e92b873..b596886 100755
--- a/test/1943-suspend-raw-monitor-wait/run
+++ b/test/1943-suspend-raw-monitor-wait/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1946-list-descriptors/run b/test/1946-list-descriptors/run
index c6e62ae..4796039 100755
--- a/test/1946-list-descriptors/run
+++ b/test/1946-list-descriptors/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1947-breakpoint-redefine-deopt/run b/test/1947-breakpoint-redefine-deopt/run
index 51875a7..ce3a55a 100755
--- a/test/1947-breakpoint-redefine-deopt/run
+++ b/test/1947-breakpoint-redefine-deopt/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1948-obsolete-const-method-handle/run b/test/1948-obsolete-const-method-handle/run
index 9eacb4c..afb1374 100755
--- a/test/1948-obsolete-const-method-handle/run
+++ b/test/1948-obsolete-const-method-handle/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Squash the exit status and put it in expected
-./default-run --jvmti "$@"
+
+def run(ctx, args):
+  # Squash the exit status and put it in expected
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1949-short-dex-file/run b/test/1949-short-dex-file/run
index c6e62ae..4796039 100755
--- a/test/1949-short-dex-file/run
+++ b/test/1949-short-dex-file/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1950-unprepared-transform/run b/test/1950-unprepared-transform/run
index adb1a1c..23b8dc3 100755
--- a/test/1950-unprepared-transform/run
+++ b/test/1950-unprepared-transform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/1951-monitor-enter-no-suspend/run b/test/1951-monitor-enter-no-suspend/run
index c6e62ae..4796039 100755
--- a/test/1951-monitor-enter-no-suspend/run
+++ b/test/1951-monitor-enter-no-suspend/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1953-pop-frame/run b/test/1953-pop-frame/run
index d16d4e6..fa08256 100755
--- a/test/1953-pop-frame/run
+++ b/test/1953-pop-frame/run
@@ -14,11 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-./default-run "$@" --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  ctx.default_run(args, jvmti=True, test_args=test_args)
diff --git a/test/1954-pop-frame-jit/run b/test/1954-pop-frame-jit/run
index d16d4e6..fa08256 100755
--- a/test/1954-pop-frame-jit/run
+++ b/test/1954-pop-frame-jit/run
@@ -14,11 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-./default-run "$@" --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  ctx.default_run(args, jvmti=True, test_args=test_args)
diff --git a/test/1955-pop-frame-jit-called/run b/test/1955-pop-frame-jit-called/run
index 2984461..5814991 100755
--- a/test/1955-pop-frame-jit-called/run
+++ b/test/1955-pop-frame-jit-called/run
@@ -14,13 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-# The jitthreshold prevents the jit from compiling anything except those which
-# we explicitly request.
-./default-run "$@" --android-runtime-option -Xjitthreshold:1000 --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  # The jitthreshold prevents the jit from compiling anything except those which
+  # we explicitly request.
+  ctx.default_run(
+      args,
+      android_runtime_option=["-Xjitthreshold:1000"],
+      jvmti=True,
+      test_args=test_args)
diff --git a/test/1956-pop-frame-jit-calling/run b/test/1956-pop-frame-jit-calling/run
index 2984461..5814991 100755
--- a/test/1956-pop-frame-jit-calling/run
+++ b/test/1956-pop-frame-jit-calling/run
@@ -14,13 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-# The jitthreshold prevents the jit from compiling anything except those which
-# we explicitly request.
-./default-run "$@" --android-runtime-option -Xjitthreshold:1000 --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  # The jitthreshold prevents the jit from compiling anything except those which
+  # we explicitly request.
+  ctx.default_run(
+      args,
+      android_runtime_option=["-Xjitthreshold:1000"],
+      jvmti=True,
+      test_args=test_args)
diff --git a/test/1957-error-ext/run b/test/1957-error-ext/run
index 8be0ed4..4796039 100755
--- a/test/1957-error-ext/run
+++ b/test/1957-error-ext/run
@@ -15,4 +15,5 @@
 # limitations under the License.
 
 
-./default-run "$@" --jvmti
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1958-transform-try-jit/run b/test/1958-transform-try-jit/run
index c6e62ae..4796039 100755
--- a/test/1958-transform-try-jit/run
+++ b/test/1958-transform-try-jit/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1959-redefine-object-instrument/run b/test/1959-redefine-object-instrument/run
index c6e62ae..4796039 100755
--- a/test/1959-redefine-object-instrument/run
+++ b/test/1959-redefine-object-instrument/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1960-obsolete-jit-multithread-native/run b/test/1960-obsolete-jit-multithread-native/run
index c6e62ae..4796039 100755
--- a/test/1960-obsolete-jit-multithread-native/run
+++ b/test/1960-obsolete-jit-multithread-native/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1961-obsolete-jit-multithread/run b/test/1961-obsolete-jit-multithread/run
index c6e62ae..4796039 100755
--- a/test/1961-obsolete-jit-multithread/run
+++ b/test/1961-obsolete-jit-multithread/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1962-multi-thread-events/run b/test/1962-multi-thread-events/run
index c6e62ae..4796039 100755
--- a/test/1962-multi-thread-events/run
+++ b/test/1962-multi-thread-events/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1963-add-to-dex-classloader-in-memory/run b/test/1963-add-to-dex-classloader-in-memory/run
index c6e62ae..4796039 100755
--- a/test/1963-add-to-dex-classloader-in-memory/run
+++ b/test/1963-add-to-dex-classloader-in-memory/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1964-add-to-dex-classloader-file/run b/test/1964-add-to-dex-classloader-file/run
index c6e62ae..4796039 100755
--- a/test/1964-add-to-dex-classloader-file/run
+++ b/test/1964-add-to-dex-classloader-file/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1965-get-set-local-primitive-no-tables/run b/test/1965-get-set-local-primitive-no-tables/run
index 9b741ee..77a45e9 100755
--- a/test/1965-get-set-local-primitive-no-tables/run
+++ b/test/1965-get-set-local-primitive-no-tables/run
@@ -14,8 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# If we compile the .oat files non-debuggable we could end up with dex2dex running over the files
-# which will cause some instructions to be removed from smali/TestCases1966.smali. This test relies
-# on the instructions being exactly as written so pass --debuggable to 'dex2oat' only to prevent
-# this from happening.
-./default-run "$@" --jvmti --compiler-only-option --debuggable
+
+def run(ctx, args):
+  # If we compile the .oat files non-debuggable we could end up with dex2dex running over the files
+  # which will cause some instructions to be removed from smali/TestCases1966.smali. This test relies
+  # on the instructions being exactly as written so pass --debuggable to 'dex2oat' only to prevent
+  # this from happening.
+  ctx.default_run(args, jvmti=True, compiler_only_option=["--debuggable"])
diff --git a/test/1966-get-set-local-objects-no-table/run b/test/1966-get-set-local-objects-no-table/run
index 9b741ee..77a45e9 100755
--- a/test/1966-get-set-local-objects-no-table/run
+++ b/test/1966-get-set-local-objects-no-table/run
@@ -14,8 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# If we compile the .oat files non-debuggable we could end up with dex2dex running over the files
-# which will cause some instructions to be removed from smali/TestCases1966.smali. This test relies
-# on the instructions being exactly as written so pass --debuggable to 'dex2oat' only to prevent
-# this from happening.
-./default-run "$@" --jvmti --compiler-only-option --debuggable
+
+def run(ctx, args):
+  # If we compile the .oat files non-debuggable we could end up with dex2dex running over the files
+  # which will cause some instructions to be removed from smali/TestCases1966.smali. This test relies
+  # on the instructions being exactly as written so pass --debuggable to 'dex2oat' only to prevent
+  # this from happening.
+  ctx.default_run(args, jvmti=True, compiler_only_option=["--debuggable"])
diff --git a/test/1967-get-set-local-bad-slot/run b/test/1967-get-set-local-bad-slot/run
index 51875a7..ce3a55a 100755
--- a/test/1967-get-set-local-bad-slot/run
+++ b/test/1967-get-set-local-bad-slot/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1968-force-early-return/run b/test/1968-force-early-return/run
index d16d4e6..fa08256 100755
--- a/test/1968-force-early-return/run
+++ b/test/1968-force-early-return/run
@@ -14,11 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-./default-run "$@" --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  ctx.default_run(args, jvmti=True, test_args=test_args)
diff --git a/test/1969-force-early-return-void/run b/test/1969-force-early-return-void/run
index e92b873..b596886 100755
--- a/test/1969-force-early-return-void/run
+++ b/test/1969-force-early-return-void/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1970-force-early-return-long/run b/test/1970-force-early-return-long/run
index d16d4e6..fa08256 100755
--- a/test/1970-force-early-return-long/run
+++ b/test/1970-force-early-return-long/run
@@ -14,11 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-./default-run "$@" --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  ctx.default_run(args, jvmti=True, test_args=test_args)
diff --git a/test/1971-multi-force-early-return/run b/test/1971-multi-force-early-return/run
index d16d4e6..fa08256 100755
--- a/test/1971-multi-force-early-return/run
+++ b/test/1971-multi-force-early-return/run
@@ -14,11 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# On RI we need to turn class-load tests off since those events are buggy around
-# pop-frame (see b/116003018).
-ARGS=""
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  ARGS="--args DISABLE_CLASS_LOAD_TESTS"
-fi
 
-./default-run "$@" --jvmti $ARGS
+def run(ctx, args):
+  # On RI we need to turn class-load tests off since those events are buggy around
+  # pop-frame (see b/116003018).
+  test_args = ["DISABLE_CLASS_LOAD_TESTS"] if args.jvm else []
+
+  ctx.default_run(args, jvmti=True, test_args=test_args)
diff --git a/test/1972-jni-id-swap-indices/run b/test/1972-jni-id-swap-indices/run
index 999b92a..81ec061 100755
--- a/test/1972-jni-id-swap-indices/run
+++ b/test/1972-jni-id-swap-indices/run
@@ -14,6 +14,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-args=$(echo "$@" | sed  's/--runtime-option -Xopaque-jni-ids\:true//g')
 
-./default-run $args --android-runtime-option -Xopaque-jni-ids:swapable --android-runtime-option -Xauto-promote-opaque-jni-ids:false
\ No newline at end of file
+def run(ctx, args):
+  for i, opt in enumerate(args.runtime_option):
+    if opt == '-Xopaque-jni-ids:true':
+      args.runtime_option.pop(i)
+      break
+
+  ctx.default_run(
+      args,
+      android_runtime_option=[
+          '-Xopaque-jni-ids:swapable', '-Xauto-promote-opaque-jni-ids:false'
+      ])
diff --git a/test/1973-jni-id-swap-pointer/run b/test/1973-jni-id-swap-pointer/run
index 999b92a..b7ed57a 100755
--- a/test/1973-jni-id-swap-pointer/run
+++ b/test/1973-jni-id-swap-pointer/run
@@ -14,6 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-args=$(echo "$@" | sed  's/--runtime-option -Xopaque-jni-ids\:true//g')
 
-./default-run $args --android-runtime-option -Xopaque-jni-ids:swapable --android-runtime-option -Xauto-promote-opaque-jni-ids:false
\ No newline at end of file
+def run(ctx, args):
+  for i, opt in enumerate(args.runtime_option):
+    if opt == '-Xopaque-jni-ids:true':
+      args.runtime_option.pop(i)
+
+  ctx.default_run(
+      args,
+      android_runtime_option=[
+          '-Xopaque-jni-ids:swapable', '-Xauto-promote-opaque-jni-ids:false'
+      ])
diff --git a/test/1974-resize-array/run b/test/1974-resize-array/run
index 96646c8..4f5f922 100755
--- a/test/1974-resize-array/run
+++ b/test/1974-resize-array/run
@@ -15,4 +15,5 @@
 # limitations under the License.
 
 
-./default-run "$@" --jvmti
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1975-hello-structural-transformation/run b/test/1975-hello-structural-transformation/run
index 03e41a5..9ef412d 100755
--- a/test/1975-hello-structural-transformation/run
+++ b/test/1975-hello-structural-transformation/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1976-hello-structural-static-methods/run b/test/1976-hello-structural-static-methods/run
index 03e41a5..9ef412d 100755
--- a/test/1976-hello-structural-static-methods/run
+++ b/test/1976-hello-structural-static-methods/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1977-hello-structural-obsolescence/run b/test/1977-hello-structural-obsolescence/run
index 03e41a5..9ef412d 100755
--- a/test/1977-hello-structural-obsolescence/run
+++ b/test/1977-hello-structural-obsolescence/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1978-regular-obsolete-then-structural-obsolescence/run b/test/1978-regular-obsolete-then-structural-obsolescence/run
index 03e41a5..9ef412d 100755
--- a/test/1978-regular-obsolete-then-structural-obsolescence/run
+++ b/test/1978-regular-obsolete-then-structural-obsolescence/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1979-threaded-structural-transformation/run b/test/1979-threaded-structural-transformation/run
index 03e41a5..9ef412d 100755
--- a/test/1979-threaded-structural-transformation/run
+++ b/test/1979-threaded-structural-transformation/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1980-obsolete-object-cleared/run b/test/1980-obsolete-object-cleared/run
index 03e41a5..9ef412d 100755
--- a/test/1980-obsolete-object-cleared/run
+++ b/test/1980-obsolete-object-cleared/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1981-structural-redef-private-method-handles/run b/test/1981-structural-redef-private-method-handles/run
index 03e41a5..9ef412d 100755
--- a/test/1981-structural-redef-private-method-handles/run
+++ b/test/1981-structural-redef-private-method-handles/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1982-no-virtuals-structural-redefinition/run b/test/1982-no-virtuals-structural-redefinition/run
index 03e41a5..9ef412d 100755
--- a/test/1982-no-virtuals-structural-redefinition/run
+++ b/test/1982-no-virtuals-structural-redefinition/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1983-structural-redefinition-failures/run b/test/1983-structural-redefinition-failures/run
index 03e41a5..9ef412d 100755
--- a/test/1983-structural-redefinition-failures/run
+++ b/test/1983-structural-redefinition-failures/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1984-structural-redefine-field-trace/run b/test/1984-structural-redefine-field-trace/run
index a36de16..69d8985 100755
--- a/test/1984-structural-redefine-field-trace/run
+++ b/test/1984-structural-redefine-field-trace/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti --android-runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(
+      args, jvmti=True, android_runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1985-structural-redefine-stack-scope/run b/test/1985-structural-redefine-stack-scope/run
index a36de16..69d8985 100755
--- a/test/1985-structural-redefine-stack-scope/run
+++ b/test/1985-structural-redefine-stack-scope/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti --android-runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(
+      args, jvmti=True, android_runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1986-structural-redefine-multi-thread-stack-scope/run b/test/1986-structural-redefine-multi-thread-stack-scope/run
index a36de16..69d8985 100755
--- a/test/1986-structural-redefine-multi-thread-stack-scope/run
+++ b/test/1986-structural-redefine-multi-thread-stack-scope/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti --android-runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(
+      args, jvmti=True, android_runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1987-structural-redefine-recursive-stack-scope/run b/test/1987-structural-redefine-recursive-stack-scope/run
index a36de16..69d8985 100755
--- a/test/1987-structural-redefine-recursive-stack-scope/run
+++ b/test/1987-structural-redefine-recursive-stack-scope/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti --android-runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(
+      args, jvmti=True, android_runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1988-multi-structural-redefine/run b/test/1988-multi-structural-redefine/run
index a36de16..69d8985 100755
--- a/test/1988-multi-structural-redefine/run
+++ b/test/1988-multi-structural-redefine/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti --android-runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(
+      args, jvmti=True, android_runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1989-transform-bad-monitor/run b/test/1989-transform-bad-monitor/run
index c6e62ae..4796039 100755
--- a/test/1989-transform-bad-monitor/run
+++ b/test/1989-transform-bad-monitor/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1990-structural-bad-verify/run b/test/1990-structural-bad-verify/run
index 03e41a5..9ef412d 100755
--- a/test/1990-structural-bad-verify/run
+++ b/test/1990-structural-bad-verify/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1991-hello-structural-retransform/run b/test/1991-hello-structural-retransform/run
index 03e41a5..9ef412d 100755
--- a/test/1991-hello-structural-retransform/run
+++ b/test/1991-hello-structural-retransform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1992-retransform-no-such-field/run b/test/1992-retransform-no-such-field/run
index c6e62ae..4796039 100755
--- a/test/1992-retransform-no-such-field/run
+++ b/test/1992-retransform-no-such-field/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/1993-fallback-non-structural/run b/test/1993-fallback-non-structural/run
index 03e41a5..9ef412d 100755
--- a/test/1993-fallback-non-structural/run
+++ b/test/1993-fallback-non-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1994-final-virtual-structural/run b/test/1994-final-virtual-structural/run
index 03e41a5..9ef412d 100755
--- a/test/1994-final-virtual-structural/run
+++ b/test/1994-final-virtual-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1995-final-virtual-structural-multithread/run b/test/1995-final-virtual-structural-multithread/run
index e912529..3fd3061 100755
--- a/test/1995-final-virtual-structural-multithread/run
+++ b/test/1995-final-virtual-structural-multithread/run
@@ -14,8 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# TODO(b/144168550) This test uses access patterns that can be replaced by
-# iget-object-quick during dex2dex compilation. This breaks the test since the
-# -quick opcode encodes the exact byte offset of fields. Since this test changes
-# the offset this causes problems.
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  # TODO(b/144168550) This test uses access patterns that can be replaced by
+  # iget-object-quick during dex2dex compilation. This breaks the test since the
+  # -quick opcode encodes the exact byte offset of fields. Since this test changes
+  # the offset this causes problems.
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1996-final-override-virtual-structural/run b/test/1996-final-override-virtual-structural/run
index 03e41a5..9ef412d 100755
--- a/test/1996-final-override-virtual-structural/run
+++ b/test/1996-final-override-virtual-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1997-structural-shadow-method/run b/test/1997-structural-shadow-method/run
index 03e41a5..9ef412d 100755
--- a/test/1997-structural-shadow-method/run
+++ b/test/1997-structural-shadow-method/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1998-structural-shadow-field/run b/test/1998-structural-shadow-field/run
index 03e41a5..9ef412d 100755
--- a/test/1998-structural-shadow-field/run
+++ b/test/1998-structural-shadow-field/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/1999-virtual-structural/run b/test/1999-virtual-structural/run
index 03e41a5..9ef412d 100755
--- a/test/1999-virtual-structural/run
+++ b/test/1999-virtual-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2000-virtual-list-structural/run b/test/2000-virtual-list-structural/run
index 03e41a5..9ef412d 100755
--- a/test/2000-virtual-list-structural/run
+++ b/test/2000-virtual-list-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2001-virtual-structural-multithread/run b/test/2001-virtual-structural-multithread/run
index 03e41a5..9ef412d 100755
--- a/test/2001-virtual-structural-multithread/run
+++ b/test/2001-virtual-structural-multithread/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2002-virtual-structural-initializing/run b/test/2002-virtual-structural-initializing/run
index 03e41a5..9ef412d 100755
--- a/test/2002-virtual-structural-initializing/run
+++ b/test/2002-virtual-structural-initializing/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2003-double-virtual-structural/run b/test/2003-double-virtual-structural/run
index b59f97c..882774b 100755
--- a/test/2003-double-virtual-structural/run
+++ b/test/2003-double-virtual-structural/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2004-double-virtual-structural-abstract/run b/test/2004-double-virtual-structural-abstract/run
index b59f97c..882774b 100755
--- a/test/2004-double-virtual-structural-abstract/run
+++ b/test/2004-double-virtual-structural-abstract/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2005-pause-all-redefine-multithreaded/run b/test/2005-pause-all-redefine-multithreaded/run
index b59f97c..882774b 100755
--- a/test/2005-pause-all-redefine-multithreaded/run
+++ b/test/2005-pause-all-redefine-multithreaded/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2006-virtual-structural-finalizing/run b/test/2006-virtual-structural-finalizing/run
index 03e41a5..9ef412d 100755
--- a/test/2006-virtual-structural-finalizing/run
+++ b/test/2006-virtual-structural-finalizing/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2007-virtual-structural-finalizable/run b/test/2007-virtual-structural-finalizable/run
index 03e41a5..9ef412d 100755
--- a/test/2007-virtual-structural-finalizable/run
+++ b/test/2007-virtual-structural-finalizable/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2008-redefine-then-old-reflect-field/run b/test/2008-redefine-then-old-reflect-field/run
index c6e62ae..4796039 100755
--- a/test/2008-redefine-then-old-reflect-field/run
+++ b/test/2008-redefine-then-old-reflect-field/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/2009-structural-local-ref/run b/test/2009-structural-local-ref/run
index 03e41a5..9ef412d 100755
--- a/test/2009-structural-local-ref/run
+++ b/test/2009-structural-local-ref/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2012-structural-redefinition-failures-jni-id/run b/test/2012-structural-redefinition-failures-jni-id/run
index 03e41a5..9ef412d 100755
--- a/test/2012-structural-redefinition-failures-jni-id/run
+++ b/test/2012-structural-redefinition-failures-jni-id/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2031-zygote-compiled-frame-deopt/run b/test/2031-zygote-compiled-frame-deopt/run
index 900099f..1dedf3b 100755
--- a/test/2031-zygote-compiled-frame-deopt/run
+++ b/test/2031-zygote-compiled-frame-deopt/run
@@ -14,8 +14,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The -Xopaque-jni-ids makes sure we can do structural redefinition. The --add-libdir-argument tells
-# default-run to pass the directory where the jvmti-agent is so we can load it later. The others
-# set the process to zygote mode and setup the jit cache size. We use a larger than normal jit-size
-# to avoid having to deal with jit-gc, a complication that's not relevant to this test.
-./default-run "$@" --runtime-option -Xopaque-jni-ids:true --add-libdir-argument --runtime-option -Xzygote --runtime-option -Xjitinitialsize:64M
+
+def run(ctx, args):
+  # The -Xopaque-jni-ids makes sure we can do structural redefinition. The --add-libdir-argument tells
+  # default-run to pass the directory where the jvmti-agent is so we can load it later. The others
+  # set the process to zygote mode and setup the jit cache size. We use a larger than normal jit-size
+  # to avoid having to deal with jit-gc, a complication that's not relevant to this test.
+  ctx.default_run(
+      args,
+      runtime_option=[
+          "-Xopaque-jni-ids:true", "-Xzygote", "-Xjitinitialsize:64M"
+      ],
+      add_libdir_argument=True)
diff --git a/test/etc/default-run b/test/2033-shutdown-mechanics/run
similarity index 77%
copy from test/etc/default-run
copy to test/2033-shutdown-mechanics/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/2033-shutdown-mechanics/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/2035-structural-native-method/run b/test/2035-structural-native-method/run
index ff387ff..3774a6c 100755
--- a/test/2035-structural-native-method/run
+++ b/test/2035-structural-native-method/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2036-structural-subclass-shadow/run b/test/2036-structural-subclass-shadow/run
index ff387ff..3774a6c 100755
--- a/test/2036-structural-subclass-shadow/run
+++ b/test/2036-structural-subclass-shadow/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --runtime-option -Xopaque-jni-ids:true
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, runtime_option=["-Xopaque-jni-ids:true"])
diff --git a/test/2038-hiddenapi-jvmti-ext/run b/test/2038-hiddenapi-jvmti-ext/run
index c6e62ae..4796039 100755
--- a/test/2038-hiddenapi-jvmti-ext/run
+++ b/test/2038-hiddenapi-jvmti-ext/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/2039-load-transform-larger/run b/test/2039-load-transform-larger/run
index 144c17d..157a24b 100755
--- a/test/2039-load-transform-larger/run
+++ b/test/2039-load-transform-larger/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/2041-bad-cleaner/run b/test/2041-bad-cleaner/run
index 54747ee..1709af5 100755
--- a/test/2041-bad-cleaner/run
+++ b/test/2041-bad-cleaner/run
@@ -14,9 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# The test logs error messages which is expected, discard them.
-export ANDROID_LOG_TAGS='*:f'
 
-# Squash the exit status and put it in expected
-./default-run --external-log-tags "${@}"
-echo "exit status:" $?
+def run(ctx, args):
+  # The test logs error messages which is expected, discard them.
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+
+  # Squash the exit status and put it in expected
+  ctx.default_run(args, external_log_tags=True, expected_exit_code=2)
diff --git a/test/2230-profile-save-hotness/run b/test/2230-profile-save-hotness/run
index 5ce577d..526b841 100644
--- a/test/2230-profile-save-hotness/run
+++ b/test/2230-profile-save-hotness/run
@@ -13,9 +13,13 @@
 # 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.
-${RUN} "${@}" \
-  -Xcompiler-option --count-hotness-in-compiled-code \
-  -Xcompiler-option --compiler-filter=speed \
-  --runtime-option -Xps-profile-aot-code \
-  --runtime-option -Xjitsaveprofilinginfo \
-  --runtime-option -Xusejit:true
+def run(ctx, args):
+  ctx.default_run(
+      args,
+      Xcompiler_option=[
+          "--count-hotness-in-compiled-code", "--compiler-filter=speed"
+      ],
+      runtime_option=[
+          "-Xps-profile-aot-code", "-Xjitsaveprofilinginfo", "-Xusejit:true"
+      ],
+  )
diff --git a/test/2232-write-metrics-to-log/run b/test/2232-write-metrics-to-log/run
index d34ec6c..11c49aa 100755
--- a/test/2232-write-metrics-to-log/run
+++ b/test/2232-write-metrics-to-log/run
@@ -14,5 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-export ANDROID_LOG_TAGS="*:i"
-exec ${RUN} $@ --external-log-tags --runtime-option -Xmetrics-write-to-logcat:true --runtime-option -Xmetrics-reporting-mods:100
+
+def run(ctx, args):
+  ctx.env.ANDROID_LOG_TAGS = "*:i"
+  ctx.default_run(
+      args,
+      external_log_tags=True,
+      runtime_option=[
+          "-Xmetrics-write-to-logcat:true", "-Xmetrics-reporting-mods:100"
+      ])
diff --git a/test/2238-checker-polymorphic-recursive-inlining/run b/test/2238-checker-polymorphic-recursive-inlining/run
index a4e2692..b85f926 100644
--- a/test/2238-checker-polymorphic-recursive-inlining/run
+++ b/test/2238-checker-polymorphic-recursive-inlining/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use a profile to put specific classes in the app image to trigger polymorphic inlining.
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  # Use a profile to put specific classes in the app image to trigger polymorphic inlining.
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/etc/default-run b/test/2239-varhandle-perf/run
similarity index 77%
copy from test/etc/default-run
copy to test/2239-varhandle-perf/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/2239-varhandle-perf/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/2240-tracing-non-invokable-method/run b/test/2240-tracing-non-invokable-method/run
index 1f7edfc..ac17e68 100755
--- a/test/2240-tracing-non-invokable-method/run
+++ b/test/2240-tracing-non-invokable-method/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --runtime-option -Xmethod-trace --runtime-option -Xmethod-trace-file:/dev/null
+
+def run(ctx, args):
+  ctx.default_run(
+      args, runtime_option=["-Xmethod-trace", "-Xmethod-trace-file:/dev/null"])
diff --git a/test/304-method-tracing/run b/test/304-method-tracing/run
index 7bd1895..2fb72d3 100755
--- a/test/304-method-tracing/run
+++ b/test/304-method-tracing/run
@@ -14,5 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Runs the test with method tracing enabled.
-exec ${RUN} "$@" --runtime-option -Xmethod-trace --runtime-option -Xmethod-trace-file:${DEX_LOCATION}/trace.bin
+
+def run(ctx, args):
+  # Runs the test with method tracing enabled.
+  ctx.default_run(
+      args,
+      runtime_option=[
+          "-Xmethod-trace", "-Xmethod-trace-file:${DEX_LOCATION}/trace.bin"
+      ])
diff --git a/test/457-regs/run b/test/457-regs/run
index 2591855..7c18068 100644
--- a/test/457-regs/run
+++ b/test/457-regs/run
@@ -14,13 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-${RUN} "$@"
-return_status1=$?
 
-# Force baseline JIT compilation as the test explicitly requests JIT
-# compilation, which by default is 'optimizing'.
-${RUN} "$@" -Xcompiler-option --baseline
-return_status2=$?
+def run(ctx, args):
+  ctx.default_run(args)
 
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2)
+  # Force baseline JIT compilation as the test explicitly requests JIT
+  # compilation, which by default is 'optimizing'.
+  ctx.default_run(args, Xcompiler_option=["--baseline"])
diff --git a/test/566-polymorphic-inlining/run b/test/566-polymorphic-inlining/run
index 6e51cb1..0bfd5dc 100644
--- a/test/566-polymorphic-inlining/run
+++ b/test/566-polymorphic-inlining/run
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# -Xjitinitialsize:32M to prevent profiling info creation failure.
-exec ${RUN} "${@}" \
-  --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # -Xjitinitialsize:32M to prevent profiling info creation failure.
+  ctx.default_run(args, runtime_option=["-Xjitinitialsize:32M"])
diff --git a/test/569-checker-pattern-replacement/run b/test/569-checker-pattern-replacement/run
index 8ab6527..d763e54 100755
--- a/test/569-checker-pattern-replacement/run
+++ b/test/569-checker-pattern-replacement/run
@@ -14,5 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@" \
-    -Xcompiler-option --no-inline-from="core-oj,569-checker-pattern-replacement.jar!classes2.dex"
+
+def run(ctx, args):
+  ctx.default_run(
+      args,
+      Xcompiler_option=[
+          "--no-inline-from=core-oj,569-checker-pattern-replacement.jar!classes2.dex"
+      ])
diff --git a/test/570-checker-osr-locals/run b/test/570-checker-osr-locals/run
index 6cef13f..d876de2 100755
--- a/test/570-checker-osr-locals/run
+++ b/test/570-checker-osr-locals/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ensure this test is not subject to code collection.
-exec ${RUN} "$@" --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # Ensure this test is not subject to code collection.
+  ctx.default_run(args, runtime_option=["-Xjitinitialsize:32M"])
diff --git a/test/570-checker-osr/run b/test/570-checker-osr/run
index 24d69b4..8109c62 100755
--- a/test/570-checker-osr/run
+++ b/test/570-checker-osr/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ensure this test is not subject to code collection.
-exec ${RUN} "$@" --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # Ensure this test is not subject to code collection.
+  ctx.default_run(args, runtime_option=["-Xjitinitialsize:32M"])
diff --git a/test/574-irreducible-and-constant-area/run b/test/574-irreducible-and-constant-area/run
index ffdbcc9..3c04d9e 100755
--- a/test/574-irreducible-and-constant-area/run
+++ b/test/574-irreducible-and-constant-area/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Don't do relocation, as this affects this test.
-exec ${RUN} "$@" --no-relocate
+
+def run(ctx, args):
+  # Don't do relocation, as this affects this test.
+  ctx.default_run(args, relocate=False)
diff --git a/test/595-profile-saving/run b/test/595-profile-saving/run
index 03d3046..96de281 100644
--- a/test/595-profile-saving/run
+++ b/test/595-profile-saving/run
@@ -14,16 +14,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Use
-# --compiler-filter=quicken to make sure that the test is not compiled AOT
-# and to make sure the test is not compiled  when loaded (by PathClassLoader)
-# -Xjitsaveprofilinginfo to enable profile saving
-# -Xusejit:false to disable jit and only test profiles.
-# -Xjitinitialsize:32M to prevent profiling info creation failure.
-exec ${RUN} "${@}" \
-  -Xcompiler-option --compiler-filter=quicken \
-  --runtime-option '-Xcompiler-option --compiler-filter=quicken' \
-  --runtime-option -Xjitinitialsize:32M \
-  --runtime-option -Xjitsaveprofilinginfo \
-  --runtime-option -Xusejit:false \
-  --runtime-option -Xps-profile-boot-class-path
+
+def run(ctx, args):
+  # Use
+  # --compiler-filter=quicken to make sure that the test is not compiled AOT
+  # and to make sure the test is not compiled  when loaded (by PathClassLoader)
+  # -Xjitsaveprofilinginfo to enable profile saving
+  # -Xusejit:false to disable jit and only test profiles.
+  # -Xjitinitialsize:32M to prevent profiling info creation failure.
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--compiler-filter=quicken"],
+      runtime_option=[
+          "-Xcompiler-option --compiler-filter=quicken",
+          "-Xjitinitialsize:32M",
+          "-Xjitsaveprofilinginfo",
+          "-Xusejit:false",
+          "-Xps-profile-boot-class-path",
+      ])
diff --git a/test/596-app-images/run b/test/596-app-images/run
index dbdcd1c..402fbb5 100644
--- a/test/596-app-images/run
+++ b/test/596-app-images/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# We need a profile to tell dex2oat to include classes in the final app image
-exec ${RUN} --profile $@
+
+def run(ctx, args):
+  # We need a profile to tell dex2oat to include classes in the final app image
+  ctx.default_run(args, profile=True)
diff --git a/test/597-app-images-same-classloader/run b/test/597-app-images-same-classloader/run
index 496273f..a335777 100644
--- a/test/597-app-images-same-classloader/run
+++ b/test/597-app-images-same-classloader/run
@@ -14,5 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# We need a profile to tell dex2oat to include classes in the final app image
-exec ${RUN} --profile --secondary-class-loader-context "PCL[$DEX_LOCATION/$TEST_NAME.jar]" $@
+
+def run(ctx, args):
+  # We need a profile to tell dex2oat to include classes in the final app image
+  pcl = f"PCL[{ctx.env.DEX_LOCATION}/{ctx.env.TEST_NAME}.jar]"
+  ctx.default_run(args, profile=True, secondary_class_loader_context=pcl)
diff --git a/test/597-deopt-busy-loop/run b/test/597-deopt-busy-loop/run
index bc04498..6107702 100644
--- a/test/597-deopt-busy-loop/run
+++ b/test/597-deopt-busy-loop/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# We want to run in debuggable mode and compiled.
-exec ${RUN} --jit -Xcompiler-option --debuggable "${@}"
+
+def run(ctx, args):
+  # We want to run in debuggable mode and compiled.
+  ctx.default_run(args, jit=True, Xcompiler_option=["--debuggable"])
diff --git a/test/597-deopt-invoke-stub/run b/test/597-deopt-invoke-stub/run
index 990c30e..b52c177 100644
--- a/test/597-deopt-invoke-stub/run
+++ b/test/597-deopt-invoke-stub/run
@@ -14,8 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# In order to test deoptimizing at quick-to-interpreter bridge,
-# we want to run in debuggable mode with jit compilation.
-# We also bump up the jit threshold to 10000 to make sure that the method
-# that should be interpreted is not compiled.
-exec ${RUN} "${@}" --jit --runtime-option -Xjitthreshold:10000 -Xcompiler-option --debuggable
+
+def run(ctx, args):
+  # In order to test deoptimizing at quick-to-interpreter bridge,
+  # we want to run in debuggable mode with jit compilation.
+  # We also bump up the jit threshold to 10000 to make sure that the method
+  # that should be interpreted is not compiled.
+  ctx.default_run(
+      args,
+      jit=True,
+      runtime_option=["-Xjitthreshold:10000"],
+      Xcompiler_option=["--debuggable"])
diff --git a/test/597-deopt-new-string/run b/test/597-deopt-new-string/run
index 9776ab3..fa958dc 100644
--- a/test/597-deopt-new-string/run
+++ b/test/597-deopt-new-string/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# We want to run in debuggable mode which keeps the call into StringFactory.newEmptyString().
-exec ${RUN} -Xcompiler-option --debuggable "${@}"
+
+def run(ctx, args):
+  # We want to run in debuggable mode which keeps the call into StringFactory.newEmptyString().
+  ctx.default_run(args, Xcompiler_option=["--debuggable"])
diff --git a/test/613-inlining-dex-cache/run b/test/613-inlining-dex-cache/run
index 9c1e7aa..f0b061b 100644
--- a/test/613-inlining-dex-cache/run
+++ b/test/613-inlining-dex-cache/run
@@ -14,7 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-flags="$@"
-# We need the dex files pre-verified to avoid running the verifier
-# at runtime which will update the dex cache.
-exec ${RUN} ${flags/verify-at-runtime/interpret-only}
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/616-cha-abstract/run b/test/616-cha-abstract/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-abstract/run
+++ b/test/616-cha-abstract/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha-interface-default/run b/test/616-cha-interface-default/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-interface-default/run
+++ b/test/616-cha-interface-default/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha-interface/run b/test/616-cha-interface/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-interface/run
+++ b/test/616-cha-interface/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha-miranda/run b/test/616-cha-miranda/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-miranda/run
+++ b/test/616-cha-miranda/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha-proxy-method-inline/run b/test/616-cha-proxy-method-inline/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-proxy-method-inline/run
+++ b/test/616-cha-proxy-method-inline/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha-unloading/run b/test/616-cha-unloading/run
index d8b4f0d..1e797a8 100644
--- a/test/616-cha-unloading/run
+++ b/test/616-cha-unloading/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/616-cha/run b/test/616-cha/run
index 9c64c7d..38164a3 100644
--- a/test/616-cha/run
+++ b/test/616-cha/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run without an app image to prevent the classes to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the classes to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/628-vdex/run b/test/628-vdex/run
index bf0ac91..066152c 100644
--- a/test/628-vdex/run
+++ b/test/628-vdex/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} -Xcompiler-option --compiler-filter=verify --vdex "${@}"
+
+def run(ctx, args):
+  ctx.default_run(
+      args, Xcompiler_option=["--compiler-filter=verify"], vdex=True)
diff --git a/test/629-vdex-speed/run b/test/629-vdex-speed/run
index 1477e3d..1d4f199 100644
--- a/test/629-vdex-speed/run
+++ b/test/629-vdex-speed/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} --vdex --vdex-filter speed "${@}"
+
+def run(ctx, args):
+  ctx.default_run(args, vdex=True, vdex_filter="speed")
diff --git a/test/634-vdex-duplicate/run b/test/634-vdex-duplicate/run
index 571ccd9..c99522d 100644
--- a/test/634-vdex-duplicate/run
+++ b/test/634-vdex-duplicate/run
@@ -14,4 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} -Xcompiler-option --compiler-filter=verify --vdex-filter speed --vdex "${@}"
+
+def run(ctx, args):
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--compiler-filter=verify"],
+      vdex_filter="speed",
+      vdex=True)
diff --git a/test/636-wrong-static-access/run b/test/636-wrong-static-access/run
index 5e99920..70a7fca 100755
--- a/test/636-wrong-static-access/run
+++ b/test/636-wrong-static-access/run
@@ -14,7 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Make verification soft fail, to ensure the verifier does not flag
-# the method we want to compile as "non-compilable" because it sees
-# the method will throw IncompatibleClassChangeError.
-exec ${RUN} $@ --verify-soft-fail
+
+def run(ctx, args):
+  # Make verification soft fail, to ensure the verifier does not flag
+  # the method we want to compile as "non-compilable" because it sees
+  # the method will throw IncompatibleClassChangeError.
+  ctx.default_run(args, verify_soft_fail=True)
diff --git a/test/638-checker-inline-cache-intrinsic/run b/test/638-checker-inline-cache-intrinsic/run
index 941ac9e..48f0f3f 100644
--- a/test/638-checker-inline-cache-intrinsic/run
+++ b/test/638-checker-inline-cache-intrinsic/run
@@ -14,11 +14,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Set threshold to 1000 to match the iterations done in the test.
-# Pass --verbose-methods to only generate the CFG of these methods.
-# The test is for JIT, but we run in "optimizing" (AOT) mode, so that the Checker
-# stanzas in test/638-checker-inline-cache-intrinsic/src/Main.java will be checked.
-# Also pass a large JIT code cache size to avoid getting the inline caches GCed.
-exec ${RUN} $@ --jit \
-  --runtime-option -Xjitinitialsize:32M --runtime-option -Xjitthreshold:1000 -Xcompiler-option \
-  --verbose-methods=inlineMonomorphic,inlinePolymorphic,knownReceiverType,stringEquals
+
+def run(ctx, args):
+  # Set threshold to 1000 to match the iterations done in the test.
+  # Pass --verbose-methods to only generate the CFG of these methods.
+  # The test is for JIT, but we run in "optimizing" (AOT) mode, so that the Checker
+  # stanzas in test/638-checker-inline-cache-intrinsic/src/Main.java will be checked.
+  # Also pass a large JIT code cache size to avoid getting the inline caches GCed.
+  ctx.default_run(
+      args,
+      jit=True,
+      runtime_option=["-Xjitinitialsize:32M", "-Xjitthreshold:1000"],
+      Xcompiler_option=[
+          "--verbose-methods=inlineMonomorphic,inlinePolymorphic,knownReceiverType,stringEquals"
+      ])
diff --git a/test/638-checker-inline-caches/run b/test/638-checker-inline-caches/run
index 146e180..7678899 100644
--- a/test/638-checker-inline-caches/run
+++ b/test/638-checker-inline-caches/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/643-checker-bogus-ic/run b/test/643-checker-bogus-ic/run
index 146e180..7678899 100644
--- a/test/643-checker-bogus-ic/run
+++ b/test/643-checker-bogus-ic/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/648-inline-caches-unresolved/run b/test/648-inline-caches-unresolved/run
index d24ef42..5ef44b3 100644
--- a/test/648-inline-caches-unresolved/run
+++ b/test/648-inline-caches-unresolved/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile
+
+def run(ctx, args):
+  ctx.default_run(args, profile=True)
diff --git a/test/652-deopt-intrinsic/run b/test/652-deopt-intrinsic/run
index 1acedf9..82f8ed0 100755
--- a/test/652-deopt-intrinsic/run
+++ b/test/652-deopt-intrinsic/run
@@ -14,9 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ensure this test is not subject to code collection.
-# We also need at least a few invocations of the method Main.$noinline$doCall
-# to ensure the inline cache sees the two types being passed to the method. Pass
-# a large number in case there's some weights on some invocation kinds (eg
-# compiler to interpreter transitions).
-exec ${RUN} "$@" --runtime-option -Xjitinitialsize:32M --runtime-option -Xjitthreshold:1000
+
+def run(ctx, args):
+  # Ensure this test is not subject to code collection.
+  # We also need at least a few invocations of the method Main.$noinline$doCall
+  # to ensure the inline cache sees the two types being passed to the method. Pass
+  # a large number in case there's some weights on some invocation kinds (eg
+  # compiler to interpreter transitions).
+  ctx.default_run(
+      args, runtime_option=["-Xjitinitialsize:32M", "-Xjitthreshold:1000"])
diff --git a/test/etc/default-run b/test/656-annotation-lookup-generic-jni/run
similarity index 77%
copy from test/etc/default-run
copy to test/656-annotation-lookup-generic-jni/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/656-annotation-lookup-generic-jni/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/660-clinit/run b/test/660-clinit/run
index a0e79ee..75c2bcb 100644
--- a/test/660-clinit/run
+++ b/test/660-clinit/run
@@ -14,4 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --initialize-app-image-classes=true
+
+def run(ctx, args):
+  ctx.default_run(
+      args,
+      profile=True,
+      Xcompiler_option=["--initialize-app-image-classes=true"])
diff --git a/test/661-oat-writer-layout/run b/test/661-oat-writer-layout/run
index 3c09690..838f3b1 100644
--- a/test/661-oat-writer-layout/run
+++ b/test/661-oat-writer-layout/run
@@ -14,9 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Always use the 'profile'.
-# Note that this test only works with --compiler-filter=speed
-# -- 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
+
+def run(ctx, args):
+  # Always use the 'profile'.
+  # Note that this test only works with --compiler-filter=speed
+  # -- 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.
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed"])
diff --git a/test/663-odd-dex-size/run b/test/663-odd-dex-size/run
index 51777ca..635d23b 100644
--- a/test/663-odd-dex-size/run
+++ b/test/663-odd-dex-size/run
@@ -14,12 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run normally.
-${RUN} $@
-return_status1=$?
 
-# Run without cdex to trigger the unalignment in the vdex file.
-${RUN} ${@} -Xcompiler-option --compact-dex-level=none
-return_status2=$?
+def run(ctx, args):
+  # Run normally.
+  ctx.default_run(args)
 
-(exit ${return_status1}) && (exit ${return_status2})
+  # Run without cdex to trigger the unalignment in the vdex file.
+  ctx.default_run(args, Xcompiler_option=["--compact-dex-level=none"])
diff --git a/test/667-jit-jni-stub/run b/test/667-jit-jni-stub/run
index b7ce913..e211a9b 100755
--- a/test/667-jit-jni-stub/run
+++ b/test/667-jit-jni-stub/run
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Disable AOT compilation of JNI stubs.
-# Ensure this test is not subject to unexpected code collection.
-${RUN} "${@}" --no-prebuild --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # Disable AOT compilation of JNI stubs.
+  # Ensure this test is not subject to unexpected code collection.
+  ctx.default_run(args, prebuild=False, runtime_option=["-Xjitinitialsize:32M"])
diff --git a/test/670-bitstring-type-check/run b/test/670-bitstring-type-check/run
index a189dc5..5bfe969 100644
--- a/test/670-bitstring-type-check/run
+++ b/test/670-bitstring-type-check/run
@@ -14,7 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test can take 7-11 mins, so raise the default 10 min timeout.
-export ART_TIME_OUT_MULTIPLIER=2
 
-exec ${RUN} "$@"
+def run(ctx, args):
+  # This test can take 7-11 mins, so raise the default 10 min timeout.
+  ctx.env.ART_TIME_OUT_MULTIPLIER = 2
+
+  ctx.default_run(args)
diff --git a/test/674-HelloWorld-Dm/run b/test/674-HelloWorld-Dm/run
index b8a61c5..9945fec 100644
--- a/test/674-HelloWorld-Dm/run
+++ b/test/674-HelloWorld-Dm/run
@@ -14,10 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-${RUN} --dex2oat-dm "${@}"
-return_status1=$?
 
-${RUN} --runtime-dm "${@}"
-return_status2=$?
+def run(ctx, args):
+  ctx.default_run(args, dex2oat_dm=True)
 
-(exit ${return_status1}) && (exit ${return_status2})
+  ctx.default_run(args, runtime_dm=True)
diff --git a/test/674-hiddenapi/run b/test/674-hiddenapi/run
index 0ab4763..c294263 100755
--- a/test/674-hiddenapi/run
+++ b/test/674-hiddenapi/run
@@ -14,9 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Make verification soft fail so that we can re-verify boot classpath
-# methods at runtime.
-#
-# N.B. Compilation of secondary dexes can prevent hidden API checks, e.g. if
-# a blocklisted field get is inlined.
-exec ${RUN} $@ --verify-soft-fail --no-secondary-compilation
+
+def run(ctx, args):
+  # Make verification soft fail so that we can re-verify boot classpath
+  # methods at runtime.
+  #
+  # N.B. Compilation of secondary dexes can prevent hidden API checks, e.g. if
+  # a blocklisted field get is inlined.
+  ctx.default_run(args, verify_soft_fail=True, secondary_compilation=False)
diff --git a/test/674-hotness-compiled/run b/test/674-hotness-compiled/run
index 85e8e3b..aca5b13 100755
--- a/test/674-hotness-compiled/run
+++ b/test/674-hotness-compiled/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-${RUN} "$@" -Xcompiler-option --count-hotness-in-compiled-code
+
+def run(ctx, args):
+  ctx.default_run(args, Xcompiler_option=["--count-hotness-in-compiled-code"])
diff --git a/test/674-vdex-uncompress/run b/test/674-vdex-uncompress/run
index edf699f..7684cbd 100644
--- a/test/674-vdex-uncompress/run
+++ b/test/674-vdex-uncompress/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} -Xcompiler-option --compiler-filter=verify --vdex "${@}"
+
+def run(ctx, args):
+  ctx.default_run(
+      args, Xcompiler_option=["--compiler-filter=verify"], vdex=True)
diff --git a/test/676-proxy-jit-at-first-use/run b/test/676-proxy-jit-at-first-use/run
index 16c9f76..4687094 100644
--- a/test/676-proxy-jit-at-first-use/run
+++ b/test/676-proxy-jit-at-first-use/run
@@ -14,6 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Enable "jit at first use" (-Xjitthreshold:0).
-# Ensure this test is not subject to unexpected code collection.
-${RUN} "${@}" --runtime-option -Xjitthreshold:0 --runtime-option -Xjitinitialsize:32M
+
+def run(ctx, args):
+  # Enable "jit at first use" (-Xjitthreshold:0).
+  # Ensure this test is not subject to unexpected code collection.
+  ctx.default_run(
+      args, runtime_option=["-Xjitthreshold:0", "-Xjitinitialsize:32M"])
diff --git a/test/677-fsi/run b/test/677-fsi/run
index 30d925e..87e22cd 100644
--- a/test/677-fsi/run
+++ b/test/677-fsi/run
@@ -14,6 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Redirect logger to stderr, as the test relies on error
-# messages being printed there.
-exec ${RUN} $@ -Xcompiler-option --copy-dex-files=always --runtime-option -Xonly-use-system-oat-files --runtime-option -Xuse-stderr-logger
+
+def run(ctx, args):
+  # Redirect logger to stderr, as the test relies on error
+  # messages being printed there.
+  ctx.default_run(
+      args,
+      Xcompiler_option=["--copy-dex-files=always"],
+      runtime_option=["-Xonly-use-system-oat-files", "-Xuse-stderr-logger"])
diff --git a/test/677-fsi2/run b/test/677-fsi2/run
index 651f082..0bbabb9 100644
--- a/test/677-fsi2/run
+++ b/test/677-fsi2/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-${RUN} $@ --runtime-option -Xonly-use-system-oat-files
+
+def run(ctx, args):
+  ctx.default_run(args, runtime_option=["-Xonly-use-system-oat-files"])
diff --git a/test/678-quickening/run b/test/678-quickening/run
index 0cc87f3..ea7a2bb 100644
--- a/test/678-quickening/run
+++ b/test/678-quickening/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.i
 
-# Run without an app image to prevent the class NotLoaded to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the class NotLoaded to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/679-locks/run b/test/679-locks/run
index 0cc87f3..ea7a2bb 100644
--- a/test/679-locks/run
+++ b/test/679-locks/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.i
 
-# Run without an app image to prevent the class NotLoaded to be loaded at startup.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # Run without an app image to prevent the class NotLoaded to be loaded at startup.
+  ctx.default_run(args, app_image=False)
diff --git a/test/688-shared-library/run b/test/688-shared-library/run
index fa6ab58..a3cf9d7 100644
--- a/test/688-shared-library/run
+++ b/test/688-shared-library/run
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# App images are incompatible with what the test is doing: loading one
-# dex file multiple times.
-exec ${RUN} "${@}" --no-app-image
+
+def run(ctx, args):
+  # App images are incompatible with what the test is doing: loading one
+  # dex file multiple times.
+  ctx.default_run(args, app_image=False)
diff --git a/test/689-zygote-jit-deopt/run b/test/689-zygote-jit-deopt/run
index 7b4b7eb..3c4f0fe 100644
--- a/test/689-zygote-jit-deopt/run
+++ b/test/689-zygote-jit-deopt/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --zygote
+
+def run(ctx, args):
+  ctx.default_run(args, zygote=True)
diff --git a/test/692-vdex-secondary-loader/run b/test/692-vdex-secondary-loader/run
index 35b55d6..7c9de5c 100644
--- a/test/692-vdex-secondary-loader/run
+++ b/test/692-vdex-secondary-loader/run
@@ -14,13 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Disable dex2oat of secondary dex files.
-${RUN} "$@" --no-secondary-compilation
-return_status1=$?
 
-# Set low RAM to hit the Madvise code which used to crash
-${RUN} "$@" --runtime-option -XX:LowMemoryMode --no-secondary-compilation
-return_status2=$?
+def run(ctx, args):
+  # Disable dex2oat of secondary dex files.
+  ctx.default_run(args, secondary_compilation=False)
 
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && (exit $return_status2)
+  # Set low RAM to hit the Madvise code which used to crash
+  ctx.default_run(
+      args, runtime_option=["-XX:LowMemoryMode"], secondary_compilation=False)
diff --git a/test/707-checker-invalid-profile/run b/test/707-checker-invalid-profile/run
index 146e180..7678899 100644
--- a/test/707-checker-invalid-profile/run
+++ b/test/707-checker-invalid-profile/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/710-varhandle-creation/run b/test/710-varhandle-creation/run
index 46b1a83..930869a 100644
--- a/test/710-varhandle-creation/run
+++ b/test/710-varhandle-creation/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Currently app images aren't unloaded when dex files are unloaded.
-exec ${RUN} $@ --no-secondary-app-image
+
+def run(ctx, args):
+  # Currently app images aren't unloaded when dex files are unloaded.
+  ctx.default_run(args, secondary_app_image=False)
diff --git a/test/714-invoke-custom-lambda-metafactory/run b/test/714-invoke-custom-lambda-metafactory/run
index 7a0d0d0..80a8a33 100755
--- a/test/714-invoke-custom-lambda-metafactory/run
+++ b/test/714-invoke-custom-lambda-metafactory/run
@@ -14,6 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Squash the exit status and put it in expected
-./default-run "$@"
-echo "exit status:" $?
+
+def run(ctx, args):
+  ctx.default_run(args, expected_exit_code=1)
diff --git a/test/727-checker-unresolved-class/run b/test/727-checker-unresolved-class/run
index 1c9dd11..986897b 100644
--- a/test/727-checker-unresolved-class/run
+++ b/test/727-checker-unresolved-class/run
@@ -14,14 +14,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-if [[ "$TEST_RUNTIME" == "jvm" ]]; then
-  exec ${RUN} $@
-else
-  # Append graphs for checker tests (we run dex2oat twice) with
-  #     --dump-cfg-append.
-  # Make some classes unresolved for AOT compilation with
-  #     --updatable-bcp-packages-file.
-  exec ${RUN} $@ \
-      -Xcompiler-option --dump-cfg-append \
-      -Xcompiler-option --updatable-bcp-packages-file="$DEX_LOCATION/res/updateable.txt"
-fi
+
+def run(ctx, args):
+  if args.jvm:
+    ctx.default_run(args)
+  else:
+    # Append graphs for checker tests (we run dex2oat twice) with
+    #     --dump-cfg-append.
+    # Make some classes unresolved for AOT compilation with
+    #     --updatable-bcp-packages-file.
+    ctx.default_run(
+        args,
+        Xcompiler_option=[
+            "--dump-cfg-append",
+            f"--updatable-bcp-packages-file={ctx.env.DEX_LOCATION}/res/updateable.txt"
+        ])
diff --git a/test/728-imt-conflict-zygote/run b/test/728-imt-conflict-zygote/run
index 8fdff6d..d0b6e49 100644
--- a/test/728-imt-conflict-zygote/run
+++ b/test/728-imt-conflict-zygote/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --zygote
+
+def run(ctx, args):
+  ctx.default_run(args, zygote=True)
diff --git a/test/729-checker-polymorphic-intrinsic/run b/test/729-checker-polymorphic-intrinsic/run
index 5fa72ed..43aa25b 100644
--- a/test/729-checker-polymorphic-intrinsic/run
+++ b/test/729-checker-polymorphic-intrinsic/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile -Xcompiler-option --compiler-filter=speed-profile
+
+def run(ctx, args):
+  ctx.default_run(
+      args, profile=True, Xcompiler_option=["--compiler-filter=speed-profile"])
diff --git a/test/800-smali/run b/test/800-smali/run
index c8ce0bc..f20dbc0 100644
--- a/test/800-smali/run
+++ b/test/800-smali/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Target 31 to have the verifier behavior the test expects.
-./default-run "$@" --runtime-option -Xtarget-sdk-version:31
+
+def run(ctx, args):
+  # Target 31 to have the verifier behavior the test expects.
+  ctx.default_run(args, runtime_option=["-Xtarget-sdk-version:31"])
diff --git a/test/etc/default-run b/test/817-hiddenapi/run
similarity index 77%
copy from test/etc/default-run
copy to test/817-hiddenapi/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/817-hiddenapi/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/818-clinit-nterp/run b/test/818-clinit-nterp/run
index 52d2b5f..0f3a0ea 100644
--- a/test/818-clinit-nterp/run
+++ b/test/818-clinit-nterp/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} $@ --profile
+
+def run(ctx, args):
+  ctx.default_run(args, profile=True)
diff --git a/test/819-verification-runtime/run b/test/819-verification-runtime/run
index c8ce0bc..f20dbc0 100755
--- a/test/819-verification-runtime/run
+++ b/test/819-verification-runtime/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Target 31 to have the verifier behavior the test expects.
-./default-run "$@" --runtime-option -Xtarget-sdk-version:31
+
+def run(ctx, args):
+  # Target 31 to have the verifier behavior the test expects.
+  ctx.default_run(args, runtime_option=["-Xtarget-sdk-version:31"])
diff --git a/test/820-vdex-multidex/run b/test/820-vdex-multidex/run
index 3f6dc3c..f597e41 100644
--- a/test/820-vdex-multidex/run
+++ b/test/820-vdex-multidex/run
@@ -14,4 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} -Xcompiler-option --compiler-filter=verify --vdex "${@}"
+
+def run(ctx, args):
+  ctx.default_run(
+      args, Xcompiler_option=["--compiler-filter=verify"], vdex=True)
diff --git a/test/821-madvise-willneed/run b/test/821-madvise-willneed/run
index 2c3917f..06d3bac 100644
--- a/test/821-madvise-willneed/run
+++ b/test/821-madvise-willneed/run
@@ -14,8 +14,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.i
 
-# Load and run HelloWorld after madvising odex, vdex, art files to 100MB size
-# limit
-exec ${RUN} "${@}" --runtime-option -XMadviseWillNeedVdexFileSize:104857600 \
-  --runtime-option -XMadviseWillNeedOdexFileSize:104857600 \
-  --runtime-option -XMadviseWillNeedArtFileSize:104857600
+
+def run(ctx, args):
+  # Load and run HelloWorld after madvising odex, vdex, art files to 100MB size
+  # limit
+  ctx.default_run(
+      args,
+      runtime_option=[
+          "-XMadviseWillNeedVdexFileSize:104857600",
+          "-XMadviseWillNeedOdexFileSize:104857600",
+          "-XMadviseWillNeedArtFileSize:104857600"
+      ])
diff --git a/test/833-background-verification/run b/test/833-background-verification/run
index c455473..44d80ac 100755
--- a/test/833-background-verification/run
+++ b/test/833-background-verification/run
@@ -14,7 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Target 31 to run the background verifier.
-# Disable dex2oat of secondary dex files to ensure we always run the background
-# verifier.
-./default-run "$@" --runtime-option -Xtarget-sdk-version:31 --no-secondary-compilation
+
+def run(ctx, args):
+  # Target 31 to run the background verifier.
+  # Disable dex2oat of secondary dex files to ensure we always run the background
+  # verifier.
+  ctx.default_run(
+      args,
+      runtime_option=["-Xtarget-sdk-version:31"],
+      secondary_compilation=False)
diff --git a/test/842-vdex-hard-failure/run b/test/842-vdex-hard-failure/run
index 4a4ee37..81ce172 100644
--- a/test/842-vdex-hard-failure/run
+++ b/test/842-vdex-hard-failure/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test is for testing vdex when calling FastVerify and doing compilation.
-exec ${RUN} --vdex --vdex-filter speed "${@}"
+
+def run(ctx, args):
+  # This test is for testing vdex when calling FastVerify and doing compilation.
+  ctx.default_run(args, vdex=True, vdex_filter="speed")
diff --git a/test/900-hello-plugin/run b/test/900-hello-plugin/run
index a19a38c..607a0e5 100755
--- a/test/900-hello-plugin/run
+++ b/test/900-hello-plugin/run
@@ -14,32 +14,26 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-plugin=libartagentd.so
-if  [[ "$@" == *"-O"* ]]; then
-  plugin=libartagent.so
-fi
 
-# Adjust the agent path when running on device.
-if  [[ "$@" != *"--host"* ]]; then
-  if [[ -z "$ANDROID_BUILD_TOP" ]]; then
-    echo 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?'
-    exit 1
-  fi
+def run(ctx, args):
+  plugin = "libartagent.so" if args.O else "libartagentd.so"
 
-  bitness_flag=--32
-  if  [[ "$@" == *"--64"* ]]; then
-    bitness_flag=--64
-  fi
+  # Adjust the agent path when running on device.
+  if not args.host:
+    for i, opt in enumerate(args.runtime_option):
+      if opt.startswith("-Djava.library.path="):
+        libpath = opt.split("=")[-1]
+        assert libpath.startswith("/data/nativetest"), libpath
 
-  # Path to native libraries installed on the device for testing purposes.
-  test_native_lib_path=$("$ANDROID_BUILD_TOP/art/test/utils/get-device-test-native-lib-path" \
-    "$bitness_flag")
+        # The linker configuration used for dalvikvm(64) in the ART APEX requires us
+        # to pass the full path to the agent to the runtime when running on device.
+        plugin = f"{libpath}/{plugin}"
+        break
 
-  # The linker configuration used for dalvikvm(64) in the ART APEX requires us
-  # to pass the full path to the agent to the runtime when running on device.
-  plugin=${test_native_lib_path}/${plugin}
-fi
-
-./default-run "$@" --runtime-option -agentpath:${plugin}=test_900 \
-                   --runtime-option -agentpath:${plugin}=test_900_round_2 \
-                   --android-runtime-option -Xplugin:${plugin}
+  ctx.default_run(
+      args,
+      runtime_option=[
+          f"-agentpath:{plugin}=test_900",
+          f"-agentpath:{plugin}=test_900_round_2"
+      ],
+      android_runtime_option=[f"-Xplugin:{plugin}"])
diff --git a/test/901-hello-ti-agent/run b/test/901-hello-ti-agent/run
index c6e62ae..4796039 100755
--- a/test/901-hello-ti-agent/run
+++ b/test/901-hello-ti-agent/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/902-hello-transformation/run b/test/902-hello-transformation/run
index c6e62ae..4796039 100755
--- a/test/902-hello-transformation/run
+++ b/test/902-hello-transformation/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/903-hello-tagging/run b/test/903-hello-tagging/run
index c6e62ae..4796039 100755
--- a/test/903-hello-tagging/run
+++ b/test/903-hello-tagging/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/904-object-allocation/run b/test/904-object-allocation/run
index c6e62ae..4796039 100755
--- a/test/904-object-allocation/run
+++ b/test/904-object-allocation/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/905-object-free/run b/test/905-object-free/run
index c6e62ae..4796039 100755
--- a/test/905-object-free/run
+++ b/test/905-object-free/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/906-iterate-heap/run b/test/906-iterate-heap/run
index c6e62ae..4796039 100755
--- a/test/906-iterate-heap/run
+++ b/test/906-iterate-heap/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/907-get-loaded-classes/run b/test/907-get-loaded-classes/run
index c6e62ae..4796039 100755
--- a/test/907-get-loaded-classes/run
+++ b/test/907-get-loaded-classes/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/908-gc-start-finish/run b/test/908-gc-start-finish/run
index c6e62ae..4796039 100755
--- a/test/908-gc-start-finish/run
+++ b/test/908-gc-start-finish/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/909-attach-agent/run b/test/909-attach-agent/run
index 71b1e1c..a3a9a6e 100755
--- a/test/909-attach-agent/run
+++ b/test/909-attach-agent/run
@@ -14,75 +14,51 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-plugin=libopenjdkjvmtid.so
-agent=libtiagentd.so
-if  [[ "$@" == *"-O"* ]]; then
-  agent=libtiagent.so
-  plugin=libopenjdkjvmti.so
-fi
 
-if [[ "$@" == *"--interpreter"* ]]; then
-  # On interpreter we are fully capable of providing the full jvmti api so we
-  # have a slightly different expected output.
-  # TODO We should really be changing this in the 'check' script.
-  patch -s expected-stdout.txt <interpreter-expected.patch
-fi
+def run(ctx, args):
+  agent = "libtiagent.so" if args.O else "libtiagentd.so"
+  plugin = "libopenjdkjvmti.so" if args.O else "libopenjdkjvmtid.so"
 
-# Provide additional runtime options when running on device.
-extra_runtime_options=
-if  [[ "$@" != *"--host"* ]]; then
-  if [[ -z "$ANDROID_BUILD_TOP" ]]; then
-    echo 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?'
-    exit 1
-  fi
+  if args.interpreter:
+    # On interpreter we are fully capable of providing the full jvmti api so we
+    # have a slightly different expected output.
+    # TODO We should really be changing this in the 'check' script.
+    ctx.bash("patch -s expected-stdout.txt <interpreter-expected.patch")
 
-  bitness_flag=--32
-  if  [[ "$@" == *"--64"* ]]; then
-    bitness_flag=--64
-  fi
+  # Provide additional runtime options when running on device.
+  if not args.host:
+    for i, opt in enumerate(args.runtime_option):
+      if opt.startswith("-Djava.library.path="):
+        libpath = opt.split("=")[-1]
+        assert libpath.startswith("/data/nativetest"), libpath
 
-  # Path to native libraries installed on the device for testing purposes.
-  test_native_lib_path=$("$ANDROID_BUILD_TOP/art/test/utils/get-device-test-native-lib-path" \
-    "$bitness_flag")
+        # The linker configuration used for dalvikvm(64) in the ART APEX requires us
+        # to pass the full path to the agent to the runtime when running on device.
+        agent = f"{libpath}/{agent}"
 
-  # The linker configuration used for dalvikvm(64) in the ART APEX requires us
-  # to pass the full path to the agent to the runtime when running on device.
-  agent=${test_native_lib_path}/${agent}
+        # The above agent path is an absolute one; append the root directory to the
+        # library path so that the agent can be found via the `java.library.path`
+        # system property (see method `Main.find` in
+        # test/909-attach-agent/src-art/Main.java).
+        args.runtime_option[i] += ":/"
+        break
 
-  # The above agent path is an absolute one; append the root directory to the
-  # library path so that the agent can be found via the `java.library.path`
-  # system property (see method `Main.find` in
-  # test/909-attach-agent/src-art/Main.java).
-  extra_runtime_options="--runtime-option -Djava.library.path=${test_native_lib_path}:/"
-fi
+  ctx.env.ANDROID_LOG_TAGS = "*:f"
+  ctx.default_run(
+      args,
+      android_runtime_option=[
+          f"-Xplugin:{plugin}", "-Xcompiler-option", "--debuggable"
+      ],
+      test_args=[f"agent:{agent}=909-attach-agent"])
 
-export ANDROID_LOG_TAGS='*:f'
-./default-run "$@" --android-runtime-option -Xplugin:${plugin} \
-                   --android-runtime-option -Xcompiler-option \
-                   --android-runtime-option --debuggable \
-                   $extra_runtime_options \
-                   --args agent:${agent}=909-attach-agent
-return_status1=$?
+  ctx.default_run(args, test_args=[f"agent:{agent}=909-attach-agent"])
 
-./default-run "$@" --android-runtime-option -Xcompiler-option \
-                   --android-runtime-option --debuggable \
-                   $extra_runtime_options \
-                   --args agent:${agent}=909-attach-agent
-return_status2=$?
+  ctx.default_run(
+      args,
+      test_args=[f"agent:{agent}=909-attach-agent"],
+      external_log_tags=True)
 
-./default-run "$@" $extra_runtime_options \
-                   --args agent:${agent}=909-attach-agent \
-                   --external-log-tags
-return_status3=$?
-
-./default-run "$@" $extra_runtime_options \
-                   --args agent:${agent}=909-attach-agent \
-                   --args disallow-debugging \
-                   --external-log-tags
-return_status4=$?
-
-# Make sure we don't silently ignore an early failure.
-(exit $return_status1) && \
-  (exit $return_status2) && \
-  (exit $return_status3) && \
-  (exit $return_status4)
+  ctx.default_run(
+      args,
+      test_args=[f"agent:{agent}=909-attach-agent", "disallow-debugging"],
+      external_log_tags=True)
diff --git a/test/910-methods/run b/test/910-methods/run
index c6e62ae..4796039 100755
--- a/test/910-methods/run
+++ b/test/910-methods/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/911-get-stack-trace/run b/test/911-get-stack-trace/run
index c6e62ae..4796039 100755
--- a/test/911-get-stack-trace/run
+++ b/test/911-get-stack-trace/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/912-classes/run b/test/912-classes/run
index f24db40..2f3b9fa 100755
--- a/test/912-classes/run
+++ b/test/912-classes/run
@@ -14,9 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test checks which classes are initiated by a classloader. App images preload classes.
-# In certain configurations, the app images may be valid even in a new classloader. Turn off
-# app images to avoid the issue.
 
-./default-run "$@" --jvmti \
-                   --no-app-image
+def run(ctx, args):
+  # This test checks which classes are initiated by a classloader. App images preload classes.
+  # In certain configurations, the app images may be valid even in a new classloader. Turn off
+  # app images to avoid the issue.
+
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/913-heaps/run b/test/913-heaps/run
index dd35526..d5731ca 100755
--- a/test/913-heaps/run
+++ b/test/913-heaps/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti -Xcompiler-option -g
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, Xcompiler_option=["-g"])
diff --git a/test/914-hello-obsolescence/run b/test/914-hello-obsolescence/run
index c6e62ae..4796039 100755
--- a/test/914-hello-obsolescence/run
+++ b/test/914-hello-obsolescence/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/915-obsolete-2/run b/test/915-obsolete-2/run
index c6e62ae..4796039 100755
--- a/test/915-obsolete-2/run
+++ b/test/915-obsolete-2/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/916-obsolete-jit/run b/test/916-obsolete-jit/run
index c6e62ae..4796039 100755
--- a/test/916-obsolete-jit/run
+++ b/test/916-obsolete-jit/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/917-fields-transformation/run b/test/917-fields-transformation/run
index c6e62ae..4796039 100755
--- a/test/917-fields-transformation/run
+++ b/test/917-fields-transformation/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/918-fields/run b/test/918-fields/run
index c6e62ae..4796039 100755
--- a/test/918-fields/run
+++ b/test/918-fields/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/919-obsolete-fields/run b/test/919-obsolete-fields/run
index c6e62ae..4796039 100755
--- a/test/919-obsolete-fields/run
+++ b/test/919-obsolete-fields/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/920-objects/run b/test/920-objects/run
index c6e62ae..4796039 100755
--- a/test/920-objects/run
+++ b/test/920-objects/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/921-hello-failure/run b/test/921-hello-failure/run
index 8be0ed4..4796039 100755
--- a/test/921-hello-failure/run
+++ b/test/921-hello-failure/run
@@ -15,4 +15,5 @@
 # limitations under the License.
 
 
-./default-run "$@" --jvmti
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/922-properties/run b/test/922-properties/run
index c6e62ae..4796039 100755
--- a/test/922-properties/run
+++ b/test/922-properties/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/923-monitors/run b/test/923-monitors/run
index c6e62ae..4796039 100755
--- a/test/923-monitors/run
+++ b/test/923-monitors/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/924-threads/run b/test/924-threads/run
index c6e62ae..4796039 100755
--- a/test/924-threads/run
+++ b/test/924-threads/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/925-threadgroups/run b/test/925-threadgroups/run
index c6e62ae..4796039 100755
--- a/test/925-threadgroups/run
+++ b/test/925-threadgroups/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/926-multi-obsolescence/run b/test/926-multi-obsolescence/run
index c6e62ae..4796039 100755
--- a/test/926-multi-obsolescence/run
+++ b/test/926-multi-obsolescence/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/927-timers/run b/test/927-timers/run
index c6e62ae..4796039 100755
--- a/test/927-timers/run
+++ b/test/927-timers/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/928-jni-table/run b/test/928-jni-table/run
index c6e62ae..4796039 100755
--- a/test/928-jni-table/run
+++ b/test/928-jni-table/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/929-search/run b/test/929-search/run
index fb6b1b8..012e279 100755
--- a/test/929-search/run
+++ b/test/929-search/run
@@ -14,10 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test checks whether dex files can be injected into parent classloaders. App images preload
-# classes, which will make the injection moot. Turn off app images to avoid the issue.
-# Pass the correct `--secondary-class-loader-context` for the "-ex" jar.
 
-./default-run "$@" --jvmti \
-                   --no-app-image \
-                   --secondary-class-loader-context "PCL[$DEX_LOCATION/$TEST_NAME.jar]"
+def run(ctx, args):
+  # This test checks whether dex files can be injected into parent classloaders. App images preload
+  # classes, which will make the injection moot. Turn off app images to avoid the issue.
+  # Pass the correct `--secondary-class-loader-context` for the "-ex" jar.
+
+  pcl = f"PCL[{ctx.env.DEX_LOCATION}/{ctx.env.TEST_NAME}.jar]"
+  ctx.default_run(
+      args, jvmti=True, app_image=False, secondary_class_loader_context=pcl)
diff --git a/test/930-hello-retransform/run b/test/930-hello-retransform/run
index c6e62ae..4796039 100755
--- a/test/930-hello-retransform/run
+++ b/test/930-hello-retransform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/931-agent-thread/run b/test/931-agent-thread/run
index 67923a7..4ac9127 100755
--- a/test/931-agent-thread/run
+++ b/test/931-agent-thread/run
@@ -14,8 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test checks whether dex files can be injected into parent classloaders. App images preload
-# classes, which will make the injection moot. Turn off app images to avoid the issue.
 
-./default-run "$@" --jvmti \
-                   --no-app-image
+def run(ctx, args):
+  # This test checks whether dex files can be injected into parent classloaders. App images preload
+  # classes, which will make the injection moot. Turn off app images to avoid the issue.
+
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/932-transform-saves/run b/test/932-transform-saves/run
index c6e62ae..4796039 100755
--- a/test/932-transform-saves/run
+++ b/test/932-transform-saves/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/933-misc-events/run b/test/933-misc-events/run
index 67923a7..5a2efa9 100755
--- a/test/933-misc-events/run
+++ b/test/933-misc-events/run
@@ -14,8 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test checks whether dex files can be injected into parent classloaders. App images preload
-# classes, which will make the injection moot. Turn off app images to avoid the issue.
 
-./default-run "$@" --jvmti \
-                   --no-app-image
+def run(ctx, args):
+  # This test checks whether dex files can be injected into parent classloaders. App images preload
+  # classes, which will make the injection moot. Turn off app images to avoid the issue.
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/934-load-transform/run b/test/934-load-transform/run
index adb1a1c..23b8dc3 100755
--- a/test/934-load-transform/run
+++ b/test/934-load-transform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/935-non-retransformable/run b/test/935-non-retransformable/run
index adb1a1c..23b8dc3 100755
--- a/test/935-non-retransformable/run
+++ b/test/935-non-retransformable/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/936-search-onload/run b/test/936-search-onload/run
index fb6b1b8..012e279 100755
--- a/test/936-search-onload/run
+++ b/test/936-search-onload/run
@@ -14,10 +14,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This test checks whether dex files can be injected into parent classloaders. App images preload
-# classes, which will make the injection moot. Turn off app images to avoid the issue.
-# Pass the correct `--secondary-class-loader-context` for the "-ex" jar.
 
-./default-run "$@" --jvmti \
-                   --no-app-image \
-                   --secondary-class-loader-context "PCL[$DEX_LOCATION/$TEST_NAME.jar]"
+def run(ctx, args):
+  # This test checks whether dex files can be injected into parent classloaders. App images preload
+  # classes, which will make the injection moot. Turn off app images to avoid the issue.
+  # Pass the correct `--secondary-class-loader-context` for the "-ex" jar.
+
+  pcl = f"PCL[{ctx.env.DEX_LOCATION}/{ctx.env.TEST_NAME}.jar]"
+  ctx.default_run(
+      args, jvmti=True, app_image=False, secondary_class_loader_context=pcl)
diff --git a/test/937-hello-retransform-package/run b/test/937-hello-retransform-package/run
index c6e62ae..4796039 100755
--- a/test/937-hello-retransform-package/run
+++ b/test/937-hello-retransform-package/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/938-load-transform-bcp/run b/test/938-load-transform-bcp/run
index adb1a1c..23b8dc3 100755
--- a/test/938-load-transform-bcp/run
+++ b/test/938-load-transform-bcp/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti --no-app-image
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True, app_image=False)
diff --git a/test/939-hello-transformation-bcp/run b/test/939-hello-transformation-bcp/run
index c6e62ae..4796039 100755
--- a/test/939-hello-transformation-bcp/run
+++ b/test/939-hello-transformation-bcp/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/940-recursive-obsolete/run b/test/940-recursive-obsolete/run
index c6e62ae..4796039 100755
--- a/test/940-recursive-obsolete/run
+++ b/test/940-recursive-obsolete/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/941-recursive-obsolete-jit/run b/test/941-recursive-obsolete-jit/run
index c6e62ae..4796039 100755
--- a/test/941-recursive-obsolete-jit/run
+++ b/test/941-recursive-obsolete-jit/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/942-private-recursive/run b/test/942-private-recursive/run
index c6e62ae..4796039 100755
--- a/test/942-private-recursive/run
+++ b/test/942-private-recursive/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/943-private-recursive-jit/run b/test/943-private-recursive-jit/run
index c6e62ae..4796039 100755
--- a/test/943-private-recursive-jit/run
+++ b/test/943-private-recursive-jit/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/944-transform-classloaders/run b/test/944-transform-classloaders/run
index c6e62ae..4796039 100755
--- a/test/944-transform-classloaders/run
+++ b/test/944-transform-classloaders/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/945-obsolete-native/run b/test/945-obsolete-native/run
index c6e62ae..4796039 100755
--- a/test/945-obsolete-native/run
+++ b/test/945-obsolete-native/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/946-obsolete-throw/run b/test/946-obsolete-throw/run
index e92b873..b596886 100755
--- a/test/946-obsolete-throw/run
+++ b/test/946-obsolete-throw/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/947-reflect-method/run b/test/947-reflect-method/run
index e92b873..b596886 100755
--- a/test/947-reflect-method/run
+++ b/test/947-reflect-method/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/948-change-annotations/run b/test/948-change-annotations/run
index c6e62ae..4796039 100755
--- a/test/948-change-annotations/run
+++ b/test/948-change-annotations/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/949-in-memory-transform/run b/test/949-in-memory-transform/run
index e92b873..b596886 100755
--- a/test/949-in-memory-transform/run
+++ b/test/949-in-memory-transform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/950-redefine-intrinsic/run b/test/950-redefine-intrinsic/run
index e92b873..b596886 100755
--- a/test/950-redefine-intrinsic/run
+++ b/test/950-redefine-intrinsic/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/951-threaded-obsolete/run b/test/951-threaded-obsolete/run
index c6e62ae..4796039 100755
--- a/test/951-threaded-obsolete/run
+++ b/test/951-threaded-obsolete/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/etc/default-run b/test/954-invoke-polymorphic-verifier/run
similarity index 77%
copy from test/etc/default-run
copy to test/954-invoke-polymorphic-verifier/run
index ecbbbc7..78fb0ce 100755
--- a/test/etc/default-run
+++ b/test/954-invoke-polymorphic-verifier/run
@@ -1,12 +1,12 @@
 #!/bin/bash
 #
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright 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
+#      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,
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-exec ${RUN} "$@"
+
+def run(ctx, args):
+  ctx.default_run(args)
diff --git a/test/961-default-iface-resolution-gen/run b/test/961-default-iface-resolution-gen/run
index fdcd2a8..7de21e3 100755
--- a/test/961-default-iface-resolution-gen/run
+++ b/test/961-default-iface-resolution-gen/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Run with a 2 minute default dex2oat timeout and a 2.5 minute hard dex2oat timeout.
-./default-run "$@" --dex2oat-timeout 120 --dex2oat-rt-timeout 180
+
+def run(ctx, args):
+  # Run with a 2 minute default dex2oat timeout and a 2.5 minute hard dex2oat timeout.
+  ctx.default_run(args, dex2oat_timeout=120, dex2oat_rt_timeout=180)
diff --git a/test/980-redefine-object/run b/test/980-redefine-object/run
index c6e62ae..4796039 100755
--- a/test/980-redefine-object/run
+++ b/test/980-redefine-object/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/981-dedup-original-dex/run b/test/981-dedup-original-dex/run
index e92b873..b596886 100755
--- a/test/981-dedup-original-dex/run
+++ b/test/981-dedup-original-dex/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/982-ok-no-retransform/run b/test/982-ok-no-retransform/run
index c6e62ae..4796039 100755
--- a/test/982-ok-no-retransform/run
+++ b/test/982-ok-no-retransform/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/983-source-transform-verify/run b/test/983-source-transform-verify/run
index c6e62ae..4796039 100755
--- a/test/983-source-transform-verify/run
+++ b/test/983-source-transform-verify/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/984-obsolete-invoke/run b/test/984-obsolete-invoke/run
index c6e62ae..4796039 100755
--- a/test/984-obsolete-invoke/run
+++ b/test/984-obsolete-invoke/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/985-re-obsolete/run b/test/985-re-obsolete/run
index e92b873..b596886 100755
--- a/test/985-re-obsolete/run
+++ b/test/985-re-obsolete/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/986-native-method-bind/run b/test/986-native-method-bind/run
index e92b873..b596886 100755
--- a/test/986-native-method-bind/run
+++ b/test/986-native-method-bind/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/987-agent-bind/run b/test/987-agent-bind/run
index e92b873..b596886 100755
--- a/test/987-agent-bind/run
+++ b/test/987-agent-bind/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/988-method-trace/run b/test/988-method-trace/run
index 51875a7..ce3a55a 100755
--- a/test/988-method-trace/run
+++ b/test/988-method-trace/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/989-method-trace-throw/run b/test/989-method-trace-throw/run
index 51875a7..ce3a55a 100755
--- a/test/989-method-trace-throw/run
+++ b/test/989-method-trace-throw/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/990-field-trace/run b/test/990-field-trace/run
index 51875a7..ce3a55a 100755
--- a/test/990-field-trace/run
+++ b/test/990-field-trace/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/991-field-trace-2/run b/test/991-field-trace-2/run
index 51875a7..ce3a55a 100755
--- a/test/991-field-trace-2/run
+++ b/test/991-field-trace-2/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/992-source-data/run b/test/992-source-data/run
index e92b873..b596886 100755
--- a/test/992-source-data/run
+++ b/test/992-source-data/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/993-breakpoints/run b/test/993-breakpoints/run
index 51875a7..ce3a55a 100755
--- a/test/993-breakpoints/run
+++ b/test/993-breakpoints/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/994-breakpoint-line/run b/test/994-breakpoint-line/run
index 51875a7..ce3a55a 100755
--- a/test/994-breakpoint-line/run
+++ b/test/994-breakpoint-line/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/995-breakpoints-throw/run b/test/995-breakpoints-throw/run
index 51875a7..ce3a55a 100755
--- a/test/995-breakpoints-throw/run
+++ b/test/995-breakpoints-throw/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/996-breakpoint-obsolete/run b/test/996-breakpoint-obsolete/run
index 51875a7..ce3a55a 100755
--- a/test/996-breakpoint-obsolete/run
+++ b/test/996-breakpoint-obsolete/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/997-single-step/run b/test/997-single-step/run
index 51875a7..ce3a55a 100755
--- a/test/997-single-step/run
+++ b/test/997-single-step/run
@@ -14,5 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Ask for stack traces to be dumped to a file rather than to stdout.
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  # Ask for stack traces to be dumped to a file rather than to stdout.
+  ctx.default_run(args, jvmti=True)
diff --git a/test/998-redefine-use-after-free/run b/test/998-redefine-use-after-free/run
index c6e62ae..4796039 100755
--- a/test/998-redefine-use-after-free/run
+++ b/test/998-redefine-use-after-free/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/999-redefine-hiddenapi/run b/test/999-redefine-hiddenapi/run
index c6e62ae..4796039 100755
--- a/test/999-redefine-hiddenapi/run
+++ b/test/999-redefine-hiddenapi/run
@@ -14,4 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-./default-run "$@" --jvmti
+
+def run(ctx, args):
+  ctx.default_run(args, jvmti=True)
diff --git a/test/Android.bp b/test/Android.bp
index 813517b..7697e84 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -1888,8 +1888,6 @@
     tool_files: [
         "art_build_rules.py",
         "run-test-build.py",
-        "etc/default-run",
-        "etc/run-test-jar",
         ":art-run-test-bootclasspath",
     ],
     tools: [
diff --git a/test/etc/apex_bootclasspath_utils.py b/test/etc/apex_bootclasspath_utils.py
index f969b14..d2415e5 100755
--- a/test/etc/apex_bootclasspath_utils.py
+++ b/test/etc/apex_bootclasspath_utils.py
@@ -23,10 +23,9 @@
 # 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"
+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).
@@ -34,40 +33,44 @@
 #  Arguments.
 #   ${1}: path prefix.
 def get_apex_bootclasspath_impl(bpath_prefix: str):
-  bpath_separator=""
-  bpath=""
-  bpath_jar=""
+  bpath_separator = ""
+  bpath = ""
+  bpath_jar = ""
   for bpath_module in bpath_modules.split(" "):
-    apex_module="com.android.art"
+    apex_module = "com.android.art"
     if bpath_module == "conscrypt":
-      apex_module="com.android.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=":"
+      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: bool):
-  bpath_prefix=""
+  bpath_prefix = ""
 
   if host:
-    bpath_prefix=ANDROID_HOST_OUT
+    bpath_prefix = os.environ["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: bool):
-  bpath_location_prefix=""
+  bpath_location_prefix = ""
 
   if host:
+    ANDROID_BUILD_TOP=os.environ["ANDROID_BUILD_TOP"]
+    ANDROID_HOST_OUT=os.environ["ANDROID_HOST_OUT"]
     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:
diff --git a/test/etc/run-test-jar b/test/etc/default_run.py
similarity index 91%
rename from test/etc/run-test-jar
rename to test/etc/default_run.py
index 004aee1..8203741 100755
--- a/test/etc/run-test-jar
+++ b/test/etc/default_run.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
 #
 # Copyright (C) 2022 The Android Open Source Project
 #
@@ -15,125 +14,16 @@
 # limitations under the License.
 
 import sys, os, shutil, shlex, re, subprocess, glob
-from apex_bootclasspath_utils import get_apex_bootclasspath, get_apex_bootclasspath_locations
-from argparse import ArgumentParser, BooleanOptionalAction
+from etc.apex_bootclasspath_utils import get_apex_bootclasspath, get_apex_bootclasspath_locations
+from argparse import ArgumentParser, BooleanOptionalAction, Namespace
 from os import path
 from os.path import isfile, isdir
 from typing import List
 from subprocess import DEVNULL, PIPE, STDOUT
 from tempfile import NamedTemporaryFile
 
-# TODO: Replace with 'def main():' (which might change variables from globals to locals)
-if True:
-  # 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")
 
-  # Script debugging: Record executed commands, but don't actually run the main test.
-  # This makes it possible the extract the test commands without waiting for days.
-  # This will make tests fail since there is no stdout.  Use with large -j value.
-  ART_TEST_DRY_RUN = os.environ.get("ART_TEST_DRY_RUN")
-
-  def run(cmdline: str,
-          env={},
-          quiet=True,
-          check=True,
-          save_cmd=True) -> subprocess.CompletedProcess:
-    env.setdefault("PATH", PATH)  # Ensure that PATH is always set.
-    env = {k: v for k, v in env.items() if v != None}  # Filter "None" entries.
-    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:
-        # 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")
-      if ART_TEST_DRY_RUN and ("dalvikvm" in cmdline or
-                               "adb shell chroot" in cmdline):
-        cmdline = "true"  # We still need to run some command, so run the no-op "true" binary instead.
-    if VERBOSE:
-      print("$ " + cmdline + "\n")
-    proc = subprocess.run([cmdline],
-                          shell=True,
-                          env=env,
-                          encoding="utf8",
-                          capture_output=True)
-    if (check and proc.returncode != 0) or (not quiet) or VERBOSE:
-      print(proc.stdout or "", file=sys.stdout, end="", flush=True)
-      print(proc.stderr or "", file=sys.stderr, end="", flush=True)
-    if (check and proc.returncode != 0):
-      raise Exception("Command returned exit code {}".format(proc.returncode))
-    return proc
-
-  class Adb():
-
-    def __init__(self):
-      self.env = {
-          "ADB_VENDOR_KEYS": os.environ.get("ADB_VENDOR_KEYS"),
-          "ANDROID_SERIAL": os.environ.get("ANDROID_SERIAL"),
-          "PATH": os.environ.get("PATH"),
-      }
-
-    def root(self) -> None:
-      run("adb root", self.env)
-
-    def wait_for_device(self) -> None:
-      run("adb wait-for-device", self.env)
-
-    def shell(self, cmdline: str, **kwargs) -> subprocess.CompletedProcess:
-      return run("adb shell " + cmdline, self.env, **kwargs)
-
-    def push(self, src: str, dst: str, **kwargs) -> None:
-      run(f"adb push {src} {dst}", self.env, **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.
-  bold_red = ""
-  if sys.stdout.isatty():
-    if int(subprocess.run(["tput", "colors"], capture_output=True).stdout) >= 1:
-      bold_red = subprocess.run(["tput", "bold"],
-                                text=True,
-                                capture_output=True).stdout.strip()
-      bold_red += subprocess.run(["tput", "setaf", "1"],
-                                 text=True,
-                                 capture_output=True).stdout.strip()
-
-  def error_msg(msg: str):
-    print(f"{bold_red}ERROR: {msg}")
-
-  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 not ANDROID_BUILD_TOP:
-    error_msg(
-        "ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?"
-    )
-    sys.exit(1)
-
-  def msg(msg: str):
-    if VERBOSE:
-      print(msg)
-
+def parse_args(argv):
   argp, opt_bool = ArgumentParser(), BooleanOptionalAction
   argp.add_argument("--64", dest="is64", action="store_true")
   argp.add_argument("--O", action="store_true")
@@ -145,7 +35,6 @@
   argp.add_argument("--android-runtime-option", default=[], action="append")
   argp.add_argument("--android-tzdata-root", default="/apex/com.android.tzdata")
   argp.add_argument("--app-image", default=True, action=opt_bool)
-  argp.add_argument("--args", default=[], action="append")
   argp.add_argument("--baseline", action="store_true")
   argp.add_argument("--bionic", action="store_true")
   argp.add_argument("--boot", default="")
@@ -205,15 +94,18 @@
   argp.add_argument("--timeout", default=0, type=int)
   argp.add_argument("--vdex", action="store_true")
   argp.add_argument("--vdex-arg", default=[], action="append")
-  argp.add_argument("--vdex-filter")
+  argp.add_argument("--vdex-filter", default="")
   argp.add_argument("--verbose", action="store_true")
   argp.add_argument("--verify", default=True, action=opt_bool)
   argp.add_argument("--verify-soft-fail", action="store_true")
   argp.add_argument("--with-agent", default=[], action="append")
   argp.add_argument("--zygote", action="store_true")
-  argp.add_argument("test_args", nargs="*", default=["Main"])
+  argp.add_argument("--test_args", default=[], action="append")
+  argp.add_argument("--stdout_file", default="")
+  argp.add_argument("--stderr_file", default="")
+  argp.add_argument("--main", default="Main")
+  argp.add_argument("--expected_exit_code", default=0)
 
-  argv = list(sys.argv[1:])
   # Python parser requires the format --key=--value, since without the equals symbol
   # it looks like the required value has been omitted and there is just another flag.
   # For example, '--args --foo --host --64' will become '--arg=--foo --host --64'
@@ -224,16 +116,153 @@
         "-Xcompiler-option", "--compiler-only-option"
     ]:
       argv[i] += "=" + argv.pop(i + 1)
-# Accept single-dash arguments as if they were double-dash arguments.
-# For exmpample, '-Xcompiler-option' becomes '--Xcompiler-option'
-# became single-dash can be used only with single-letter arguments.
+
+  # Accept single-dash arguments as if they were double-dash arguments.
+  # For exmpample, '-Xcompiler-option' becomes '--Xcompiler-option'
+  # became single-dash can be used only with single-letter arguments.
   for i, arg in list(enumerate(argv)):
     if arg.startswith("-") and not arg.startswith("--"):
       argv[i] = "-" + arg
     if arg == "--":
       break
 
-  args = argp.parse_args(argv)
+  return argp.parse_args(argv)
+
+
+def default_run(ctx, args, **kwargs):
+  # Clone the args so we can modify them without affecting args in the caller.
+  args = Namespace(**vars(args))
+
+  # Overwrite args based on the named parameters.
+  # E.g. the caller can do `default_run(args, jvmti=True)` to modify args.jvmti.
+  for name, new_value in kwargs.items():
+    old_value = getattr(args, name)
+    assert isinstance(new_value, old_value.__class__), name + " should have type " + str(old_value.__class__)
+    if isinstance(old_value, list):
+      setattr(args, name, old_value + new_value)  # Lists get merged.
+    else:
+      setattr(args, name, new_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")
+
+  # Script debugging: Record executed commands, but don't actually run the main test.
+  # This makes it possible the extract the test commands without waiting for days.
+  # This will make tests fail since there is no stdout.  Use with large -j value.
+  ART_TEST_DRY_RUN = os.environ.get("ART_TEST_DRY_RUN")
+
+  def run(cmdline: str,
+          env={},
+          stdout_file=None,
+          stderr_file=None,
+          check=True,
+          expected_exit_code=0,
+          save_cmd=True) -> subprocess.CompletedProcess:
+    env.setdefault("PATH", PATH)  # Ensure that PATH is always set.
+    env = {k: v for k, v in env.items() if v != None}  # Filter "None" entries.
+    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:
+        # 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")
+      if ART_TEST_DRY_RUN and ("dalvikvm" in cmdline or
+                               "adb shell chroot" in cmdline):
+        cmdline = "true"  # We still need to run some command, so run the no-op "true" binary instead.
+    proc = subprocess.run([cmdline],
+                          shell=True,
+                          env=env,
+                          encoding="utf8",
+                          capture_output=True)
+
+    # Save copy of the output on disk.
+    if stdout_file:
+      with open(stdout_file, "a") as f:
+        f.write(proc.stdout)
+        if proc.returncode != 0:
+          f.write("exit status: {}\n".format(proc.returncode))
+    if stderr_file:
+      with open(stderr_file, "a") as f:
+        f.write(proc.stderr)
+
+    # Check the exit code.
+    if (check and proc.returncode != expected_exit_code) or VERBOSE:
+      print("$ " + cmdline)
+      print(proc.stdout or "", file=sys.stdout, end="", flush=True)
+      print(proc.stderr or "", file=sys.stderr, end="", flush=True)
+    if (check and proc.returncode != expected_exit_code):
+      suffix = ""
+      if proc.returncode == 124:
+        suffix = " (TIME OUT)"
+      elif expected_exit_code != 0:
+        suffix = " (expected {})".format(expected_exit_code)
+      raise Exception("Command returned exit code {}{}".format(proc.returncode, suffix))
+
+    return proc
+
+  class Adb():
+
+    def __init__(self):
+      self.env = {
+          "ADB_VENDOR_KEYS": os.environ.get("ADB_VENDOR_KEYS"),
+          "ANDROID_SERIAL": os.environ.get("ANDROID_SERIAL"),
+          "PATH": os.environ.get("PATH"),
+      }
+
+    def root(self) -> None:
+      run("adb root", self.env)
+
+    def wait_for_device(self) -> None:
+      run("adb wait-for-device", self.env)
+
+    def shell(self, cmdline: str, **kwargs) -> subprocess.CompletedProcess:
+      return run("adb shell " + cmdline, self.env, **kwargs)
+
+    def push(self, src: str, dst: str, **kwargs) -> None:
+      run(f"adb push {src} {dst}", self.env, **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.
+  bold_red = ""
+  if sys.stdout.isatty():
+    if int(subprocess.run(["tput", "colors"], capture_output=True).stdout) >= 1:
+      bold_red = subprocess.run(["tput", "bold"],
+                                text=True,
+                                capture_output=True).stdout.strip()
+      bold_red += subprocess.run(["tput", "setaf", "1"],
+                                 text=True,
+                                 capture_output=True).stdout.strip()
+
+  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", "")
+
+  assert ANDROID_BUILD_TOP, "Did you forget to run `lunch`?"
+
+  def msg(msg: str):
+    if VERBOSE:
+      print(msg)
 
   ANDROID_ROOT = args.android_root
   ANDROID_ART_ROOT = args.android_art_root
@@ -282,7 +311,7 @@
   ISA = "x86"
   LIBRARY_DIRECTORY = "lib"
   TEST_DIRECTORY = "nativetest"
-  MAIN = args.test_args.pop(0)
+  MAIN = args.main
   OPTIMIZE = args.optimize
   PREBUILD = args.prebuild
   RELOCATE = args.relocate
@@ -353,7 +382,7 @@
     TIME_OUT_EXTRA += 1200
   for arg in args.testlib:
     ARGS += f" {arg}"
-  for arg in args.args:
+  for arg in args.test_args:
     ARGS += f" {arg}"
   for arg in args.compiler_only_option:
     COMPILE_FLAGS += f" {arg}"
@@ -495,8 +524,6 @@
   if CREATE_ANDROID_ROOT:
     ANDROID_ROOT = f"{DEX_LOCATION}/android-root"
 
-  test_args = (" " + " ".join(args.test_args)) if args.test_args else ""
-
   if ZYGOTE == "":
     if OPTIMIZE:
       if VERIFY == "y":
@@ -540,9 +567,7 @@
   elif DEBUGGER == "agent":
     PORT = 12345
     # TODO Support ddms connection and support target.
-    if not HOST:
-      error_msg("--debug-agent not supported yet for target!")
-      sys.exit(1)
+    assert HOST, "--debug-agent not supported yet for target!"
     AGENTPATH = DEBUGGER_AGENT
     if WRAP_DEBUGGER_AGENT:
       WRAPPROPS = f"{ANDROID_ROOT}/{LIBRARY_DIRECTORY}/libwrapagentpropertiesd.so"
@@ -626,7 +651,7 @@
       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 = f"{JAVA} {DEBUGGER_OPTS} {JVM_VERIFY_ARG} -Xmx256m -classpath classes:classes2 {FLAGS} {MAIN} {test_args} {ARGS}"
+    cmdline = f"{JAVA} {DEBUGGER_OPTS} {JVM_VERIFY_ARG} -Xmx256m -classpath classes:classes2 {FLAGS} {MAIN} {ARGS}"
     if CREATE_RUNNER:
       with open("runit.sh", "w") as f:
         f.write("#!/bin/bash")
@@ -635,10 +660,14 @@
       os.chmod("runit.sh", 0o777)
       pwd = os.getcwd()
       print(f"Runnable test script written to {pwd}/runit.sh")
-      sys.exit(0)
+      return
     else:
-      exit_value = run(cmdline, env, check=False, quiet=False).returncode
-      sys.exit(exit_value)
+      run(cmdline,
+          env,
+          stdout_file=args.stdout_file,
+          stderr_file=args.stderr_file,
+          expected_exit_code=args.expected_exit_code)
+      return
 
   b_path = get_apex_bootclasspath(HOST)
   b_path_locations = get_apex_bootclasspath_locations(HOST)
@@ -667,17 +696,11 @@
   DALVIKVM_BOOT_OPT = f"-Ximage:{BOOT_IMAGE}"
 
   if USE_GDB_DEX2OAT:
-    if not HOST:
-      print(
-          "The --gdb-dex2oat option is not yet implemented for target.",
-          file=sys.stderr)
-      sys.exit(1)
+    assert HOST, "The --gdb-dex2oat option is not yet implemented for target."
 
   if USE_GDB:
-    if USE_GDBSERVER:
-      error_msg("Cannot pass both --gdb and --gdbserver at the same time!")
-      sys.exit(1)
-    elif not HOST:
+    assert not USE_GDBSERVER, "Cannot pass both --gdb and --gdbserver at the same time!"
+    if not HOST:
       # We might not have any hostname resolution if we are using a chroot.
       GDB = f"{GDBSERVER_DEVICE} --no-startup-with-shell 127.0.0.1{GDBSERVER_PORT}"
     else:
@@ -697,9 +720,7 @@
     else:
       GDB = f"{GDBSERVER_HOST} {GDBSERVER_PORT}"
 
-      if shutil.which(GDBSERVER_HOST) is None:
-        error_msg(f"{GDBSERVER_HOST} is not available")
-        sys.exit(1)
+      assert shutil.which(GDBSERVER_HOST), f"{GDBSERVER_HOST} is not available"
 
   if INTERPRETER:
     INT_OPTS += " -Xint"
@@ -733,9 +754,8 @@
   if BIONIC:
     # 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 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)
+    assert path.exists(f"{OUT_DIR}/soong/host/linux_bionic-x86"), (
+        "linux_bionic-x86 target doesn't seem to have been built!")
     # Set TIMEOUT_DUMPER manually so it works even with apex's
     TIMEOUT_DUMPER = f"{OUT_DIR}/soong/host/linux_bionic-x86/bin/signal_dumper"
 
@@ -754,12 +774,7 @@
   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)
+  assert len(VDEX_NAME) <= max_filename_size, "Dex location path too long"
 
   if HOST:
     # On host, run binaries (`dex2oat(d)`, `dalvikvm`, `profman`) from the `bin`
@@ -860,13 +875,12 @@
     # 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 (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!")
-      sys.exit(1)
+    if isdir(f"{ANDROID_BUILD_TOP}/{CLANG_BASE}/{PREBUILT_NAME}"):
+      assert isdir(CLANG_PREBUILT_HOST_PATH), (
+          "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!")
 
     # The lldb-server binary is a dependency of lldb.
     os.environ[
@@ -881,7 +895,7 @@
     return f"{CLANG_PREBUILT_HOST_PATH}/bin/lldb.sh"
 
   def write_dex2oat_cmdlines(name: str):
-    global dex2oat_cmdline, dm_cmdline, vdex_cmdline
+    nonlocal dex2oat_cmdline, dm_cmdline, vdex_cmdline
 
     class_loader_context = ""
     enable_app_image = False
@@ -891,7 +905,7 @@
     # If the name ends in -ex then this is a secondary dex file
     if name.endswith("-ex"):
       # Lazily realize the default value in case DEX_LOCATION/TEST_NAME change
-      global SECONDARY_CLASS_LOADER_CONTEXT
+      nonlocal SECONDARY_CLASS_LOADER_CONTEXT
       if SECONDARY_CLASS_LOADER_CONTEXT == "":
         if SECONDARY_DEX == "":
           # Tests without `--secondary` load the "-ex" jar in a separate PathClassLoader
@@ -908,7 +922,7 @@
     if enable_app_image:
       app_image = f"--app-image-file={DEX_LOCATION}/oat/{ISA}/{name}.art --resolve-startup-const-strings=true"
 
-    global GDB_DEX2OAT, GDB_DEX2OAT_ARGS
+    nonlocal GDB_DEX2OAT, GDB_DEX2OAT_ARGS
     if USE_GDB_DEX2OAT:
       prebuilt_lldb_path = get_prebuilt_lldb_path()
       GDB_DEX2OAT = f"{prebuilt_lldb_path} -f"
@@ -1031,7 +1045,7 @@
                     {QUOTED_DALVIKVM_BOOT_OPT} \
                     {TMP_DIR_OPTION} \
                     -XX:DumpNativeStackOnSigQuit:false \
-                    -cp {DALVIKVM_CLASSPATH} {MAIN} {ARGS} {test_args}".strip()
+                    -cp {DALVIKVM_CLASSPATH} {MAIN} {ARGS}"
 
   if SIMPLEPERF:
     dalvikvm_cmdline = f"simpleperf record {dalvikvm_cmdline} && simpleperf report"
@@ -1044,6 +1058,7 @@
       args.append(arg)
     return " ".join(args)
 
+
 # Remove whitespace.
 
   dex2oat_cmdline = sanitize_dex2oat_cmdline(dex2oat_cmdline)
@@ -1180,9 +1195,9 @@
       "PATH": f"{PREPEND_TARGET_PATH}:$PATH",
     }  # pyformat: disable
 
-    def run_cmd(cmdline: str, env={}, check: bool = True) -> int:
-      if cmdline == "true":  # Noop command which just executes the linux 'true' binary.
-        return 0
+    def run_cmd(cmdline: str, env={}, **kwargs) -> subprocess.CompletedProcess:
+      if cmdline == "true" or DRY_RUN:
+        return run('true')  # Noop command which just executes the linux 'true' binary.
       cmdline = (f"cd {DEX_LOCATION} && " +
                  "".join(f"export {k}={v} && " for k, v in env.items()) +
                  cmdline)
@@ -1194,13 +1209,8 @@
         adb.push(
             cmdfile.name, f"{CHROOT_DEX_LOCATION}/cmdline.sh", save_cmd=False)
         run('echo cmdline.sh "' + cmdline.replace('"', '\\"') + '"')
-      if not DRY_RUN:
-        chroot_prefix = f"chroot {CHROOT} " if CHROOT else ""
-        return adb.shell(
-            f"{chroot_prefix} sh {DEX_LOCATION}/cmdline.sh",
-            check=check,
-            quiet=False).returncode
-      return 0
+      chroot_prefix = f"chroot {CHROOT} " if CHROOT else ""
+      return adb.shell(f"{chroot_prefix} sh {DEX_LOCATION}/cmdline.sh", **kwargs)
 
     if VERBOSE and (USE_GDB or USE_GDBSERVER):
       print(f"Forward {GDBSERVER_PORT} to local port and connect GDB")
@@ -1213,10 +1223,11 @@
     run_cmd(f"{vdex_cmdline}", env)
     run_cmd(f"{strip_cmdline}")
     run_cmd(f"{sync_cmdline}")
-    exit_status = run_cmd(
-        f"{timeout_prefix} {dalvikvm_cmdline}", env, check=False)
-
-    sys.exit(exit_status)
+    run_cmd(f"{timeout_prefix} {dalvikvm_cmdline}",
+            env,
+            stdout_file=args.stdout_file,
+            stderr_file=args.stderr_file,
+            expected_exit_code=args.expected_exit_code)
   else:
     # Host run.
     if USE_ZIPAPEX or USE_EXRACTED_ZIPAPEX:
@@ -1323,7 +1334,7 @@
       os.chmod("{DEX_LOCATION}/runit.sh", 0o777)
       print(f"Runnable test script written to {DEX_LOCATION}/runit.sh")
     if DRY_RUN:
-      sys.exit(0)
+      return
 
     if USE_GDB:
       # When running under gdb, we cannot do piping and grepping...
@@ -1335,12 +1346,11 @@
       subprocess.run(cmdline, env=env, shell=True)
     else:
       if TIME_OUT != "gdb":
-        proc = run(cmdline, env, check=False, quiet=False)
-        exit_value = proc.returncode
-        # Add extra detail if time out is enabled.
-        if exit_value == 124 and TIME_OUT == "timeout":
-          print("\e[91mTEST TIMED OUT!\e[0m", file=sys.stderr)
-        sys.exit(exit_value)
+        run(cmdline,
+            env,
+            stdout_file=args.stdout_file,
+            stderr_file=args.stderr_file,
+            expected_exit_code=args.expected_exit_code)
       else:
         # With a thread dump that uses gdb if a timeout.
         proc = run(cmdline, check=False)
@@ -1353,12 +1363,6 @@
         #     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.
-          sys.exit(0)
-        else:
-          # The test failed or timed out.
-          if test_exit_status == 124:
-            # The test timed out.
-            print("\e[91mTEST TIMED OUT!\e[0m", file=sys.stderr)
-          sys.exit(test_exit_status)
+        if proc.returncode == 124 and TIME_OUT == "timeout":
+          print("\e[91mTEST TIMED OUT!\e[0m", file=sys.stderr)
+        assert proc.returncode == args.expected_exit_code, f"exit code: {proc.returncode}"
diff --git a/test/run-test b/test/run-test
index f5820fa..70c9b97 100755
--- a/test/run-test
+++ b/test/run-test
@@ -14,7 +14,43 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import os, sys, glob, re, shutil, subprocess
+import os, sys, glob, re, shutil, subprocess, shlex, resource
+
+import etc.default_run
+
+from importlib.machinery import SourceFileLoader
+from pathlib import Path
+
+
+# Helper class which allows us to access the environment using syntax sugar.
+# E.g. `env.ANDROID_BUILD_TOP` instead of `os.environ["ANDROID_BUILD_TOP"]`.
+class Environment:
+
+  def __getattr__(self, name):
+    return os.environ.get(name)
+
+  def __setattr__(self, name, value):
+    os.environ[name] = str(value)
+
+
+# Context passed to individual tests to let them customize the behaviour.
+class RunTestContext:
+
+  def __init__(self):
+    self.env = Environment()
+
+  def echo(self, text):
+    with open(test_stdout, "a") as f:
+      f.write(text + "\n")
+
+  # Let the test execute arbitrary bash command.
+  def bash(self, cmd):
+    subprocess.run(cmd, shell=True)
+
+  # Execute the default runner (possibly with modified arguments).
+  def default_run(self, args, **kwargs):
+    etc.default_run.default_run(self, args, **kwargs)
+
 
 # TODO: Replace with 'def main():' (which might change variables from globals to locals)
 if True:
@@ -57,8 +93,7 @@
   def error(msg) -> None:
     print(msg, file=sys.stderr, flush=True)
 
-# ANDROID_BUILD_TOP is not set in a build environment.
-
+  # ANDROID_BUILD_TOP is not set in a build environment.
   ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP")
   if not ANDROID_BUILD_TOP:
     export("ANDROID_BUILD_TOP", oldwd)
@@ -860,7 +895,7 @@
 
   run_args += [f'--testlib "{testlib}"']
 
-  run(f"ulimit -f {file_ulimit}")
+  resource.setrlimit(resource.RLIMIT_FSIZE, (file_ulimit * 1024, resource.RLIM_INFINITY))
 
   # Extract run-test data from the zip file.
   shutil.rmtree(tmp_dir)
@@ -884,84 +919,51 @@
   joined_run_args = " ".join(run_args)
   joined_args = " ".join(args)
 
-  good = "no"
-  good_run = "yes"
-  export("TEST_RUNTIME", runtime)
-  if dev_mode == "yes":
-    gdb_mode = "--gdb" in run_args
-    gdbserver_mode = "--gdbserver" in run_args
-    capture_output = not (gdb_mode or gdbserver_mode)
-    if gdb_mode:
-      export("TERM", os.environ.get("TERM"))
-    verbose(f"{test_dir}: running...")
-    run_exit = run(
-        f"./{run_cmd} {joined_run_args} {joined_args}",
-        check=False,
-        capture_output=capture_output).returncode
+  # Execute the "run" method in the per-test specific script file.
+  def run_test_script():
+    parsed_args = etc.default_run.parse_args(shlex.split(" ".join(run_args + args)))
+    parsed_args.stdout_file = os.path.join(tmp_dir, test_stdout)
+    parsed_args.stderr_file = os.path.join(tmp_dir, test_stderr)
 
-    if run_exit == 0:
-      if run_checker == "yes":
-        if target_mode == "yes":
-          run(f'adb pull "{chroot}/{cfg_output_dir}/{cfg_output}"')
-        checker_exit = run(
-            '"{checker}" {checker_args} "{cfg_output}" "{tmp_dir}"',
-            check=False).returncode
-        if checker_exit == 0:
-          good = "yes"
-        verbose(f"checker exit status: {checker_exit}")
-      else:
-        good = "yes"
-    verbose(f"run exit status: {run_exit}")
-  elif update_mode == "yes":
-    verbose(f"{test_dir}: running...")
-    proc = run(
-        f"./{run_cmd} {joined_run_args} {joined_args} >{test_stdout} 2>{test_stderr}",
-        check=False)
-    if run_checker == "yes":
-      if target_mode == "yes":
-        run(f'adb pull "{chroot}/{cfg_output_dir}/{cfg_output}"')
-      run(f'"{checker}" -q {checker_args} "{cfg_output}" "{tmp_dir}" >>"{test_stdout}" 2>>"{test_stderr}"'
-         )
-    run(f'''sed -e 's/[[:cntrl:]]$//g' <"{test_stdout}" >"{td_expected_stdout}"'''
-       )
-    run(f'''sed -e 's/[[:cntrl:]]$//g' <"{test_stderr}" >"{td_expected_stderr}"'''
-       )
-    good = "yes"
-  else:
-    verbose(f"{test_dir}: running...")
-    proc = run(
-        f"./{run_cmd} {joined_run_args} {joined_args} >{test_stdout} 2>{test_stderr}",
-        check=False)
-    run_exit = proc.returncode
-    if run_exit != 0:
-      error(f"run exit status: {run_exit}")
-      good_run = "no"
-    elif run_checker == "yes":
-      if target_mode == "yes":
-        run(f'adb pull "{chroot}/{cfg_output_dir}/{cfg_output}"')
-      proc = run(
-          f'"{checker}" -q {checker_args} "{cfg_output}" "{tmp_dir}" >>"{test_stdout}" 2>>"{test_stderr}"',
-          check=False)
-      checker_exit = proc.returncode
-      if checker_exit != 0:
-        error(f"checker exit status: {checker_exit}")
-        good_run = "no"
-      else:
-        good_run = "yes"
+    ctx = RunTestContext()
+    script = os.path.join(tmp_dir, "run")
+    if os.path.exists(script):
+      module = SourceFileLoader("run_" + TEST_NAME, script).load_module()
+      module.run(ctx, parsed_args)
     else:
-      good_run = "yes"
-    if os.path.exists(check_cmd):
-      run(f'./{check_cmd} "{expected_stdout}" "{test_stdout}" "{expected_stderr}" "{test_stderr}"')
-    proc = run(
-      f'diff --strip-trailing-cr -u "{expected_stdout}" "{test_stdout}" &&'
-      f'diff --strip-trailing-cr -u "{expected_stderr}" "{test_stderr}"',
-      check=False  # Don't crash on non-zero exit code.
-    )
-    if proc.returncode == 0:
-      if good_run == "yes":
-        # test_stdout == expected_stdout && test_stderr == expected_stderr
-        good = "yes"
-        verbose(f"${test_dir}: succeeded!")
+      etc.default_run.default_run(ctx, parsed_args)
+
+  # Test might not execute anything but we still expect the output files to exist.
+  Path(test_stdout).touch()
+  Path(test_stderr).touch()
+
+  good = "no"
+  export("TEST_RUNTIME", runtime)
+
+  verbose(f"{test_dir}: running...")
+  run_test_script()
+  # NB: There is no exit code or return value.
+  # Failing tests just raise python exception.
+  os.chdir(tmp_dir)
+  if run_checker == "yes":
+    if target_mode == "yes":
+      run(f'adb pull "{chroot}/{cfg_output_dir}/{cfg_output}"')
+    run(f'"{checker}" -q {checker_args} "{cfg_output}" "{tmp_dir}" >>"{test_stdout}" 2>>"{test_stderr}"')
+  if update_mode == "yes":
+    run(f'''sed -e 's/[[:cntrl:]]$//g' <"{test_stdout}" >"{td_expected_stdout}"''')
+    run(f'''sed -e 's/[[:cntrl:]]$//g' <"{test_stderr}" >"{td_expected_stderr}"''')
+    good = "yes"
+
+  if os.path.exists(check_cmd):
+    run(f'./{check_cmd} "{expected_stdout}" "{test_stdout}" "{expected_stderr}" "{test_stderr}"')
+  proc = run(
+    f'diff --strip-trailing-cr -u "{expected_stdout}" "{test_stdout}" &&'
+    f'diff --strip-trailing-cr -u "{expected_stderr}" "{test_stderr}"',
+    check=False  # Don't crash on non-zero exit code.
+  )
+  if proc.returncode == 0:
+    good = "yes"
+    verbose(f"${test_dir}: succeeded!")
 
   if good != "yes" and update_mode != "yes":
     error(f"{test_dir}: FAILED!")
diff --git a/test/run-test-build.py b/test/run-test-build.py
index aa2cde1..dd0a89c 100755
--- a/test/run-test-build.py
+++ b/test/run-test-build.py
@@ -22,6 +22,7 @@
 import argparse, os, shutil, subprocess, glob, re, json, multiprocessing, pathlib, fcntl
 import art_build_rules
 from importlib.machinery import SourceFileLoader
+from os.path import join, basename
 
 import art_build_rules
 
@@ -42,22 +43,8 @@
 def copy_sources(args, tmp, mode, srcdir):
   """Copy test files from Android tree into the build sandbox and return its path."""
 
-  join = os.path.join
-  test = os.path.basename(srcdir)
-  dstdir = join(tmp, mode, test)
-
-  # Copy all source files to the temporary directory.
+  dstdir = join(tmp, mode, basename(srcdir))
   shutil.copytree(srcdir, dstdir)
-
-  # Copy the default scripts if the test does not have a custom ones.
-  for name in ["run"]:
-    src, dst = f"art/test/etc/default-{name}", join(dstdir, name)
-    if os.path.exists(dst):
-      shutil.copy2(src, dstdir)  # Copy default script next to the custom script.
-    else:
-      shutil.copy2(src, dst)  # Use just the default script.
-    os.chmod(dst, 0o755)
-
   return dstdir
 
 def build_test(args, mode, build_top, sbox, dstdir):
diff --git a/test/utils/get-device-test-native-lib-path b/test/utils/get-device-test-native-lib-path
deleted file mode 100755
index 21ea98c..0000000
--- a/test/utils/get-device-test-native-lib-path
+++ /dev/null
@@ -1,47 +0,0 @@
-#! /bin/bash
-#
-# Copyright 2019 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.
-
-usage() {
-  cat >&2 <<EOF
-Determine the 32- or 64-bit architecture of a device and print the path to
-native libraries installed on the device for testing purposes.
-
-Usage:
-  $0 --32    Select the 32-bit architecture
-  $0 --64    Select the 64-bit architecture
-EOF
-  exit 1
-}
-
-if [[ $# -ne 1 ]]; then
-  usage
-fi
-
-case "$1" in
-  (--32) TEST_DIRECTORY="nativetest";;
-  (--64) TEST_DIRECTORY="nativetest64";;
-  (*) usage;;
-esac
-
-if [[ -z "$ANDROID_BUILD_TOP" ]]; then
-  echo 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?'
-  exit 1
-fi
-
-bitness_flag=$1
-ISA=$("$ANDROID_BUILD_TOP/art/test/utils/get-device-isa" "$bitness_flag")
-
-echo "/data/${TEST_DIRECTORY}/art/${ISA}"