1#!/usr/bin/env python3 2# 3# Copyright 2020 - 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 17import datetime 18import os 19 20from acts.libs.proto.proto_utils import parse_proto_to_ascii 21from acts.libs.testtracker.protos.gen.testtracker_result_pb2 import Result 22from acts.records import TestResultEnums 23from acts.records import TestResultRecord 24 25from acts import signals 26 27KEY_DETAILS = 'details' 28KEY_EFFORT_NAME = 'effort_name' 29KEY_PROJECT_ID = 'project_id' 30KEY_TESTTRACKER_UUID = 'test_tracker_uuid' 31KEY_USER = 'user' 32KEY_UUID = 'uuid' 33 34TESTTRACKER_PATH = 'test_tracker_results/test_effort_name=%s/test_case_uuid=%s' 35RESULT_FILE_NAME = 'result.pb.txt' 36 37_TEST_RESULT_TO_STATUS_MAP = { 38 TestResultEnums.TEST_RESULT_PASS: Result.PASSED, 39 TestResultEnums.TEST_RESULT_FAIL: Result.FAILED, 40 TestResultEnums.TEST_RESULT_SKIP: Result.SKIPPED, 41 TestResultEnums.TEST_RESULT_ERROR: Result.ERROR 42} 43 44 45class TestTrackerError(Exception): 46 """Exception class for errors raised within TestTrackerResultsWriter""" 47 pass 48 49 50class TestTrackerResultsWriter(object): 51 """Takes a test record, converts it to a TestTracker result proto, and 52 writes it to the log directory. In automation, these protos will 53 automatically be read from Sponge and uploaded to TestTracker. 54 """ 55 def __init__(self, log_path, properties): 56 """Creates a TestTrackerResultsWriter 57 58 Args: 59 log_path: Base log path to store TestTracker results. Must be within 60 the ACTS directory. 61 properties: dict representing key-value pairs to be uploaded as 62 TestTracker properties. 63 """ 64 self._log_path = log_path 65 self._properties = properties 66 self._validate_properties() 67 68 def write_results(self, record): 69 """Create a Result proto from test record, then write it to a file. 70 71 Args: 72 record: An acts.records.TestResultRecord object 73 """ 74 proto = self._create_result_proto(record) 75 proto_dir = self._create_results_dir(proto.uuid) 76 with open(os.path.join(proto_dir, RESULT_FILE_NAME), mode='w') as f: 77 f.write(parse_proto_to_ascii(proto)) 78 79 def write_results_from_test_signal(self, signal, begin_time=None): 80 """Create a Result proto from a test signal, then write it to a file. 81 82 Args: 83 signal: An acts.signals.TestSignal object 84 begin_time: Optional. Sets the begin_time of the test record. 85 """ 86 record = TestResultRecord('') 87 record.begin_time = begin_time 88 if not record.begin_time: 89 record.test_begin() 90 if isinstance(signal, signals.TestPass): 91 record.test_pass(signal) 92 elif isinstance(signal, signals.TestFail): 93 record.test_fail(signal) 94 elif isinstance(signal, signals.TestSkip): 95 record.test_skip(signal) 96 else: 97 record.test_error(signal) 98 self.write_results(record) 99 100 def _validate_properties(self): 101 """Checks that the required properties are set 102 103 Raises: 104 TestTrackerError if one or more required properties is absent 105 """ 106 required_props = [KEY_USER, KEY_PROJECT_ID, KEY_EFFORT_NAME] 107 missing_props = [p for p in required_props if p not in self._properties] 108 if missing_props: 109 raise TestTrackerError( 110 'Missing the following required properties for TestTracker: %s' 111 % missing_props) 112 113 @staticmethod 114 def _add_property(result_proto, name, value): 115 """Adds a Property to a given Result proto 116 117 Args: 118 result_proto: Result proto to modify 119 name: Property name 120 value: Property value 121 """ 122 new_prop = result_proto.property.add() 123 new_prop.name = name 124 if isinstance(value, bool): 125 new_prop.bool_value = value 126 elif isinstance(value, int): 127 new_prop.int_value = value 128 elif isinstance(value, float): 129 new_prop.double_value = value 130 else: 131 new_prop.string_value = str(value) 132 133 def _create_result_proto(self, record): 134 """Create a Result proto object from test record. Fills in uuid, status, 135 and properties with info gathered from the test record. 136 137 Args: 138 record: An acts.records.TestResultRecord object 139 140 Returns: Result proto, or None if record is invalid 141 """ 142 uuid = record.extras[KEY_TESTTRACKER_UUID] 143 result = Result() 144 result.uuid = uuid 145 result.status = _TEST_RESULT_TO_STATUS_MAP[record.result] 146 result.timestamp = ( 147 datetime.datetime.fromtimestamp( 148 record.begin_time / 1000, datetime.timezone.utc) 149 .isoformat(timespec='milliseconds') 150 .replace('+00:00', 'Z')) 151 152 self._add_property(result, KEY_UUID, uuid) 153 if record.details: 154 self._add_property(result, KEY_DETAILS, record.details) 155 156 for key, value in self._properties.items(): 157 self._add_property(result, key, value) 158 159 return result 160 161 def _create_results_dir(self, uuid): 162 """Creates the TestTracker directory given the test uuid 163 164 Args: 165 uuid: The TestTracker uuid of the test 166 167 Returns: Path to the created directory. 168 """ 169 dir_path = os.path.join(self._log_path, TESTTRACKER_PATH % ( 170 self._properties[KEY_EFFORT_NAME], uuid)) 171 os.makedirs(dir_path, exist_ok=True) 172 return dir_path 173