result_processing.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Helper functions for test result xml merging and processing.
  6. """
  7. import glob
  8. import os
  9. import xml.etree.ElementTree as xet
  10. TEST_RESULTS_DIR = 'Testing'
  11. def _get_ctest_tag_content(cmake_build_path):
  12. """
  13. Get the content of the CTest TAG file. This file contains the name of the CTest test results directory.
  14. :param cmake_build_path: Path to the CMake build directory.
  15. :return: First line of the TAG file.
  16. """
  17. tag_file_path = os.path.join(cmake_build_path, TEST_RESULTS_DIR, 'TAG')
  18. if not os.path.exists(tag_file_path):
  19. raise FileNotFoundError(f'Could not find CTest TAG file at {tag_file_path}')
  20. first_line = None
  21. with open(tag_file_path) as tag_file:
  22. first_line = tag_file.readline().strip()
  23. return first_line
  24. def _build_ctest_test_results_path(cmake_build_path):
  25. """
  26. Build the path to the CTest test results directory.
  27. :param cmake_build_path: Path to the CMake build directory.
  28. :return: Path to the CTest test results directory.
  29. """
  30. tag_content = _get_ctest_tag_content(cmake_build_path)
  31. if not tag_content:
  32. raise Exception('TAG file is empty.')
  33. ctest_results_path = os.path.join(cmake_build_path, TEST_RESULTS_DIR, tag_content)
  34. return ctest_results_path
  35. def _build_gtest_test_results_path(cmake_build_path):
  36. """
  37. Build the path to the GTest test results directory.
  38. :param cmake_build_path: Path to the CMake build directory.
  39. :return: Path to the GTest test results directory.
  40. """
  41. gtest_results_path = os.path.join(cmake_build_path, TEST_RESULTS_DIR, 'Gtest')
  42. return gtest_results_path
  43. def _build_pytest_test_results_path(cmake_build_path):
  44. """
  45. Build the path to the Pytest test results directory.
  46. :param cmake_build_path: Path to the CMake build directory.
  47. :return: Path to the Pytest test results directory.
  48. """
  49. pytest_results_path = os.path.join(cmake_build_path, TEST_RESULTS_DIR, 'Pytest')
  50. return pytest_results_path
  51. def _get_all_test_results_paths(cmake_build_path, ctest_path_error_ok=False):
  52. """
  53. Build and return the test result paths for all test harnesses.
  54. :param cmake_build_path: Path to the CMake build directory.
  55. :param ctest_path_error_ok: Ignore errors that occur while building CTest results path.
  56. :return: Test result paths for all test harnesses.
  57. """
  58. paths = [
  59. _build_gtest_test_results_path(cmake_build_path),
  60. _build_pytest_test_results_path(cmake_build_path)
  61. ]
  62. try:
  63. paths.append(_build_ctest_test_results_path(cmake_build_path))
  64. except FileNotFoundError as e:
  65. if ctest_path_error_ok:
  66. print(e)
  67. else:
  68. raise
  69. return paths
  70. def _merge_xml_results(xml_results_path, prefix, merged_xml_name, parent_element_name, child_element_name,
  71. attributes_to_aggregate):
  72. """
  73. Merge the contents of XML test result files.
  74. :param xml_results_path: Path to the directory containing the files to merge.
  75. :param prefix: Test result file prefix.
  76. :param merged_xml_name: Name for the merged test result file.
  77. :param parent_element_name: Name of the XML element that will store the test results.
  78. :param child_element_name: Name of the XML element that contains the test results.
  79. :param attributes_to_aggregate: List of AttributeInfo items used for test result aggregation.
  80. """
  81. xml_files = glob.glob(os.path.join(xml_results_path, f'{prefix}*.xml'))
  82. if not xml_files:
  83. return
  84. temp_dict = {}
  85. for attribute in attributes_to_aggregate:
  86. temp_dict[attribute.name] = attribute.func(0)
  87. def _aggregate_attributes(nodes):
  88. for node in nodes:
  89. for attribute in attributes_to_aggregate:
  90. if attribute.name in node.attrib:
  91. temp_dict[attribute.name] += attribute.func(node.attrib[attribute.name])
  92. else:
  93. print("Failed to find key {} in {}, continuing...".format(attribute.name, node.tag))
  94. base_tree = xet.parse(xml_files[0])
  95. base_tree_root = base_tree.getroot()
  96. if base_tree_root.tag == parent_element_name:
  97. parent_element = base_tree_root
  98. else:
  99. parent_element = base_tree_root.find(parent_element_name)
  100. _aggregate_attributes(base_tree_root.findall(child_element_name))
  101. for xml_file in xml_files[1:]:
  102. root = xet.parse(xml_file).getroot()
  103. child_nodes = root.findall(child_element_name)
  104. _aggregate_attributes(child_nodes)
  105. parent_element.extend(child_nodes)
  106. for attribute in attributes_to_aggregate:
  107. parent_element.attrib[attribute.name] = str(temp_dict[attribute.name])
  108. base_tree.write(os.path.join(xml_results_path, merged_xml_name), encoding='UTF-8', xml_declaration=True)
  109. def clean_test_results(cmake_build_path):
  110. """
  111. Clean the test results directories.
  112. :param cmake_build_path: Path to the CMake build directory.
  113. """
  114. # Using ctest_path_error_ok=True since the CTest path might not exist before tests are run for the first
  115. # time in a clean build.
  116. for path in _get_all_test_results_paths(cmake_build_path, ctest_path_error_ok=True):
  117. xml_files = glob.glob(os.path.join(path, '*.xml'))
  118. for xml_file in xml_files:
  119. os.remove(xml_file)
  120. def rename_test_results(cmake_build_path, prefix, iteration, total):
  121. """
  122. Rename the test result files with a prefix to prevent files being overwritten by subsequent test runs.
  123. :param cmake_build_path: Path to the CMake build directory.
  124. :param prefix: Test result file prefix.
  125. :param iteration: Test run number.
  126. :param total: Total number of test runs.
  127. """
  128. for path in _get_all_test_results_paths(cmake_build_path):
  129. xml_files = glob.glob(os.path.join(path, '*.xml'))
  130. for xml_file in xml_files:
  131. filename = os.path.basename(xml_file)
  132. directory = os.path.dirname(xml_file)
  133. if not filename.startswith(f'{prefix}-'):
  134. new_name = os.path.join(directory, f'{prefix}-{iteration}-{total}-{filename}')
  135. os.rename(xml_file, new_name)
  136. def collect_test_results(cmake_build_path, prefix):
  137. """
  138. Combines and aggregates test results for each test harness.
  139. :param cmake_build_path: Path to the CMake build directory.
  140. :param prefix: Test result file prefix.
  141. """
  142. class AttributeInfo:
  143. def __init__(self, name, func):
  144. self.name = name
  145. self.func = func
  146. # Attributes that will be aggregated for JUnit-like reports (GTest and Pytest)
  147. attributes_to_aggregate = [AttributeInfo('tests', int),
  148. AttributeInfo('failures', int),
  149. AttributeInfo('disabled', int),
  150. AttributeInfo('errors', int),
  151. AttributeInfo('time', float)]
  152. results_to_process = [
  153. # CTest results don't need aggregation, just merging.
  154. [_build_ctest_test_results_path(cmake_build_path), 'Site', 'Testing', []],
  155. # GTest and Pytest results need aggregation and merging.
  156. [_build_gtest_test_results_path(cmake_build_path), 'testsuites', 'testsuite', attributes_to_aggregate],
  157. [_build_pytest_test_results_path(cmake_build_path), 'testsuites', 'testsuite', attributes_to_aggregate]
  158. ]
  159. for result in results_to_process:
  160. _merge_xml_results(result[0], prefix, 'Merged.xml', result[1], result[2], result[3])
  161. def summarize_test_results(cmake_build_path, total):
  162. """
  163. Writes a summary of the test results.
  164. :param cmake_build_path: Path to the CMake build directory.
  165. :param total: Total number of times the tests were executed.
  166. :return: A list of tests failed with their failure rate.
  167. """
  168. failed_tests = {}
  169. ctest_results_file = os.path.join(_build_ctest_test_results_path(cmake_build_path), 'Merged.xml')
  170. base_tree = xet.parse(ctest_results_file)
  171. base_tree_root = base_tree.getroot()
  172. testing_nodes = base_tree_root.findall('Testing')
  173. for testing_node in testing_nodes:
  174. test_nodes = testing_node.findall('Test')
  175. for test_node in test_nodes:
  176. if test_node.get('Status') == 'failed':
  177. name_element = test_node.find('Name')
  178. name = name_element.text
  179. failed_tests[name] = failed_tests.get(name, 0) + 1
  180. report = []
  181. for test, count in failed_tests.items():
  182. percent = count/total
  183. report.append(f'{test} failed {count}/{total} times for a failure rate of ~{percent:.2%}')
  184. return report