Home | History | Annotate | Download | only in web
      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 
     17 import base64
     18 import getpass
     19 import logging
     20 import os
     21 import socket
     22 import time
     23 
     24 from vts.proto import VtsReportMessage_pb2 as ReportMsg
     25 from vts.runners.host import keys
     26 from vts.utils.python.web import dashboard_rest_client
     27 from vts.utils.python.web import feature_utils
     28 
     29 _PROFILING_POINTS = "profiling_points"
     30 
     31 
     32 class WebFeature(feature_utils.Feature):
     33     """Feature object for web functionality.
     34 
     35     Attributes:
     36         enabled: boolean, True if systrace is enabled, False otherwise
     37         report_msg: TestReportMessage, Proto summarizing the test run
     38         current_test_report_msg: TestCaseReportMessage, Proto summarizing the current test case
     39         rest_client: DashboardRestClient, client to which data will be posted
     40     """
     41 
     42     _TOGGLE_PARAM = keys.ConfigKeys.IKEY_ENABLE_WEB
     43     _REQUIRED_PARAMS = [
     44         keys.ConfigKeys.IKEY_DASHBOARD_POST_COMMAND,
     45         keys.ConfigKeys.IKEY_SERVICE_JSON_PATH,
     46         keys.ConfigKeys.KEY_TESTBED_NAME, keys.ConfigKeys.IKEY_BUILD,
     47         keys.ConfigKeys.IKEY_ANDROID_DEVICE, keys.ConfigKeys.IKEY_ABI_NAME,
     48         keys.ConfigKeys.IKEY_ABI_BITNESS
     49     ]
     50     _OPTIONAL_PARAMS = []
     51 
     52     def __init__(self, user_params):
     53         """Initializes the web feature.
     54 
     55         Parses the arguments and initializes the web functionality.
     56 
     57         Args:
     58             user_params: A dictionary from parameter name (String) to parameter value.
     59         """
     60         self.ParseParameters(
     61             toggle_param_name=self._TOGGLE_PARAM,
     62             required_param_names=self._REQUIRED_PARAMS,
     63             optional_param_names=self._OPTIONAL_PARAMS,
     64             user_params=user_params)
     65         if not self.enabled:
     66             return
     67 
     68         # Initialize the dashboard client
     69         post_cmd = getattr(self, keys.ConfigKeys.IKEY_DASHBOARD_POST_COMMAND)
     70         service_json_path = str(
     71             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 GetTestModuleKeys(self):
    364         """Returns the test module name and start timestamp.
    365 
    366         Those two values can be used to find the corresponding entry
    367         in a used nosql database without having to lock all the data
    368         (which is infesiable) thus are essential for strong consistency.
    369         """
    370         return self.report_msg.test, self.report_msg.start_timestamp
    371 
    372     def GenerateReportMessage(self, requested, executed):
    373         """Uploads the result to the web service.
    374 
    375         Requires the feature to be enabled; no-op otherwise.
    376 
    377         Args:
    378             requested: list, A list of test case records requested to run
    379             executed: list, A list of test case records that were executed
    380 
    381         Returns:
    382             binary string, serialized report message.
    383             None if web is not enabled.
    384         """
    385         if not self.enabled:
    386             return None
    387 
    388         # Handle case when runner fails, tests aren't executed
    389         if (executed and executed[-1].test_name == "setup_class"):
    390             # Test failed during setup, all tests were not executed
    391             start_index = 0
    392         else:
    393             # Runner was aborted. Remaining tests weren't executed
    394             start_index = len(executed)
    395 
    396         for test in requested[start_index:]:
    397             msg = self.report_msg.test_case.add()
    398             msg.name = test.test_name
    399             msg.start_timestamp = feature_utils.GetTimestamp()
    400             msg.end_timestamp = msg.start_timestamp
    401             msg.test_result = ReportMsg.TEST_CASE_RESULT_FAIL
    402 
    403         self.report_msg.end_timestamp = feature_utils.GetTimestamp()
    404 
    405         build = getattr(self, keys.ConfigKeys.IKEY_BUILD)
    406         if keys.ConfigKeys.IKEY_BUILD_ID in build:
    407             build_id = str(build[keys.ConfigKeys.IKEY_BUILD_ID])
    408             self.report_msg.build_info.id = build_id
    409 
    410         logging.info("_tearDownClass hook: start (username: %s)",
    411                      getpass.getuser())
    412 
    413         if len(self.report_msg.test_case) == 0:
    414             logging.info("_tearDownClass hook: skip uploading (no test case)")
    415             return ''
    416 
    417         post_msg = ReportMsg.DashboardPostMessage()
    418         post_msg.test_report.extend([self.report_msg])
    419 
    420         self.rest_client.AddAuthToken(post_msg)
    421 
    422         message_b = base64.b64encode(post_msg.SerializeToString())
    423 
    424         logging.info('Result proto message generated. size: %s',
    425                      len(message_b))
    426 
    427         logging.info("_tearDownClass hook: status upload time stamp %s",
    428                      str(self.report_msg.start_timestamp))
    429 
    430         return message_b
    431