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 "