1#!/usr/bin/env python3 2 3# Copyright (C) 2024 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""A converter for Mobly result schema to Resultstore schema. 18 19Each Mobly test class maps to a Resultstore testsuite and each Mobly test method 20maps to a Resultstore testcase. For example: 21 22 Mobly schema: 23 24 Test Class: HelloWorldTest 25 Test Name: test_hello 26 Type: Record 27 Result: PASS 28 29 Resultstore schema: 30 31 <testsuite name="HelloWorldTest" tests=1> 32 <testcase name="test_hello"/> 33 </testsuite> 34""" 35 36import dataclasses 37import datetime 38import enum 39import logging 40import pathlib 41import re 42from typing import Any, Dict, Iterator, List, Mapping, Optional 43from xml.etree import ElementTree 44 45from mobly import records 46import yaml 47 48_MOBLY_RECORD_TYPE_KEY = 'Type' 49 50_MOBLY_TEST_SUITE_NAME = 'MoblyTest' 51 52_TEST_INTERRUPTED_MESSAGE = 'Details: Test was interrupted manually.' 53 54_ILLEGAL_XML_CHARS = re.compile( 55 '[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]' 56) 57 58_ILLEGAL_YAML_CHARS = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]') 59 60 61class MoblyResultstoreProperties(enum.Enum): 62 """Resultstore properties defined specifically for all Mobly tests. 63 64 All these properties apply to the testcase level. TEST_CLASS and 65 TEST_TYPE apply to both the testcase and testsuite level. 66 """ 67 68 BEGIN_TIME = 'mobly_begin_time' 69 END_TIME = 'mobly_end_time' 70 TEST_CLASS = 'mobly_test_class' 71 TEST_TYPE = 'test_type' 72 UID = 'mobly_uid' 73 TEST_OUTPUT = 'test_output' 74 TEST_SIGNATURE = 'mobly_signature' 75 SKIP_REASON = 'skip_reason' 76 ERROR_MESSAGE = 'mobly_error_message' 77 ERROR_TYPE = 'mobly_error_type' 78 STACK_TRACE = 'mobly_stack_trace' 79 80 81_MOBLY_PROPERTY_VALUES = frozenset(e.value for e in MoblyResultstoreProperties) 82 83 84class ResultstoreTreeTags(enum.Enum): 85 """Common tags for Resultstore tree nodes.""" 86 87 TESTSUITES = 'testsuites' 88 TESTSUITE = 'testsuite' 89 TESTCASE = 'testcase' 90 PROPERTIES = 'properties' 91 PROPERTY = 'property' 92 FAILURE = 'failure' 93 ERROR = 'error' 94 95 96class ResultstoreTreeAttributes(enum.Enum): 97 """Common attributes for Resultstore tree nodes.""" 98 99 ERRORS = 'errors' 100 FAILURES = 'failures' 101 TESTS = 'tests' 102 CLASS_NAME = 'classname' 103 RESULT = 'result' 104 STATUS = 'status' 105 TIME = 'time' 106 TIMESTAMP = 'timestamp' 107 NAME = 'name' 108 VALUE = 'value' 109 MESSAGE = 'message' 110 RETRY_NUMBER = 'retrynumber' 111 REPEAT_NUMBER = 'repeatnumber' 112 ERROR_TYPE = 'type' 113 RERAN_TEST_NAME = 'rerantestname' 114 115 116@dataclasses.dataclass 117class TestSuiteSummary: 118 num_tests: int 119 num_errors: int 120 num_failures: int 121 122 123@dataclasses.dataclass 124class ReranNode: 125 reran_test_name: str 126 original_test_name: str 127 index: int 128 node_type: records.TestParentType 129 130 131def _find_all_elements( 132 mobly_root: ElementTree.Element, 133 class_name: Optional[str], 134 test_name: Optional[str], 135) -> Iterator[ElementTree.Element]: 136 """Finds all elements in the Resultstore tree with class name and/or 137 test_name. 138 139 If class name is absent, it will find all elements with the test name 140 across all test classes. If test name is absent it will find all elements 141 with class name. If both are absent, it will just return the Mobly root 142 tree. 143 144 Args: 145 mobly_root: Root element of the Mobly test Resultstore tree. 146 class_name: Mobly test class name to get the elements for. 147 test_name: Mobly test names to get the elements for. 148 149 Yields: 150 Iterator of elements satisfying the class_name and test_name search 151 criteria. 152 """ 153 if class_name is None and test_name is None: 154 yield mobly_root 155 return 156 157 xpath = f'./{ResultstoreTreeTags.TESTSUITE.value}' 158 if class_name is not None: 159 xpath += f'[@{ResultstoreTreeAttributes.NAME.value}="{class_name}"]' 160 if test_name is not None: 161 xpath += ( 162 f'/{ResultstoreTreeTags.TESTCASE.value}' 163 f'[@{ResultstoreTreeAttributes.NAME.value}="{test_name}"]' 164 ) 165 166 yield from mobly_root.iterfind(xpath) 167 168 169def _create_or_return_properties_element( 170 element: ElementTree.Element, 171) -> ElementTree.Element: 172 properties_element = element.find( 173 f'./{ResultstoreTreeTags.PROPERTIES.value}') 174 if properties_element is not None: 175 return properties_element 176 return ElementTree.SubElement(element, ResultstoreTreeTags.PROPERTIES.value) 177 178 179def _add_or_update_property_element( 180 properties_element: ElementTree.Element, name: str, value: str 181): 182 """Adds a property element or update the property value.""" 183 name = _ILLEGAL_XML_CHARS.sub('', name) 184 value = _ILLEGAL_XML_CHARS.sub('', value) 185 property_element = properties_element.find( 186 f'./{ResultstoreTreeTags.PROPERTY.value}' 187 f'[@{ResultstoreTreeAttributes.NAME.value}="{name}"]' 188 ) 189 if property_element is None: 190 property_element = ElementTree.SubElement( 191 properties_element, ResultstoreTreeTags.PROPERTY.value 192 ) 193 property_element.set(ResultstoreTreeAttributes.NAME.value, name) 194 property_element.set(ResultstoreTreeAttributes.VALUE.value, value) 195 196 197def _add_file_annotations( 198 entry: Mapping[str, Any], 199 properties_element: ElementTree.Element, 200 mobly_base_directory: Optional[pathlib.Path], 201) -> None: 202 """Adds file annotations for a Mobly test case files. 203 204 The mobly_base_directory is used to find the files belonging to a test case. 205 The files under "mobly_base_directory/test_class/test_method" belong to the 206 test_class#test_method Resultstore node. Additionally, it is used to 207 determine the relative path of the files for Resultstore undeclared outputs. 208 The file annotation must be written for the relative path. 209 210 Args: 211 entry: Mobly summary entry for the test case. 212 properties_element: Test case properties element. 213 mobly_base_directory: Base directory of the Mobly test. 214 """ 215 # If mobly_base_directory is not provided, the converter will not add the 216 # annotations to associate the files with the test cases. 217 if ( 218 mobly_base_directory is None 219 or entry.get(records.TestResultEnums.RECORD_SIGNATURE, None) is None 220 ): 221 return 222 223 test_class = entry[records.TestResultEnums.RECORD_CLASS] 224 test_case_directory = mobly_base_directory.joinpath( 225 test_class, 226 entry[records.TestResultEnums.RECORD_SIGNATURE] 227 ) 228 229 test_case_files = test_case_directory.rglob('*') 230 file_counter = 0 231 for file_path in test_case_files: 232 if not file_path.is_file(): 233 continue 234 relative_path = file_path.relative_to(mobly_base_directory) 235 _add_or_update_property_element( 236 properties_element, 237 f'test_output{file_counter}', 238 str(relative_path.as_posix()), 239 ) 240 file_counter += 1 241 242 243def _create_mobly_root_element( 244 summary_record: Mapping[str, Any] 245) -> ElementTree.Element: 246 """Creates a Resultstore XML testsuite node for a Mobly test summary.""" 247 full_summary = TestSuiteSummary( 248 num_tests=summary_record['Requested'], 249 num_errors=summary_record['Error'], 250 num_failures=summary_record['Failed'], 251 ) 252 # Create the root Resultstore node to wrap the Mobly test. 253 main_wrapper = ElementTree.Element(ResultstoreTreeTags.TESTSUITES.value) 254 main_wrapper.set(ResultstoreTreeAttributes.NAME.value, '__main__') 255 main_wrapper.set(ResultstoreTreeAttributes.TIME.value, '0') 256 main_wrapper.set( 257 ResultstoreTreeAttributes.ERRORS.value, str(full_summary.num_errors) 258 ) 259 main_wrapper.set( 260 ResultstoreTreeAttributes.FAILURES.value, str(full_summary.num_failures) 261 ) 262 main_wrapper.set( 263 ResultstoreTreeAttributes.TESTS.value, str(full_summary.num_tests) 264 ) 265 266 mobly_test_root = ElementTree.SubElement( 267 main_wrapper, ResultstoreTreeTags.TESTSUITE.value 268 ) 269 mobly_test_root.set( 270 ResultstoreTreeAttributes.NAME.value, _MOBLY_TEST_SUITE_NAME 271 ) 272 mobly_test_root.set(ResultstoreTreeAttributes.TIME.value, '0') 273 mobly_test_root.set( 274 ResultstoreTreeAttributes.ERRORS.value, str(full_summary.num_errors) 275 ) 276 mobly_test_root.set( 277 ResultstoreTreeAttributes.FAILURES.value, str(full_summary.num_failures) 278 ) 279 mobly_test_root.set( 280 ResultstoreTreeAttributes.TESTS.value, str(full_summary.num_tests) 281 ) 282 283 return main_wrapper 284 285 286def _create_class_element( 287 class_name: str, class_summary: TestSuiteSummary 288) -> ElementTree.Element: 289 """Creates a Resultstore XML testsuite node for a Mobly test class summary. 290 291 Args: 292 class_name: Mobly test class name. 293 class_summary: Mobly test class summary. 294 295 Returns: 296 A Resultstore testsuite node representing one Mobly test class. 297 """ 298 class_element = ElementTree.Element(ResultstoreTreeTags.TESTSUITE.value) 299 class_element.set(ResultstoreTreeAttributes.NAME.value, class_name) 300 class_element.set(ResultstoreTreeAttributes.TIME.value, '0') 301 class_element.set( 302 ResultstoreTreeAttributes.TESTS.value, str(class_summary.num_tests) 303 ) 304 class_element.set( 305 ResultstoreTreeAttributes.ERRORS.value, str(class_summary.num_errors) 306 ) 307 class_element.set( 308 ResultstoreTreeAttributes.FAILURES.value, 309 str(class_summary.num_failures) 310 ) 311 312 properties_element = _create_or_return_properties_element(class_element) 313 _add_or_update_property_element( 314 properties_element, 315 MoblyResultstoreProperties.TEST_CLASS.value, 316 class_name, 317 ) 318 _add_or_update_property_element( 319 properties_element, 320 MoblyResultstoreProperties.TEST_TYPE.value, 321 'mobly_class', 322 ) 323 324 return class_element 325 326 327def _set_rerun_node( 328 signature: str, 329 child_parent_map: Mapping[str, str], 330 parent_type_map: Mapping[str, records.TestParentType], 331 signature_test_name_map: Mapping[str, str], 332 rerun_node_map: Dict[str, ReranNode], 333) -> None: 334 """Sets the rerun node in the rerun node map for the current test signature. 335 336 This function traverses the child parent map recursively until it finds the 337 root test run for the rerun chain. Then it uses the original test name from 338 there and builds the indices. 339 340 Args: 341 signature: Current test signature. 342 child_parent_map: Map of test signature to the parent test signature. 343 parent_type_map: Map of parent test signature to the parent type. 344 signature_test_name_map: Map of test signature to test name. 345 rerun_node_map: Map of test signature to rerun information. 346 """ 347 if signature in rerun_node_map: 348 return 349 350 # If there is no parent, then this is the root test in the retry chain. 351 if signature not in child_parent_map: 352 if parent_type_map[signature] == records.TestParentType.REPEAT: 353 # If repeat, remove the '_#' suffix to get the original test name. 354 original_test_name = \ 355 signature_test_name_map[signature].rsplit('_', 1)[0] 356 else: 357 original_test_name = signature_test_name_map[signature] 358 rerun_node_map[signature] = ReranNode( 359 signature_test_name_map[signature], 360 original_test_name, 361 0, 362 parent_type_map[signature], 363 ) 364 return 365 366 parent_signature = child_parent_map[signature] 367 _set_rerun_node( 368 parent_signature, 369 child_parent_map, 370 parent_type_map, 371 signature_test_name_map, 372 rerun_node_map, 373 ) 374 375 parent_node = rerun_node_map[parent_signature] 376 rerun_node_map[signature] = ReranNode( 377 signature_test_name_map[signature], 378 parent_node.original_test_name, 379 parent_node.index + 1, 380 parent_node.node_type, 381 ) 382 383 384def _get_reran_nodes( 385 entries: List[Mapping[str, Any]] 386) -> Mapping[str, ReranNode]: 387 """Gets the nodes for any test case reruns. 388 389 Args: 390 entries: Summary entries for the Mobly test runs. 391 392 Returns: 393 A map of test signature to node information. 394 """ 395 child_parent_map = {} 396 parent_type_map = {} 397 signature_test_name_map = {} 398 for entry in entries: 399 if records.TestResultEnums.RECORD_SIGNATURE not in entry: 400 continue 401 current_signature = entry[records.TestResultEnums.RECORD_SIGNATURE] 402 signature_test_name_map[current_signature] = entry[ 403 records.TestResultEnums.RECORD_NAME 404 ] 405 # This is a dictionary with parent and type. 406 rerun_parent = entry.get(records.TestResultEnums.RECORD_PARENT, None) 407 if rerun_parent is not None: 408 parent_signature = rerun_parent['parent'] 409 parent_type = ( 410 records.TestParentType.RETRY 411 if rerun_parent['type'] == 'retry' 412 else records.TestParentType.REPEAT 413 ) 414 child_parent_map[current_signature] = parent_signature 415 parent_type_map[parent_signature] = parent_type 416 417 rerun_node_map = {} 418 for signature in child_parent_map: 419 # Populates the rerun node map. 420 _set_rerun_node( 421 signature, 422 child_parent_map, 423 parent_type_map, 424 signature_test_name_map, 425 rerun_node_map, 426 ) 427 428 return rerun_node_map 429 430 431def _process_record( 432 entry: Mapping[str, Any], 433 reran_node: Optional[ReranNode], 434 mobly_base_directory: Optional[pathlib.Path], 435) -> ElementTree.Element: 436 """Processes a single Mobly test record entry to a Resultstore test case 437 node. 438 439 Args: 440 entry: Summary of a single Mobly test case. 441 reran_node: Rerun information if this test case is a rerun. Only present 442 if this test is part of a rerun chain. 443 mobly_base_directory: Base directory for the Mobly test. Artifacts from 444 the Mobly test will be saved here. 445 446 Returns: 447 A Resultstore XML node representing a single test case. 448 """ 449 begin_time = entry[records.TestResultEnums.RECORD_BEGIN_TIME] 450 end_time = entry[records.TestResultEnums.RECORD_END_TIME] 451 testcase_element = ElementTree.Element(ResultstoreTreeTags.TESTCASE.value) 452 result = entry[records.TestResultEnums.RECORD_RESULT] 453 454 if reran_node is not None: 455 if reran_node.node_type == records.TestParentType.RETRY: 456 testcase_element.set( 457 ResultstoreTreeAttributes.RETRY_NUMBER.value, 458 str(reran_node.index) 459 ) 460 elif reran_node.node_type == records.TestParentType.REPEAT: 461 testcase_element.set( 462 ResultstoreTreeAttributes.REPEAT_NUMBER.value, 463 str(reran_node.index) 464 ) 465 testcase_element.set( 466 ResultstoreTreeAttributes.NAME.value, reran_node.original_test_name 467 ) 468 testcase_element.set( 469 ResultstoreTreeAttributes.RERAN_TEST_NAME.value, 470 reran_node.reran_test_name, 471 ) 472 else: 473 testcase_element.set( 474 ResultstoreTreeAttributes.NAME.value, 475 entry[records.TestResultEnums.RECORD_NAME], 476 ) 477 testcase_element.set( 478 ResultstoreTreeAttributes.RERAN_TEST_NAME.value, 479 entry[records.TestResultEnums.RECORD_NAME], 480 ) 481 testcase_element.set( 482 ResultstoreTreeAttributes.CLASS_NAME.value, 483 entry[records.TestResultEnums.RECORD_CLASS], 484 ) 485 if result == records.TestResultEnums.TEST_RESULT_SKIP: 486 testcase_element.set(ResultstoreTreeAttributes.RESULT.value, 'skipped') 487 testcase_element.set(ResultstoreTreeAttributes.STATUS.value, 'notrun') 488 testcase_element.set(ResultstoreTreeAttributes.TIME.value, '0') 489 elif result is None: 490 testcase_element.set(ResultstoreTreeAttributes.RESULT.value, 491 'completed') 492 testcase_element.set(ResultstoreTreeAttributes.STATUS.value, 'run') 493 testcase_element.set(ResultstoreTreeAttributes.TIME.value, '0') 494 testcase_element.set( 495 ResultstoreTreeAttributes.TIMESTAMP.value, 496 datetime.datetime.fromtimestamp( 497 begin_time / 1000, tz=datetime.timezone.utc 498 ).strftime('%Y-%m-%dT%H:%M:%SZ'), 499 ) 500 else: 501 testcase_element.set(ResultstoreTreeAttributes.RESULT.value, 502 'completed') 503 testcase_element.set(ResultstoreTreeAttributes.STATUS.value, 'run') 504 testcase_element.set( 505 ResultstoreTreeAttributes.TIME.value, str(end_time - begin_time) 506 ) 507 testcase_element.set( 508 ResultstoreTreeAttributes.TIMESTAMP.value, 509 datetime.datetime.fromtimestamp( 510 begin_time / 1000, tz=datetime.timezone.utc 511 ).strftime('%Y-%m-%dT%H:%M:%SZ'), 512 ) 513 514 # Add Mobly specific test case properties. 515 properties_element = _create_or_return_properties_element(testcase_element) 516 if result == records.TestResultEnums.TEST_RESULT_SKIP: 517 _add_or_update_property_element( 518 properties_element, 519 MoblyResultstoreProperties.SKIP_REASON.value, 520 f'Details: {entry[records.TestResultEnums.RECORD_DETAILS]}', 521 ) 522 elif result is None: 523 _add_or_update_property_element( 524 properties_element, 525 MoblyResultstoreProperties.BEGIN_TIME.value, 526 str(begin_time), 527 ) 528 else: 529 _add_or_update_property_element( 530 properties_element, 531 MoblyResultstoreProperties.BEGIN_TIME.value, 532 str(begin_time), 533 ) 534 _add_or_update_property_element( 535 properties_element, 536 MoblyResultstoreProperties.END_TIME.value, 537 str(end_time), 538 ) 539 _add_or_update_property_element( 540 properties_element, 541 MoblyResultstoreProperties.TEST_CLASS.value, 542 entry[records.TestResultEnums.RECORD_CLASS], 543 ) 544 _add_or_update_property_element( 545 properties_element, 546 MoblyResultstoreProperties.TEST_TYPE.value, 547 'mobly_test', 548 ) 549 550 if entry.get(records.TestResultEnums.RECORD_SIGNATURE, None) is not None: 551 _add_or_update_property_element( 552 properties_element, 553 MoblyResultstoreProperties.TEST_SIGNATURE.value, 554 entry[records.TestResultEnums.RECORD_SIGNATURE], 555 ) 556 557 _add_file_annotations( 558 entry, 559 properties_element, 560 mobly_base_directory, 561 ) 562 563 if entry[records.TestResultEnums.RECORD_UID] is not None: 564 _add_or_update_property_element( 565 properties_element, 566 MoblyResultstoreProperties.UID.value, 567 entry[records.TestResultEnums.RECORD_UID], 568 ) 569 570 if result is None: 571 error_element = ElementTree.SubElement( 572 testcase_element, ResultstoreTreeTags.ERROR.value 573 ) 574 error_element.set( 575 ResultstoreTreeAttributes.MESSAGE.value, _TEST_INTERRUPTED_MESSAGE 576 ) 577 error_element.text = _TEST_INTERRUPTED_MESSAGE 578 elif ( 579 result == records.TestResultEnums.TEST_RESULT_FAIL 580 or result == records.TestResultEnums.TEST_RESULT_ERROR 581 ): 582 error_message = ( 583 f'Details: {entry[records.TestResultEnums.RECORD_DETAILS]}') 584 tag = ( 585 ResultstoreTreeTags.FAILURE.value 586 if result == records.TestResultEnums.TEST_RESULT_FAIL 587 else ResultstoreTreeTags.ERROR.value 588 ) 589 failure_or_error_element = ElementTree.SubElement(testcase_element, tag) 590 failure_or_error_element.set( 591 ResultstoreTreeAttributes.MESSAGE.value, error_message 592 ) 593 _add_or_update_property_element( 594 properties_element, 595 MoblyResultstoreProperties.ERROR_MESSAGE.value, 596 error_message, 597 ) 598 599 # Add termination signal type and stack trace to the failure XML element 600 # and the test case properties. 601 termination_signal_type = entry[ 602 records.TestResultEnums.RECORD_TERMINATION_SIGNAL_TYPE 603 ] 604 if termination_signal_type is None: 605 logging.warning( 606 'Test %s has %s result without a termination signal type.', 607 entry[records.TestResultEnums.RECORD_NAME], 608 result, 609 ) 610 else: 611 failure_or_error_element.set( 612 ResultstoreTreeAttributes.ERROR_TYPE.value, 613 termination_signal_type 614 ) 615 _add_or_update_property_element( 616 properties_element, 617 MoblyResultstoreProperties.ERROR_TYPE.value, 618 termination_signal_type, 619 ) 620 stack_trace = entry[records.TestResultEnums.RECORD_STACKTRACE] 621 if stack_trace is None: 622 logging.warning( 623 'Test %s has %s result without a stack trace.', 624 entry[records.TestResultEnums.RECORD_NAME], 625 result, 626 ) 627 else: 628 failure_or_error_element.text = stack_trace 629 _add_or_update_property_element( 630 properties_element, 631 MoblyResultstoreProperties.STACK_TRACE.value, 632 stack_trace, 633 ) 634 return testcase_element 635 636 637def convert( 638 mobly_results_path: pathlib.Path, 639 mobly_base_directory: Optional[pathlib.Path] = None, 640) -> ElementTree.ElementTree: 641 """Converts a Mobly results summary file to Resultstore XML schema. 642 643 The mobly_base_directory will be used to compute the file links for each 644 Resultstore tree element. If it is absent then the file links will be 645 omitted. 646 647 Args: 648 mobly_results_path: Path to the Mobly summary YAML file. 649 mobly_base_directory: Base directory of the Mobly test. 650 651 Returns: 652 A Resultstore XML tree for the Mobly test. 653 """ 654 logging.info('Generating Resultstore tree...') 655 656 with mobly_results_path.open('r', encoding='utf-8') as f: 657 summary_entries = list( 658 yaml.safe_load_all(_ILLEGAL_YAML_CHARS.sub('', f.read())) 659 ) 660 661 summary_record = next( 662 entry 663 for entry in summary_entries 664 if entry[_MOBLY_RECORD_TYPE_KEY] 665 == records.TestSummaryEntryType.SUMMARY.value 666 ) 667 668 main_root = _create_mobly_root_element(summary_record) 669 670 mobly_test_root = main_root[0] 671 mobly_root_properties = _create_or_return_properties_element( 672 mobly_test_root) 673 # Add files under the Mobly root directory to the Mobly test suite node. 674 if mobly_base_directory is not None: 675 file_counter = 0 676 for file_path in mobly_base_directory.iterdir(): 677 if not file_path.is_file(): 678 continue 679 relative_path = file_path.relative_to(mobly_base_directory) 680 _add_or_update_property_element( 681 mobly_root_properties, 682 f'test_output{file_counter}', 683 str(relative_path.as_posix()), 684 ) 685 file_counter += 1 686 687 test_case_entries = [ 688 entry 689 for entry in summary_entries 690 if (entry[_MOBLY_RECORD_TYPE_KEY] 691 == records.TestSummaryEntryType.RECORD.value) 692 ] 693 # Populate the class summaries. 694 class_summaries = {} 695 for entry in test_case_entries: 696 class_name = entry[records.TestResultEnums.RECORD_CLASS] 697 698 if class_name not in class_summaries: 699 class_summaries[class_name] = TestSuiteSummary( 700 num_tests=0, num_errors=0, num_failures=0 701 ) 702 703 class_summaries[class_name].num_tests += 1 704 if ( 705 entry[records.TestResultEnums.RECORD_RESULT] 706 == records.TestResultEnums.TEST_RESULT_ERROR 707 ): 708 class_summaries[class_name].num_errors += 1 709 elif ( 710 entry[records.TestResultEnums.RECORD_RESULT] 711 == records.TestResultEnums.TEST_RESULT_FAIL 712 ): 713 class_summaries[class_name].num_failures += 1 714 715 # Create class nodes. 716 class_elements = {} 717 for class_name, summary in class_summaries.items(): 718 class_elements[class_name] = _create_class_element(class_name, summary) 719 mobly_test_root.append(class_elements[class_name]) 720 721 # Append test case nodes to test class nodes. 722 reran_nodes = _get_reran_nodes(test_case_entries) 723 for entry in test_case_entries: 724 class_name = entry[records.TestResultEnums.RECORD_CLASS] 725 if ( 726 records.TestResultEnums.RECORD_SIGNATURE in entry 727 and 728 entry[records.TestResultEnums.RECORD_SIGNATURE] in reran_nodes 729 ): 730 reran_node = reran_nodes[ 731 entry[records.TestResultEnums.RECORD_SIGNATURE]] 732 else: 733 reran_node = None 734 class_elements[class_name].append( 735 _process_record(entry, reran_node, mobly_base_directory) 736 ) 737 738 user_data_entries = [ 739 entry 740 for entry in summary_entries 741 if (entry[_MOBLY_RECORD_TYPE_KEY] 742 == records.TestSummaryEntryType.USER_DATA.value) 743 ] 744 745 for user_data_entry in user_data_entries: 746 class_name = user_data_entry.get(records.TestResultEnums.RECORD_CLASS, 747 None) 748 test_name = user_data_entry.get(records.TestResultEnums.RECORD_NAME, 749 None) 750 751 properties = user_data_entry.get('properties', None) 752 if not isinstance(properties, dict): 753 continue 754 for element in _find_all_elements(mobly_test_root, class_name, 755 test_name): 756 properties_element = _create_or_return_properties_element(element) 757 for name, value in properties.items(): 758 if name in _MOBLY_PROPERTY_VALUES: 759 # Do not override Mobly properties. 760 continue 761 _add_or_update_property_element( 762 properties_element, str(name), str(value) 763 ) 764 765 return ElementTree.ElementTree(main_root) 766