1#
2# Copyright (C) 2017 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import getpass
18import logging
19import os
20import socket
21import time
22
23from vts.proto import VtsReportMessage_pb2 as ReportMsg
24from vts.runners.host import keys
25from vts.utils.python.web import dashboard_rest_client
26from vts.utils.python.web import feature_utils
27
28_PROFILING_POINTS = "profiling_points"
29
30
31class WebFeature(feature_utils.Feature):
32    """Feature object for web functionality.
33
34    Attributes:
35        enabled: boolean, True if systrace is enabled, False otherwise
36        report_msg: TestReportMessage, Proto summarizing the test run
37        current_test_report_msg: TestCaseReportMessage, Proto summarizing the current test case
38        rest_client: DashboardRestClient, client to which data will be posted
39    """
40
41    _TOGGLE_PARAM = keys.ConfigKeys.IKEY_ENABLE_WEB
42    _REQUIRED_PARAMS = [
43        keys.ConfigKeys.IKEY_DASHBOARD_POST_COMMAND,
44        keys.ConfigKeys.IKEY_SERVICE_JSON_PATH,
45        keys.ConfigKeys.KEY_TESTBED_NAME,
46        keys.ConfigKeys.IKEY_BUILD,
47        keys.ConfigKeys.IKEY_ANDROID_DEVICE,
48        keys.ConfigKeys.IKEY_ABI_NAME,
49        keys.ConfigKeys.IKEY_ABI_BITNESS
50    ]
51    _OPTIONAL_PARAMS = []
52
53    def __init__(self, user_params):
54        """Initializes the web feature.
55
56        Parses the arguments and initializes the web functionality.
57
58        Args:
59            user_params: A dictionary from parameter name (String) to parameter value.
60        """
61        self.ParseParameters(
62            toggle_param_name=self._TOGGLE_PARAM,
63            required_param_names=self._REQUIRED_PARAMS,
64            optional_param_names=self._OPTIONAL_PARAMS,
65            user_params=user_params)
66        if not self.enabled:
67            return
68
69        # Initialize the dashboard client
70        post_cmd = getattr(self, keys.ConfigKeys.IKEY_DASHBOARD_POST_COMMAND)
71        service_json_path = str(getattr(self, keys.ConfigKeys.IKEY_SERVICE_JSON_PATH))
72        self.rest_client = dashboard_rest_client.DashboardRestClient(
73            post_cmd, service_json_path)
74        if not self.rest_client.Initialize():
75            self.enabled = False
76
77        self.report_msg = ReportMsg.TestReportMessage()
78        self.report_msg.test = str(
79            getattr(self, keys.ConfigKeys.KEY_TESTBED_NAME))
80        self.report_msg.test_type = ReportMsg.VTS_HOST_DRIVEN_STRUCTURAL
81        self.report_msg.start_timestamp = feature_utils.GetTimestamp()
82        self.report_msg.host_info.hostname = socket.gethostname()
83
84        android_devices = getattr(self, keys.ConfigKeys.IKEY_ANDROID_DEVICE,
85                                  None)
86        if not android_devices or not isinstance(android_devices, list):
87            logging.warn("android device information not available")
88            return
89
90        for device_spec in android_devices:
91            dev_info = self.report_msg.device_info.add()
92            for elem in [
93                    keys.ConfigKeys.IKEY_PRODUCT_TYPE,
94                    keys.ConfigKeys.IKEY_PRODUCT_VARIANT,
95                    keys.ConfigKeys.IKEY_BUILD_FLAVOR,
96                    keys.ConfigKeys.IKEY_BUILD_ID, keys.ConfigKeys.IKEY_BRANCH,
97                    keys.ConfigKeys.IKEY_BUILD_ALIAS,
98                    keys.ConfigKeys.IKEY_API_LEVEL, keys.ConfigKeys.IKEY_SERIAL
99            ]:
100                if elem in device_spec:
101                    setattr(dev_info, elem, str(device_spec[elem]))
102            # TODO: get abi information differently for multi-device support.
103            setattr(dev_info, keys.ConfigKeys.IKEY_ABI_NAME,
104                    str(getattr(self, keys.ConfigKeys.IKEY_ABI_NAME)))
105            setattr(dev_info, keys.ConfigKeys.IKEY_ABI_BITNESS,
106                    str(getattr(self, keys.ConfigKeys.IKEY_ABI_BITNESS)))
107
108    def SetTestResult(self, result=None):
109        """Set the current test case result to the provided result.
110
111        If None is provided as a result, the current test report will be cleared, which results
112        in a silent skip.
113
114        Requires the feature to be enabled; no-op otherwise.
115
116        Args:
117            result: ReportMsg.TestCaseResult, the result of the current test or None.
118        """
119        if not self.enabled:
120            return
121
122        if not result:
123            self.report_msg.test_case.remove(self.current_test_report_msg)
124            self.current_test_report_msg = None
125        else:
126            self.current_test_report_msg.test_result = result
127
128    def AddTestReport(self, test_name):
129        """Creates a report for the specified test.
130
131        Requires the feature to be enabled; no-op otherwise.
132
133        Args:
134            test_name: String, the name of the test
135        """
136        if not self.enabled:
137            return
138        self.current_test_report_msg = self.report_msg.test_case.add()
139        self.current_test_report_msg.name = test_name
140        self.current_test_report_msg.start_timestamp = feature_utils.GetTimestamp(
141        )
142
143    def AddCoverageReport(self,
144                          coverage_vec,
145                          src_file_path,
146                          git_project_name,
147                          git_project_path,
148                          revision,
149                          covered_count,
150                          line_count,
151                          isGlobal=True):
152        """Adds a coverage report to the VtsReportMessage.
153
154        Processes the source information, git project information, and processed
155        coverage information and stores it into a CoverageReportMessage within the
156        report message.
157
158        Args:
159            coverage_vec: list, list of coverage counts (int) for each line
160            src_file_path: the path to the original source file
161            git_project_name: the name of the git project containing the source
162            git_project_path: the path from the root to the git project
163            revision: the commit hash identifying the source code that was used to
164                      build a device image
165            covered_count: int, number of lines covered
166            line_count: int, total number of lines
167            isGlobal: boolean, True if the coverage data is for the entire test, False if only for
168                      the current test case.
169        """
170        if not self.enabled:
171            return
172
173        if isGlobal:
174            report = self.report_msg
175        else:
176            report = self.current_test_report_msg
177
178        coverage = report.coverage.add()
179        coverage.total_line_count = line_count
180        coverage.covered_line_count = covered_count
181        coverage.line_coverage_vector.extend(coverage_vec)
182
183        src_file_path = os.path.relpath(src_file_path, git_project_path)
184        coverage.file_path = src_file_path
185        coverage.revision = revision
186        coverage.project_name = git_project_name
187
188    def AddProfilingDataTimestamp(
189            self,
190            name,
191            start_timestamp,
192            end_timestamp,
193            x_axis_label="Latency (nano secs)",
194            y_axis_label="Frequency",
195            regression_mode=ReportMsg.VTS_REGRESSION_MODE_INCREASING):
196        """Adds the timestamp profiling data to the web DB.
197
198        Requires the feature to be enabled; no-op otherwise.
199
200        Args:
201            name: string, profiling point name.
202            start_timestamp: long, nanoseconds start time.
203            end_timestamp: long, nanoseconds end time.
204            x-axis_label: string, the x-axis label title for a graph plot.
205            y-axis_label: string, the y-axis label title for a graph plot.
206            regression_mode: specifies the direction of change which indicates
207                             performance regression.
208        """
209        if not self.enabled:
210            return
211
212        if not hasattr(self, _PROFILING_POINTS):
213            setattr(self, _PROFILING_POINTS, set())
214
215        if name in getattr(self, _PROFILING_POINTS):
216            logging.error("profiling point %s is already active.", name)
217            return
218
219        getattr(self, _PROFILING_POINTS).add(name)
220        profiling_msg = self.report_msg.profiling.add()
221        profiling_msg.name = name
222        profiling_msg.type = ReportMsg.VTS_PROFILING_TYPE_TIMESTAMP
223        profiling_msg.regression_mode = regression_mode
224        profiling_msg.start_timestamp = start_timestamp
225        profiling_msg.end_timestamp = end_timestamp
226        profiling_msg.x_axis_label = x_axis_label
227        profiling_msg.y_axis_label = y_axis_label
228
229    def AddProfilingDataVector(
230            self,
231            name,
232            labels,
233            values,
234            data_type,
235            options=[],
236            x_axis_label="x-axis",
237            y_axis_label="y-axis",
238            regression_mode=ReportMsg.VTS_REGRESSION_MODE_INCREASING):
239        """Adds the vector profiling data in order to upload to the web DB.
240
241        Requires the feature to be enabled; no-op otherwise.
242
243        Args:
244            name: string, profiling point name.
245            labels: a list or set of labels.
246            values: a list or set of values where each value is an integer.
247            data_type: profiling data type.
248            options: a set of options.
249            x-axis_label: string, the x-axis label title for a graph plot.
250            y-axis_label: string, the y-axis label title for a graph plot.
251            regression_mode: specifies the direction of change which indicates
252                             performance regression.
253        """
254        if not self.enabled:
255            return
256
257        if not hasattr(self, _PROFILING_POINTS):
258            setattr(self, _PROFILING_POINTS, set())
259
260        if name in getattr(self, _PROFILING_POINTS):
261            logging.error("profiling point %s is already active.", name)
262            return
263
264        getattr(self, _PROFILING_POINTS).add(name)
265        profiling_msg = self.report_msg.profiling.add()
266        profiling_msg.name = name
267        profiling_msg.type = data_type
268        profiling_msg.regression_mode = regression_mode
269        if labels:
270            profiling_msg.label.extend(labels)
271        profiling_msg.value.extend(values)
272        profiling_msg.x_axis_label = x_axis_label
273        profiling_msg.y_axis_label = y_axis_label
274        profiling_msg.options.extend(options)
275
276    def AddProfilingDataLabeledVector(
277            self,
278            name,
279            labels,
280            values,
281            options=[],
282            x_axis_label="x-axis",
283            y_axis_label="y-axis",
284            regression_mode=ReportMsg.VTS_REGRESSION_MODE_INCREASING):
285        """Adds the labeled vector profiling data in order to upload to the web DB.
286
287        Requires the feature to be enabled; no-op otherwise.
288
289        Args:
290            name: string, profiling point name.
291            labels: a list or set of labels.
292            values: a list or set of values where each value is an integer.
293            options: a set of options.
294            x-axis_label: string, the x-axis label title for a graph plot.
295            y-axis_label: string, the y-axis label title for a graph plot.
296            regression_mode: specifies the direction of change which indicates
297                             performance regression.
298        """
299        self.AddProfilingDataVector(
300            name, labels, values, ReportMsg.VTS_PROFILING_TYPE_LABELED_VECTOR,
301            options, x_axis_label, y_axis_label, regression_mode)
302
303    def AddProfilingDataUnlabeledVector(
304            self,
305            name,
306            values,
307            options=[],
308            x_axis_label="x-axis",
309            y_axis_label="y-axis",
310            regression_mode=ReportMsg.VTS_REGRESSION_MODE_INCREASING):
311        """Adds the unlabeled vector profiling data in order to upload to the web DB.
312
313        Requires the feature to be enabled; no-op otherwise.
314
315        Args:
316            name: string, profiling point name.
317            values: a list or set of values where each value is an integer.
318            options: a set of options.
319            x-axis_label: string, the x-axis label title for a graph plot.
320            y-axis_label: string, the y-axis label title for a graph plot.
321            regression_mode: specifies the direction of change which indicates
322                             performance regression.
323        """
324        self.AddProfilingDataVector(
325            name, None, values, ReportMsg.VTS_PROFILING_TYPE_UNLABELED_VECTOR,
326            options, x_axis_label, y_axis_label, regression_mode)
327
328    def AddSystraceUrl(self, url):
329        """Creates a systrace report message with a systrace URL.
330
331        Adds a systrace report to the current test case report and supplies the
332        url to the systrace report.
333
334        Requires the feature to be enabled; no-op otherwise.
335
336        Args:
337            url: String, the url of the systrace report.
338        """
339        if not self.enabled:
340            return
341        systrace_msg = self.current_test_report_msg.systrace.add()
342        systrace_msg.url.append(url)
343
344    def AddLogUrls(self, urls):
345        """Creates a log message with log file URLs.
346
347        Adds a log message to the current test module report and supplies the
348        url to the log files.
349
350        Requires the feature to be enabled; no-op otherwise.
351
352        Args:
353            urls: list of string, the URLs of the logs.
354        """
355        if not self.enabled or urls is None:
356            return
357
358        for url in urls:
359            log_msg = self.report_msg.log.add()
360            log_msg.url = url
361            log_msg.name = os.path.basename(url)
362
363    def Upload(self, requested, executed):
364        """Uploads the result to the web service.
365
366        Requires the feature to be enabled; no-op otherwise.
367
368        Args:
369            requested: list, A list of test case names requested to run
370            executed: list, A list of test case names that were executed
371        """
372        if not self.enabled:
373            return
374
375        # Handle case when runner fails, tests aren't executed
376        if (executed and executed[-1].test_name == "setup_class"):
377            # Test failed during setup, all tests were not executed
378            start_index = 0
379        else:
380            # Runner was aborted. Remaining tests weren't executed
381            start_index = len(executed)
382
383        for test in requested[start_index:]:
384            msg = self.report_msg.test_case.add()
385            msg.name = test
386            msg.start_timestamp = feature_utils.GetTimestamp()
387            msg.end_timestamp = msg.start_timestamp
388            msg.test_result = ReportMsg.TEST_CASE_RESULT_FAIL
389
390        self.report_msg.end_timestamp = feature_utils.GetTimestamp()
391
392        build = getattr(self, keys.ConfigKeys.IKEY_BUILD)
393        if keys.ConfigKeys.IKEY_BUILD_ID in build:
394            build_id = str(build[keys.ConfigKeys.IKEY_BUILD_ID])
395            self.report_msg.build_info.id = build_id
396
397        logging.info("_tearDownClass hook: start (username: %s)",
398                     getpass.getuser())
399
400        if len(self.report_msg.test_case) > 0:
401            post_msg = ReportMsg.DashboardPostMessage()
402            post_msg.test_report.extend([self.report_msg])
403
404            # Post new data to the dashboard
405            self.rest_client.PostData(post_msg)
406
407            logging.info("_tearDownClass hook: status upload time stamp %s",
408                         str(self.report_msg.start_timestamp))
409        else:
410            logging.info("_tearDownClass hook: skip uploading (no test case)")
411