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 os
20import time
21
22from acts.metrics.core import ProtoMetric
23from acts.metrics.logger import MetricLogger
24from acts.test_utils.bt.loggers.protos import bluetooth_metric_pb2
25
26
27def recursive_assign(proto, dct):
28    """Assign values in dct to proto recursively."""
29    for metric in dir(proto):
30        if metric in dct:
31            if (isinstance(dct[metric], dict) and
32                    isinstance(getattr(proto, metric), message.Message)):
33                recursive_assign(getattr(proto, metric), dct[metric])
34            else:
35                setattr(proto, metric, dct[metric])
36
37
38class BluetoothMetricLogger(MetricLogger):
39    """A logger for gathering Bluetooth test metrics
40
41    Attributes:
42        proto_module: Module used to store Bluetooth metrics in a proto
43        results: Stores ProtoMetrics to be published for each logger context
44        proto_map: Maps test case names to the appropriate protos for each case
45    """
46
47    def __init__(self, event):
48        super().__init__(event=event)
49        self.proto_module = bluetooth_metric_pb2
50        self.results = []
51        self.start_time = int(time.time())
52
53        self.proto_map = {'BluetoothPairAndConnectTest': self.proto_module
54                              .BluetoothPairAndConnectTestResult(),
55                          'BluetoothReconnectTest': self.proto_module
56                              .BluetoothReconnectTestResult(),
57                          'BluetoothThroughputTest': self.proto_module
58                              .BluetoothDataTestResult(),
59                          'BluetoothLatencyTest': self.proto_module
60                              .BluetoothDataTestResult(),
61                          'BtCodecSweepTest': self.proto_module
62                              .BluetoothAudioTestResult(),
63                          'BtRangeCodecTest': self.proto_module
64                              .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 = {'device_class': 'phone',
86                    'device_model': device.model,
87                    'android_release_id': device.build_info['build_id'],
88                    'android_build_type': device.build_info['build_type'],
89                    'android_build_number': device.build_info[
90                        'incremental_build_id'],
91                    'android_branch_name': 'git_qt-release',
92                    'software_version': device.build_info['build_id']}
93
94        if device.__class__.__name__ == 'ParentDevice':
95            data = {'device_class': 'headset',
96                    'device_model': device.dut_type,
97                    'software_version': device.get_version()[1][
98                        'Fw Build Label'],
99                    'android_build_number': device.version}
100
101        return data
102
103    def add_config_data_to_proto(self, proto, pri_device, conn_device=None):
104        """Add to configuration data field of proto.
105
106        Adds test start time and device configuration info.
107        Args:
108            proto: protobuf to add configuration data to.
109            pri_device: some controller object.
110            conn_device: optional second controller object.
111        """
112        pri_device_proto = proto.configuration_data.primary_device
113        conn_device_proto = proto.configuration_data.connected_device
114        proto.configuration_data.test_date_time = self.start_time
115
116        pri_config = self.get_configuration_data(pri_device)
117
118        for metric in dir(pri_device_proto):
119            if metric in pri_config:
120                setattr(pri_device_proto, metric, pri_config[metric])
121
122        if conn_device:
123            conn_config = self.get_configuration_data(conn_device)
124
125            for metric in dir(conn_device_proto):
126                if metric in conn_config:
127                    setattr(conn_device_proto, metric, conn_config[metric])
128
129    def get_proto_dict(self, test, proto):
130        """Return dict with proto, readable ascii proto, and test name."""
131        return {'proto': base64.b64encode(ProtoMetric(test, proto)
132                                          .get_binary()).decode('utf-8'),
133                'proto_ascii': ProtoMetric(test, proto).get_ascii(),
134                'test_name': test}
135
136    def add_proto_to_results(self, proto, test):
137        """Adds proto as ProtoMetric object to self.results."""
138        self.results.append(ProtoMetric(test, proto))
139
140    def get_results(self, results, test, pri_device, conn_device=None):
141        """Gets the metrics associated with each test case.
142
143        Gets the test case metrics and configuration data for each test case and
144        stores them for publishing.
145
146        Args:
147            results: A dictionary containing test metrics.
148            test: The name of the test case associated with these results.
149            pri_device: The primary AndroidDevice object for the test.
150            conn_device: The connected AndroidDevice object for the test, if
151                applicable.
152
153        """
154
155        proto_result = self.proto_map[test]
156        recursive_assign(proto_result, results)
157        self.add_config_data_to_proto(proto_result, pri_device, conn_device)
158        self.add_proto_to_results(proto_result, test)
159        return self.get_proto_dict(test, proto_result)
160
161    def end(self, event):
162        return self.publisher.publish(self.results)
163