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
17from functools import wraps
18
19from acts import context
20from acts.metrics.core import ProtoMetric
21from acts.metrics.core import ProtoMetricPublisher
22from acts.metrics.loggers.protos.gen import acts_usage_metadata_pb2
23
24
25_usage_map = {}
26
27
28def record_api_usage(f):
29    """Decorator method that wraps api functions to log their usage.
30    Reads __module__ and __name__ attributes of the function to pass
31    as args to log_usage
32
33    Args:
34        f: The function to record.
35
36    Returns:
37        A wrapper function with the same name as the original
38        function that records function calls.
39    """
40    @wraps(f)
41    def wrapper(*args, **kwargs):
42        log_usage(f.__module__, f.__name__)
43        return f(*args, **kwargs)
44
45    return wrapper
46
47
48def log_usage(module_name, func_name):
49    """Creates a dict key from the params and the current context, and
50    uses them to update a counter for that key.
51    Key is an instance of UsageMetadataKey, where
52    method_name: module_name.func_name
53    test_context: current_context.identifier
54
55    Args:
56        module_name: function module
57        func_name: function name
58    """
59    current_context = context.get_current_context()
60
61    name = '.'.join([module_name, func_name])
62
63    usage_key = UsageMetadataKey(name, current_context.identifier)
64
65    _usage_map[usage_key] = _usage_map.get(usage_key, 0) + 1
66
67
68def _reset():
69    """Clear the _usage_map.
70    """
71    _usage_map.clear()
72
73
74class UsageMetadataPublisher(ProtoMetricPublisher):
75    """Publisher with the added ability to convert the _usage_map into a
76    ProtoMetric object for publishing.
77    """
78
79    def __init__(self):
80        """Initializes the publisher, passing RootContext to the parent
81        implementation.
82        """
83        super().__init__(context.RootContext())
84
85    def publish(self):
86        """Create a ProtoMetric object and call _publish_single implementation,
87         typically to write ProtoMetric to a file.
88        """
89        metric = self._usage_map_to_proto_metric()
90        self._publish_single(metric)
91
92    def _usage_map_to_proto_metric(self):
93        """Iterate over _usage_map, creating an ActsMethodUsageMetadata for
94        each entry.
95
96        Returns:
97            ProtoMetric wrapper object with name='acts_usage_metadata' and
98            data=ActsTestRunMetadata()
99        """
100        data = acts_usage_metadata_pb2.ActsTestRunMetadata()
101
102        for usage in _usage_map:
103            method_count = data.usage.add()
104            method_count.test_context = usage.test_context
105            method_count.method_identifier = usage.method_name
106            method_count.count = _usage_map[usage]
107
108        return ProtoMetric(name='acts_usage_metadata', data=data)
109
110
111class UsageMetadataKey:
112    """Dict key for counting method invocations. Used as keys in _usage_map.
113    Simple tuple object with hash and eq.
114
115    Attributes:
116        test_context: Identifier for calling test
117        method_name: Identifier for api method
118    """
119
120    def __init__(self, method_name, test_context):
121        """Initialize a UsageMetadataKey.
122
123        Args:
124            method_name: Fully qualified name of the method to record,
125                typically an ACTS api method.
126            test_context: Contextual identifier
127        """
128        self.test_context = test_context
129        self.method_name = method_name
130
131    def __hash__(self):
132        return hash(self.test_context) ^ hash(self.method_name)
133
134    def __eq__(self, other):
135        return (self.test_context == other.test_context and
136            self.method_name == other.method_name)
137
138    def __repr__(self):
139        return ('UsageMetadataKey{' + self.method_name + ', ' +
140               self.test_context + '}')
141