diff --git a/e2etests/MODULE.bazel b/e2etests/MODULE.bazel
index 9f08161fed2..a28cf92e75e 100644
--- a/e2etests/MODULE.bazel
+++ b/e2etests/MODULE.bazel
@@ -1,5 +1,6 @@
bazel_dep(name = "gazelle", version = "0.40.0")
bazel_dep(name = "rules_go", version = "0.50.1")
+bazel_dep(name = "rules_python", version = "1.1.0")
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.1")
diff --git a/e2etests/cvd/BUILD.bazel b/e2etests/cvd/BUILD.bazel
index 43fa9a4c318..f1769f77602 100644
--- a/e2etests/cvd/BUILD.bazel
+++ b/e2etests/cvd/BUILD.bazel
@@ -1,4 +1,25 @@
-load("boot_tests.bzl", "cvd_command_boot_test", "cvd_load_boot_test", "launch_cvd_boot_test")
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+load("boot_tests.bzl", "cvd_command_boot_test", "cvd_cts_test", "cvd_load_boot_test", "launch_cvd_boot_test")
+
+py_library(
+ name = "convert_xts_xml_to_junit_xml",
+ srcs = ["convert_xts_xml_to_junit_xml.py"],
+ srcs_version = "PY3",
+)
+
+py_test(
+ name = "convert_xts_xml_to_junit_xml_test",
+ srcs = ["convert_xts_xml_to_junit_xml_test.py"],
+ srcs_version = "PY3",
+ deps = [":convert_xts_xml_to_junit_xml"],
+)
+
+py_binary(
+ name = "convert_xts_xml_to_junit_xml_bin",
+ srcs = ["convert_xts_xml_to_junit_xml.py"],
+ srcs_version = "PY3",
+ main = "convert_xts_xml_to_junit_xml.py",
+)
# cvd fetch + launch_cvd tests
launch_cvd_boot_test(
@@ -106,13 +127,83 @@ cvd_command_boot_test(
target = "aosp_cf_x86_64_only_phone-userdebug",
)
-cvd_command_boot_test(
- name = "this_test_will_fail",
- branch = "aosp-android-latest-release",
- cvd_command = [
- "this_command_does_not_exist",
+cvd_cts_test(
+ name = "aosp_android_latest_swiftshader_deqp_vk_smoke_tests",
+ cuttlefish_branch = "aosp-android-latest-release",
+ cuttlefish_target = "aosp_cf_x86_64_only_phone-userdebug",
+ cuttlefish_create_args = [
+ "--gpu_mode=guest_swiftshader",
],
- target = "aosp_cf_x86_64_only_phone-userdebug",
+ cts_branch = "aosp-android15-tests-release",
+ cts_target = "test_suites_x86_64",
+ cts_args = [
+ "--include-filter=CtsDeqpTestCases",
+ "--module-arg=CtsDeqpTestCases:include-filter:dEQP-VK.api.smoke*",
+ ],
+)
+
+cvd_cts_test(
+ name = "aosp_android_latest_auto_enables_gfxstream_test",
+ cuttlefish_branch = "aosp-android-latest-release",
+ cuttlefish_target = "aosp_cf_x86_64_only_phone-userdebug",
+ cuttlefish_create_args = [
+ "--gpu_mode=auto",
+ ],
+ cts_branch = "aosp-android15-tests-release",
+ cts_target = "test_suites_x86_64",
+ cts_args = [
+ "--include-filter=CuttlefishGraphicsConfigurationTest",
+ "--module-arg=CuttlefishGraphicsConfigurationTest:set-option:expected-egl:emulation",
+ ],
+ tags = ["requires_gpu"],
+)
+
+cvd_cts_test(
+ name = "aosp_android_latest_gfxstream_deqp_vk_smoke_tests",
+ cuttlefish_branch = "aosp-android-latest-release",
+ cuttlefish_target = "aosp_cf_x86_64_only_phone-userdebug",
+ cuttlefish_create_args = [
+ "--gpu_mode=gfxstream",
+ ],
+ cts_branch = "aosp-android15-tests-release",
+ cts_target = "test_suites_x86_64",
+ cts_args = [
+ "--include-filter=CtsDeqpTestCases",
+ "--module-arg=CtsDeqpTestCases:include-filter:dEQP-VK.api.smoke*",
+ ],
+ tags = ["requires_gpu"],
+)
+
+cvd_cts_test(
+ name = "aosp_android_latest_gfxstream_guest_angle_host_swiftshader_deqp_vk_smoke_tests",
+ cuttlefish_branch = "aosp-android-latest-release",
+ cuttlefish_target = "aosp_cf_x86_64_only_phone-userdebug",
+ cuttlefish_create_args = [
+ "--gpu_mode=gfxstream_guest_angle_host_swiftshader",
+ ],
+ cts_branch = "aosp-android15-tests-release",
+ cts_target = "test_suites_x86_64",
+ cts_args = [
+ "--include-filter=CtsDeqpTestCases",
+ "--module-arg=CtsDeqpTestCases:include-filter:dEQP-VK.api.smoke*",
+ ],
+ # TODO: still need vulkan loader available in kokoro images.
+ tags = ["requires_gpu"],
+)
+
+cvd_cts_test(
+ name = "aosp_android_latest_gfxstream_guest_angle_host_swiftshader_graphics_cts_tests",
+ cuttlefish_branch = "aosp-android-latest-release",
+ cuttlefish_target = "aosp_cf_x86_64_only_phone-userdebug",
+ cuttlefish_create_args = [
+ "--gpu_mode=gfxstream_guest_angle_host_swiftshader",
+ ],
+ cts_branch = "aosp-android15-tests-release",
+ cts_target = "test_suites_x86_64",
+ cts_args = [
+ "--include-filter=CtsGraphicsTestCases",
+ ],
+ # TODO: still need vulkan loader available in kokoro images.
tags = ["requires_gpu"],
)
diff --git a/e2etests/cvd/boot_tests.bzl b/e2etests/cvd/boot_tests.bzl
index aa7264a6bbe..39d0b051380 100644
--- a/e2etests/cvd/boot_tests.bzl
+++ b/e2etests/cvd/boot_tests.bzl
@@ -49,3 +49,49 @@ def cvd_command_boot_test(name, branch, target, cvd_command = [], credential_sou
"no-sandbox",
] + tags,
)
+
+def cvd_cts_test(
+ name,
+ cuttlefish_branch,
+ cuttlefish_target,
+ cts_branch,
+ cts_target,
+ cts_args,
+ cuttlefish_create_args = [],
+ credential_source = "",
+ substitutions = "",
+ tags = []):
+
+ args = [
+ "--cuttlefish-create-args=\"" + " ".join(cuttlefish_create_args) + "\"",
+ "--cuttlefish-fetch-branch=" + cuttlefish_branch,
+ "--cuttlefish-fetch-target=" + cuttlefish_target,
+ "--xml-test-result-converter-path=$(location //cvd:convert_xts_xml_to_junit_xml_bin)",
+ "--xts-args=\"" + " ".join(cts_args) + "\"",
+ "--xts-fetch-branch=" + cts_branch,
+ "--xts-fetch-target=" + cts_target,
+ "--xts-type=cts",
+ ]
+
+ if credential_source:
+ args += ["--credential-source=" + credential_source]
+
+ if substitutions:
+ args += ["--substitutions=\"" + ",".join(substitutions) + "\""]
+
+
+ native.sh_test(
+ name = name,
+ size = "enormous",
+ srcs = ["cvd_xts_test.sh"],
+ args = args,
+ data = [
+ "//cvd:convert_xts_xml_to_junit_xml_bin",
+ ],
+ tags = [
+ "exclusive",
+ "external",
+ "no-sandbox",
+ ] + tags,
+ )
+
diff --git a/e2etests/cvd/convert_xts_xml_to_junit_xml.py b/e2etests/cvd/convert_xts_xml_to_junit_xml.py
new file mode 100644
index 00000000000..5497dd41efd
--- /dev/null
+++ b/e2etests/cvd/convert_xts_xml_to_junit_xml.py
@@ -0,0 +1,118 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2025 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.
+#
+"""Converts Android CTS test_result.xml files to Bazel result xml files.
+
+JUnit XML structure: https://github.com/testmoapp/junitxml
+
+"""
+
+import argparse
+import os
+import re
+import sys
+
+from xml.etree import ElementTree
+import xml.dom.minidom
+
+def to_pretty_xml(root, indent=" "):
+ return xml.dom.minidom.parseString(ElementTree.tostring(root, encoding='UTF-8', xml_declaration=True)).toprettyxml(indent)
+
+def convert_xts_xml_to_junit_xml(input_xml_string):
+ input_xml_root = ElementTree.ElementTree(ElementTree.fromstring(input_xml_string)).getroot()
+
+ output_xml_testsuites = ElementTree.Element("testsuites")
+ output_xml_testsuites.set('name', 'AllTests')
+
+ output_xml_testsuites_fail = 0
+ output_xml_testsuites_total = 0
+
+ for input_xml_result in input_xml_root.iter('Result'):
+ for input_xml_module in input_xml_result.iter('Module'):
+ # e.g. CtsDeqpTestCases
+ input_module_name = input_xml_module.get('name')
+
+ output_xml_module_testsuite = ElementTree.SubElement(output_xml_testsuites, "testsuite")
+ output_xml_module_testsuite.set('name', input_module_name)
+
+ output_xml_module_testsuite_fail = 0
+ output_xml_module_testsuite_total = 0
+
+ for input_xml_testcase in input_xml_module.iter('TestCase'):
+ # e.g. dEQP-EGL.functional.image.create
+ input_testcase_name = input_xml_testcase.get('name')
+
+ output_xml_testcase_testsuite = ElementTree.SubElement(output_xml_module_testsuite, "testsuite")
+ output_xml_testcase_testsuite.set('name', input_testcase_name)
+
+ output_xml_testcase_testsuite_fail = 0
+ output_xml_testcase_testsuite_total = 0
+
+ for input_xml_test in input_xml_testcase.iter('Test'):
+ # e.g. gles2_android_native_rgba4_texture
+ input_test_name = input_xml_test.get('name')
+
+ # e.g. dEQP-EGL.functional.image.create#gles2_android_native_rgba4_texture
+ output_test_name = input_testcase_name + '#' + input_test_name
+
+ output_xml_testcase = ElementTree.SubElement(output_xml_testcase_testsuite, "testcase")
+ output_xml_testcase.set('name', output_test_name)
+
+ input_test_result = input_xml_test.get('result')
+ if input_test_result == 'failed':
+ output_xml_testcase_failure = ElementTree.SubElement(output_xml_testcase, "failure")
+ output_xml_testcase_failure.set('name', output_test_name)
+
+ output_xml_testcase_testsuite_fail += 1
+
+ output_xml_testcase_testsuite_total += 1
+
+ output_xml_testcase_testsuite.set('tests', str(output_xml_testcase_testsuite_total))
+ output_xml_testcase_testsuite.set('failures', str(output_xml_testcase_testsuite_fail))
+
+ output_xml_module_testsuite_fail += output_xml_testcase_testsuite_fail
+ output_xml_module_testsuite_total += output_xml_testcase_testsuite_total
+
+ output_xml_module_testsuite.set('tests', str(output_xml_module_testsuite_total))
+ output_xml_module_testsuite.set('failures', str(output_xml_module_testsuite_fail))
+
+ output_xml_testsuites_fail += output_xml_module_testsuite_fail
+ output_xml_testsuites_total += output_xml_testcase_testsuite_total
+
+ output_xml_testsuites.set('tests', str(output_xml_module_testsuite_total))
+ output_xml_testsuites.set('failures', str(output_xml_module_testsuite_fail))
+
+ output_xml_string = to_pretty_xml(output_xml_testsuites)
+
+ return output_xml_string
+
+def main():
+ parser = argparse.ArgumentParser(description="Converts Android CTS test_result.xml files to Bazel result xml files")
+ parser.add_argument("--input_xml_file", help="The path to input Android CTS test_result.xml file.")
+ parser.add_argument("--output_xml_file", help="The path for the output xml file that is digestable by bazel.")
+ args = parser.parse_args()
+
+ input_xml_string = None
+ with open(args.input_xml_file, 'r') as intput_xml_file:
+ input_xml_string = intput_xml_file.read()
+
+ output_xml_string = convert_xts_xml_to_junit_xml(input_xml_string)
+
+ with open(args.output_xml_file, 'w') as output_xml_file:
+ output_xml_file.write(output_xml_string)
+
+if __name__ == '__main__':
+ main()
diff --git a/e2etests/cvd/convert_xts_xml_to_junit_xml_test.py b/e2etests/cvd/convert_xts_xml_to_junit_xml_test.py
new file mode 100644
index 00000000000..38c092baa6f
--- /dev/null
+++ b/e2etests/cvd/convert_xts_xml_to_junit_xml_test.py
@@ -0,0 +1,60 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2025 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.
+#
+"""Tests for convert_xts_xml_to_junit_xml.py"""
+
+import unittest
+
+from cvd.convert_xts_xml_to_junit_xml import *
+
+class TestConvertXtsXmlToJunitXml(unittest.TestCase):
+
+ def test_basic_conversion(self):
+ input = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ actual = convert_xts_xml_to_junit_xml(input)
+ expected = """
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ self.assertEqual(actual, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/e2etests/cvd/cvd_xts_test.sh b/e2etests/cvd/cvd_xts_test.sh
new file mode 100755
index 00000000000..fc10df11e4a
--- /dev/null
+++ b/e2etests/cvd/cvd_xts_test.sh
@@ -0,0 +1,273 @@
+#!/usr/bin/env bash
+
+echo ""
+echo ""
+echo ""
+echo "NOTE: consider using Bazel's '--test_timeout=' when running"
+echo "a XTS test for the first time as the XTS download may be slow!"
+echo ""
+echo "'cvd fetch' caches the downloads and subsequent runs should be faster."
+echo ""
+echo ""
+echo ""
+
+# Exit on failure:
+set -e
+# Print commands before running:
+set -x
+
+# Parse command line flags:
+CREDENTIAL_SOURCE="${CREDENTIAL_SOURCE:-}"
+SUBSTITUTIONS=""
+CUTTLEFISH_CREATE_ARGS=""
+CUTTLEFISH_FETCH_BRANCH=""
+CUTTLEFISH_FETCH_TARGET=""
+XML_CONVERTER_PATH=""
+XTS_ARGS=""
+XTS_FETCH_BRANCH=""
+XTS_FETCH_TARGET=""
+XTS_TYPE=""
+for arg in "$@"; do
+ case "${arg}" in
+ --cuttlefish-create-args=*)
+ CUTTLEFISH_CREATE_ARGS="${arg#*=}"
+ ;;
+ --cuttlefish-fetch-branch=*)
+ CUTTLEFISH_FETCH_BRANCH="${arg#*=}"
+ ;;
+ --cuttlefish-fetch-target=*)
+ CUTTLEFISH_FETCH_TARGET="${arg#*=}"
+ ;;
+ --credential-source=*)
+ CREDENTIAL_SOURCE="${arg#*=}"
+ ;;
+ --substitutions=*)
+ SUBSTITUTIONS="${arg#*=}"
+ ;;
+ --xml-test-result-converter-path=*)
+ XML_CONVERTER_PATH="${arg#*=}"
+ ;;
+ --xts-args=*)
+ XTS_ARGS="${arg#*=}"
+ ;;
+ --xts-fetch-branch=*)
+ XTS_FETCH_BRANCH="${arg#*=}"
+ ;;
+ --xts-fetch-target=*)
+ XTS_FETCH_TARGET="${arg#*=}"
+ ;;
+ --xts-type=*)
+ XTS_TYPE="${arg#*=}"
+ ;;
+ *)
+ echo "Unknown flag: ${arg}" >&2
+ exit 1
+ ;;
+ esac
+done
+if [ -z "${CUTTLEFISH_FETCH_BRANCH}" ]; then
+ echo "Missing required --cuttlefish-fetch-branch flag."
+ exit 1
+fi
+if [ -z "${CUTTLEFISH_FETCH_TARGET}" ]; then
+ echo "Missing required --cuttlefish-fetch-targt flag."
+ exit 1
+fi
+if [ -z "${XML_CONVERTER_PATH}" ]; then
+ echo "Missing required --xml-test-result-converter-path flag."
+ exit 1
+fi
+if [ -z "${XTS_ARGS}" ]; then
+ echo "Missing required --xts-args flag."
+ exit 1
+fi
+if [ -z "${XTS_FETCH_BRANCH}" ]; then
+ echo "Missing required --xts-fetch-branch flag."
+ exit 1
+fi
+if [ -z "${XTS_FETCH_TARGET}" ]; then
+ echo "Missing required --xts-fetch-target flag."
+ exit 1
+fi
+if [ -z "${XTS_TYPE}" ]; then
+ echo "Missing required --xts-type flag."
+ exit 1
+fi
+XML_CONVERTER_PATH="$(realpath ${XML_CONVERTER_PATH})"
+echo "Parsed command line args:"
+echo " * CREDENTIAL_SOURCE: ${CREDENTIAL_SOURCE}"
+echo " * CUTTLEFISH_CREATE_ARGS: ${CUTTLEFISH_CREATE_ARGS}"
+echo " * CUTTLEFISH_FETCH_BRANCH: ${CUTTLEFISH_FETCH_BRANCH}"
+echo " * CUTTLEFISH_FETCH_TARGET: ${CUTTLEFISH_FETCH_TARGET}"
+echo " * XML_CONVERTER_PATH: ${XML_CONVERTER_PATH}"
+echo " * XTS_ARGS: ${XTS_ARGS}"
+echo " * XTS_FETCH_BRANCH: ${XTS_FETCH_BRANCH}"
+echo " * XTS_FETCH_TARGET: ${XTS_FETCH_TARGET}"
+echo " * XTS_TYPE: ${XTS_TYPE}"
+
+
+RUN_DIRECTORY="$(pwd)"
+
+workdir="$(mktemp -d -t cvd_xts_test.XXXXXX)"
+cd "${workdir}"
+
+XTS_DIRECTORY="${workdir}/test_suites"
+XTS_LATEST_RESULTS_DIRECTORY=""
+XTS_RUNNER=""
+if [ "${XTS_TYPE}" == "cts" ]; then
+ XTS_LATEST_RESULTS_DIRECTORY="${XTS_DIRECTORY}/android-cts/results/latest"
+ XTS_RUNNER="${XTS_DIRECTORY}/android-cts/tools/cts-tradefed"
+elif [ "${XTS_TYPE}" == "vts" ]; then
+ XTS_LATEST_RESULTS_DIRECTORY="${XTS_DIRECTORY}/android-vts/results/latest"
+ XTS_RUNNER="${XTS_DIRECTORY}/android-vts/tools/vts-tradefed"
+else
+ echo "Unsupported XTS type: ${XTS_TYPE}. Failure."
+ exit 1
+fi
+XTS_LATEST_RESULT_XML="${XTS_LATEST_RESULTS_DIRECTORY}/test_result.xml"
+XTS_LATEST_RESULT_CONVERTED_XML="${XTS_LATEST_RESULTS_DIRECTORY}/test.xml"
+
+# Additional files to keep track of and save to the final test results directory:
+CVD_CREATE_LOG_FILE="${workdir}/cvd_create_logs.txt"
+CVD_FETCH_LOG_FILE="${workdir}/cvd_fetch_logs.txt"
+XTS_LOG_FILE="${workdir}/xts_logs.txt"
+
+function collect_logs_and_cleanup() {
+ # Don't immediately exit on failure anymore
+ set +e
+
+ if [[ -n "${TEST_UNDECLARED_OUTPUTS_DIR}" ]] && [[ -d "${TEST_UNDECLARED_OUTPUTS_DIR}" ]]; then
+ echo "Copying logs to test output directory..."
+
+ cp "${workdir}"/*.log "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ cp "${workdir}"/cuttlefish_runtime/*.log "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ cp "${workdir}"/cuttlefish_runtime/logcat "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ cp "${workdir}"/cuttlefish_runtime/cuttlefish_config.json "${TEST_UNDECLARED_OUTPUTS_DIR}"
+
+ cp "${CVD_FETCH_LOG_FILE}" "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ cp "${CVD_CREATE_LOG_FILE}" "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ cp "${XTS_LOG_FILE}" "${TEST_UNDECLARED_OUTPUTS_DIR}"
+ fi
+
+ if [[ -n "${XML_OUTPUT_FILE}" ]]; then
+ echo "Copying converted XML results for Bazel consumption..."
+ cp "${XTS_LATEST_RESULT_CONVERTED_XML}" "${XML_OUTPUT_FILE}"
+ echo "Copied!"
+ fi
+
+ # Be nice, don't leave devices behind.
+ cd "${workdir}"
+ cvd reset -y
+
+ rm -rf "${workdir}"
+}
+
+# Regardless of whether and when a failure occurs logs must collected
+trap collect_logs_and_cleanup EXIT
+
+# Make sure to run in a clean environment, without any devices running
+cvd reset -y
+
+
+# Fetch Cuttlefish and XTS:
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Fetching Cuttlefish and ${XTS_TYPE}..."
+
+credential_arg=""
+if [[ -n "$CREDENTIAL_SOURCE" ]]; then
+ credential_arg="--credential_source=${CREDENTIAL_SOURCE}"
+fi
+
+cvd fetch \
+ --default_build="${CUTTLEFISH_FETCH_BRANCH}/${CUTTLEFISH_FETCH_TARGET}" \
+ --test_suites_build="${XTS_FETCH_BRANCH}/${XTS_FETCH_TARGET}" \
+ --target_directory="${workdir}" \
+ ${credential_arg} \
+ 2>&1 | tee "${CVD_FETCH_LOG_FILE}"
+
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Fetch completed!"
+
+
+# Android CTS includes some files with a 'kernel' suffix which confuses the
+# Cuttlefish launcher prior to
+# https://github.com/google/android-cuttlefish/commit/881728ed85329afaeb16e3b849d60c7a32fedcb7.
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Modifying 'fetcher_config.json' for 'kernel' file suffix workaround ..."
+sed -i 's|_kernel"|_kernel_zzz"|g' ${workdir}/fetcher_config.json
+
+
+# Create a new Cuttlefish device:
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Creating a Cuttlefish device with: ${CUTTLEFISH_CREATE_ARGS}"
+
+# This argument is supported at fetch and create time. Applying it to both
+# invocations would make it replace the debian_substition_marker list,
+# applying it only to one out of two cases makes it additive.
+substitution_arg=""
+if [[ -n "$SUBSTITUTIONS" ]]; then
+ substitution_arg="--host_substitutions=${SUBSTITUTIONS}"
+fi
+
+# Note: 'eval' used because 'CUTTLEFISH_CREATE_ARGS' might have been
+# escaped by Bazel.
+eval \
+HOME="$(pwd)" \
+cvd create \
+ --report_anonymous_usage_stats=y \
+ --undefok=report_anonymous_usage_stats \
+ "${substitution_arg}" \
+ "${CUTTLEFISH_CREATE_ARGS}" \
+ 2>&1 | tee "${CVD_CREATE_LOG_FILE}"
+
+echo "Cuttlefish device created!"
+
+
+# Wait for the new Cuttlefish device to appear in adb:
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Waiting for the Cuttlefish device to connect to adb..."
+timeout --kill-after=30s 29s adb wait-for-device
+if [ $? -eq 0 ]; then
+ echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+ echo "Cuttlefish device connected to adb."
+ adb devices
+else
+ echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+ echo "Timeout waiting for Cuttlefish device to connect to adb!"
+ exit 1
+fi
+
+
+# Run XTS. Note: 'eval' used because 'XTS_ARGS' might have been
+# escaped by Bazel.
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Running ${XTS_TYPE}..."
+cd "${XTS_DIRECTORY}"
+HOME="$(pwd)" \
+eval ${XTS_RUNNER} run commandAndExit "${XTS_TYPE}" \
+ --log-level-display=INFO \
+ "${XTS_ARGS}" \
+ 2>&1 | tee "${XTS_LOG_FILE}"
+echo "Finished running ${XTS_TYPE}!"
+
+
+# Convert results to Bazel friendly format:
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Converting ${XTS_TYPE} test result output to Bazel XML format..."
+python3 ${XML_CONVERTER_PATH} \
+ --input_xml_file="${XTS_LATEST_RESULT_XML}" \
+ --output_xml_file="${XTS_LATEST_RESULT_CONVERTED_XML}"
+echo "Converted!"
+
+
+# Determine exit code by looking at XTS results:
+echo "$(date +'%Y-%m-%dT%H:%M:%S%z')"
+echo "Checking if any ${XTS_TYPE} tests failed..."
+failures=$(cat ${XTS_LATEST_RESULT_CONVERTED_XML} | grep "