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