1# /usr/bin/env python3
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# 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, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16
17import base64
18from google.protobuf import message
19import time
20
21from acts.metrics.core import ProtoMetric
22from acts.metrics.logger import MetricLogger
23from acts_contrib.test_utils.bt.loggers.protos import bluetooth_metric_pb2
24
25
26def recursive_assign(proto, dct):
27    """Assign values in dct to proto recursively."""
28    for metric in dir(proto):
29        if metric in dct:
30            if (isinstance(dct[metric], dict)
31                    and isinstance(getattr(proto, metric), message.Message)):
32                recursive_assign(getattr(proto, metric), dct[metric])
33            else:
34                setattr(proto, metric, dct[metric])
35
36
37class BluetoothMetricLogger(MetricLogger):
38    """A logger for gathering Bluetooth test metrics
39
40    Attributes:
41        proto_module: Module used to store Bluetooth metrics in a proto
42        results: Stores ProtoMetrics to be published for each logger context
43        proto_map: Maps test case names to the appropriate protos for each case
44    """
45
46    def __init__(self, event):
47        super().__init__(event=event)
48        self.proto_module = bluetooth_metric_pb2
49        self.results = []
50        self.start_time = int(time.time())
51
52        self.proto_map = {
53            'BluetoothPairAndConnectTest':
54            self.proto_module.BluetoothPairAndConnectTestResult(),
55            'BluetoothReconnectTest':
56            self.proto_module.BluetoothReconnectTestResult(),
57            'BluetoothThroughputTest':
58            self.proto_module.BluetoothDataTestResult(),
59            'BluetoothLatencyTest':
60            self.proto_module.BluetoothDataTestResult(),
61            'BtCodecSweepTest':
62            self.proto_module.BluetoothAudioTestResult(),
63            'BtRangeCodecTest':
64            self.proto_module.BluetoothAudioTestResult(),
65        }
66
67    @staticmethod
68    def get_configuration_data(device):
69        """Gets the configuration data of a device.
70
71        Gets the configuration data of a device and organizes it in a
72        dictionary.
73
74        Args:
75            device: The device object to get the configuration data from.
76
77        Returns:
78            A dictionary containing configuration data of a device.
79        """
80        # TODO(b/126931820): Genericize and move to lib when generic DUT interface is implemented
81        data = {'device_class': device.__class__.__name__}
82
83        if device.__class__.__name__ == 'AndroidDevice':
84            # TODO(b/124066126): Add remaining config data
85            data = {
86                'device_class': 'phone',
87                'device_model': device.model,
88                'android_release_id': device.build_info['build_id'],
89                'android_build_type': device.build_info['build_type'],
90                'android_build_number':
91                device.build_info['incremental_build_id'],
92                'android_branch_name': 'git_qt-release',
93                'software_version': device.build_info['build_id']
94            }
95
96        if device.__class__.__name__ == 'ParentDevice':
97            data = {
98                'device_class': 'headset',
99                'device_model': device.dut_type,
100                'software_version': device.get_version()[1]['Fw Build Label'],
101                'android_build_number': device.version
102            }
103
104        return data
105
106    def add_config_data_to_proto(self, proto, pri_device, conn_device=None):
107        """Add to configuration data field of proto.
108
109        Adds test start time and device configuration info.
110        Args:
111            proto: protobuf to add configuration data to.
112            pri_device: some controller object.
113            conn_device: optional second controller object.
114        """
115        pri_device_proto = proto.configuration_data.primary_device
116        conn_device_proto = proto.configuration_data.connected_device
117        proto.configuration_data.test_date_time = self.start_time
118
119        pri_config = self.get_configuration_data(pri_device)
120
121        for metric in dir(pri_device_proto):
122            if metric in pri_config:
123                setattr(pri_device_proto, metric, pri_config[metric])
124
125        if conn_device:
126            conn_config = self.get_configuration_data(conn_device)
127
128            for metric in dir(conn_device_proto):
129                if metric in conn_config:
130                    setattr(conn_device_proto, metric, conn_config[metric])
131
132    def get_proto_dict(self, test, proto):
133        """Return dict with proto, readable ascii proto, and test name."""
134        return {
135            'proto':
136            base64.b64encode(ProtoMetric(test,
137                                         proto).get_binary()).decode('utf-8'),
138            'proto_ascii':
139            ProtoMetric(test, proto).get_ascii(),
140            'test_name':
141            test
142        }
143
144    def add_proto_to_results(self, proto, test):
145        """Adds proto as ProtoMetric object to self.results."""
146        self.results.append(ProtoMetric(test, proto))
147
148    def get_results(self, results, test, pri_device, conn_device=None):
149        """Gets the metrics associated with each test case.
150
151        Gets the test case metrics and configuration data for each test case and
152        stores them for publishing.
153
154        Args:
155            results: A dictionary containing test metrics.
156            test: The name of the test case associated with these results.
157            pri_device: The primary AndroidDevice object for the test.
158            conn_device: The connected AndroidDevice object for the test, if
159                applicable.
160
161        """
162
163        proto_result = self.proto_map[test]
164        recursive_assign(proto_result, results)
165        self.add_config_data_to_proto(proto_result, pri_device, conn_device)
166        self.add_proto_to_results(proto_result, test)
167        return self.get_proto_dict(test, proto_result)
168
169    def end(self, event):
170        return self.publisher.publish(self.results)
171