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 48 49class TestTrackerResultsWriter(object): 50 """Takes a test record, converts it to a TestTracker result proto, and 51 writes it to the log directory. In automation, these protos will 52 automatically be read from Sponge and uploaded to TestTracker. 53 """ 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 = [ 108 p for p in required_props if p not in self._properties 109 ] 110 if missing_props: 111 raise TestTrackerError( 112 'Missing the following required properties for TestTracker: %s' 113 % missing_props) 114 115 @staticmethod 116 def _add_property(result_proto, name, value): 117 """Adds a Property to a given Result proto 118 119 Args: 120 result_proto: Result proto to modify 121 name: Property name 122 value: Property value 123 """ 124 new_prop = result_proto.property.add() 125 new_prop.name = name 126 if isinstance(value, bool): 127 new_prop.bool_value = value 128 elif isinstance(value, int): 129 new_prop.int_value = value 130 elif isinstance(value, float): 131 new_prop.double_value = value 132 else: 133 new_prop.string_value = str(value) 134 135 def _create_result_proto(self, record): 136 """Create a Result proto object from test record. Fills in uuid, status, 137 and properties with info gathered from the test record. 138 139 Args: 140 record: An acts.records.TestResultRecord object 141 142 Returns: Result proto, or None if record is invalid 143 """ 144 uuid = record.extras[KEY_TESTTRACKER_UUID] 145 result = Result() 146 result.uuid = uuid 147 result.status = _TEST_RESULT_TO_STATUS_MAP[record.result] 148 result.timestamp = (datetime.datetime.fromtimestamp( 149 record.begin_time / 1000, 150 datetime.timezone.utc).isoformat(timespec='milliseconds').replace( 151 '+00:00', 'Z')) 152 153 self._add_property(result, KEY_UUID, uuid) 154 if record.details: 155 self._add_property(result, KEY_DETAILS, record.details) 156 157 for key, value in self._properties.items(): 158 self._add_property(result, key, value) 159 160 return result 161 162 def _create_results_dir(self, uuid): 163 """Creates the TestTracker directory given the test uuid 164 165 Args: 166 uuid: The TestTracker uuid of the test 167 168 Returns: Path to the created directory. 169 """ 170 dir_path = os.path.join( 171 self._log_path, 172 TESTTRACKER_PATH % (self._properties[KEY_EFFORT_NAME], uuid)) 173 os.makedirs(dir_path, exist_ok=True) 174 return dir_path 175