1#
2# Copyright (C) 2016 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.
15import argparse
16import io
17import json
18import logging
19import os
20import shutil
21import sys
22import time
23import zipfile
24
25from vts.proto import VtsReportMessage_pb2 as ReportMsg
26from vts.runners.host import keys
27from vts.utils.python.archive import archive_parser
28from vts.utils.python.common import cmd_utils
29from vts.utils.python.controllers.adb import AdbError
30from vts.utils.python.coverage import coverage_report
31from vts.utils.python.coverage import gcda_parser
32from vts.utils.python.coverage import gcno_parser
33from vts.utils.python.coverage.parser import FileFormatError
34from vts.utils.python.os import path_utils
35from vts.utils.python.web import feature_utils
36
37FLUSH_PATH_VAR = "GCOV_PREFIX"  # environment variable for gcov flush path
38TARGET_COVERAGE_PATH = "/data/misc/trace/"  # location to flush coverage
39LOCAL_COVERAGE_PATH = "/tmp/vts-test-coverage"  # location to pull coverage to host
40
41# Environment for test process
42COVERAGE_TEST_ENV = "GCOV_PREFIX_OVERRIDE=true GCOV_PREFIX=/data/misc/trace/self"
43
44GCNO_SUFFIX = ".gcno"
45GCDA_SUFFIX = ".gcda"
46COVERAGE_SUFFIX = ".gcnodir"
47GIT_PROJECT = "git_project"
48MODULE_NAME = "module_name"
49NAME = "name"
50PATH = "path"
51GEN_TAG = "/gen/"
52
53_BUILD_INFO = "BUILD_INFO"  # name of build info artifact
54_GCOV_ZIP = "gcov.zip"  # name of gcov artifact zip
55_REPO_DICT = "repo-dict"  # name of dictionary from project to revision in BUILD_INFO
56
57_CLEAN_TRACE_COMMAND = "rm -rf /data/misc/trace/*"
58_FLUSH_COMMAND = (
59    "GCOV_PREFIX_OVERRIDE=true GCOV_PREFIX=/data/local/tmp/flusher "
60    "/data/local/tmp/vts_coverage_configure flush")
61_SP_COVERAGE_PATH = "self"  # relative location where same-process coverage is dumped.
62
63_CHECKSUM_GCNO_DICT = "checksum_gcno_dict"
64_COVERAGE_ZIP = "coverage_zip"
65_REVISION_DICT = "revision_dict"
66
67
68class CoverageFeature(feature_utils.Feature):
69    """Feature object for coverage functionality.
70
71    Attributes:
72        enabled: boolean, True if coverage is enabled, False otherwise
73        web: (optional) WebFeature, object storing web feature util for test run
74        local_coverage_path: path to store the coverage files.
75        _device_resource_dict: a map from device serial number to host resources directory.
76        _hal_names: the list of hal names for which to process coverage.
77        _coverage_report_file_prefix: prefix of the output coverage report file.
78    """
79
80    _TOGGLE_PARAM = keys.ConfigKeys.IKEY_ENABLE_COVERAGE
81    _REQUIRED_PARAMS = [keys.ConfigKeys.IKEY_ANDROID_DEVICE]
82    _OPTIONAL_PARAMS = [
83        keys.ConfigKeys.IKEY_MODULES,
84        keys.ConfigKeys.IKEY_OUTPUT_COVERAGE_REPORT,
85        keys.ConfigKeys.IKEY_GLOBAL_COVERAGE,
86        keys.ConfigKeys.IKEY_EXCLUDE_COVERAGE_PATH,
87        keys.ConfigKeys.IKEY_COVERAGE_REPORT_PATH,
88    ]
89
90    _DEFAULT_EXCLUDE_PATHS = [
91        "bionic", "external/libcxx", "system/core", "system/libhidl",
92        "system/libfmq"
93    ]
94
95    def __init__(self, user_params, web=None):
96        """Initializes the coverage feature.
97
98        Args:
99            user_params: A dictionary from parameter name (String) to parameter value.
100            web: (optional) WebFeature, object storing web feature util for test run
101            local_coverage_path: (optional) path to store the .gcda files and coverage reports.
102        """
103        self.ParseParameters(self._TOGGLE_PARAM, self._REQUIRED_PARAMS,
104                             self._OPTIONAL_PARAMS, user_params)
105        self.web = web
106        self._device_resource_dict = {}
107        self._hal_names = None
108
109        timestamp_seconds = str(int(time.time() * 1000000))
110        self.local_coverage_path = os.path.join(LOCAL_COVERAGE_PATH,
111                                                timestamp_seconds)
112        if os.path.exists(self.local_coverage_path):
113            logging.debug("removing existing coverage path: %s",
114                          self.local_coverage_path)
115            shutil.rmtree(self.local_coverage_path)
116        os.makedirs(self.local_coverage_path)
117
118        self._coverage_report_dir = getattr(
119            self, keys.ConfigKeys.IKEY_COVERAGE_REPORT_PATH, None)
120
121        self._coverage_report_file_prefix = ""
122
123        self.global_coverage = getattr(
124            self, keys.ConfigKeys.IKEY_GLOBAL_COVERAGE, True)
125        if self.enabled:
126            android_devices = getattr(self,
127                                      keys.ConfigKeys.IKEY_ANDROID_DEVICE)
128            if not isinstance(android_devices, list):
129                logging.warn("Android device information not available.")
130                self.enabled = False
131            for device in android_devices:
132                serial = device.get(keys.ConfigKeys.IKEY_SERIAL)
133                coverage_resource_path = device.get(
134                    keys.ConfigKeys.IKEY_GCOV_RESOURCES_PATH)
135                if not serial:
136                    logging.error("Missing serial information in device: %s",
137                                  device)
138                    continue
139                if not coverage_resource_path:
140                    logging.error(
141                        "Missing coverage resource path in device: %s", device)
142                    continue
143                self._device_resource_dict[str(serial)] = str(
144                    coverage_resource_path)
145
146        if self.enabled:
147            logging.info("Coverage is enabled")
148        else:
149            logging.debug("Coverage is disabled.")
150
151    def _FindGcnoSummary(self, gcda_file_path, gcno_file_parsers):
152        """Find the corresponding gcno summary for given gcda file.
153
154        Identify the corresponding gcno summary for given gcda file from a list
155        of gcno files with the same checksum as the gcda file by matching
156        the the gcda file path.
157        Note: if none of the gcno summary contains the source file same as the
158        given gcda_file_path (e.g. when the corresponding source file does not
159        contain any executable codes), just return the last gcno summary in the
160        list as a fall back solution.
161
162        Args:
163            gcda_file_path: the path of gcda file (without extensions).
164            gcno_file_parsers: a list of gcno file parser that has the same
165                               chechsum.
166
167        Returns:
168            The corresponding gcno summary for given gcda file.
169        """
170        gcno_summary = None
171        # For each gcno files with the matched checksum, compare the
172        # gcda_file_path to find the corresponding gcno summary.
173        for gcno_file_parser in gcno_file_parsers:
174            try:
175                gcno_summary = gcno_file_parser.Parse()
176            except FileFormatError:
177                logging.error("Error parsing gcno for gcda %s", gcda_file_path)
178                break
179            legacy_build = "soong/.intermediates" not in gcda_file_path
180            for key in gcno_summary.functions:
181                src_file_path = gcno_summary.functions[key].src_file_name
182                src_file_name = src_file_path.rsplit(".", 1)[0]
183                # If build with legacy compile system, compare only the base
184                # source file name. Otherwise, compare the full source file name
185                # (with path info).
186                if legacy_build:
187                    base_src_file_name = os.path.basename(src_file_name)
188                    if gcda_file_path.endswith(base_src_file_name):
189                        return gcno_summary
190                else:
191                    if gcda_file_path.endswith(src_file_name):
192                        return gcno_summary
193        # If no gcno file matched with the gcda_file_name, return the last
194        # gcno summary as a fall back solution.
195        return gcno_summary
196
197    def _GetChecksumGcnoDict(self, cov_zip):
198        """Generates a dictionary from gcno checksum to GCNOParser object.
199
200        Processes the gcnodir files in the zip file to produce a mapping from gcno
201        checksum to the GCNOParser object wrapping the gcno content.
202        Note there might be multiple gcno files corresponds to the same checksum.
203
204        Args:
205            cov_zip: the zip file containing gcnodir files from the device build
206
207        Returns:
208            the dictionary of gcno checksums to GCNOParser objects
209        """
210        checksum_gcno_dict = dict()
211        fnames = cov_zip.namelist()
212        instrumented_modules = [
213            f for f in fnames if f.endswith(COVERAGE_SUFFIX)
214        ]
215        for instrumented_module in instrumented_modules:
216            # Read the gcnodir file
217            archive = archive_parser.Archive(
218                cov_zip.open(instrumented_module).read())
219            try:
220                archive.Parse()
221            except ValueError:
222                logging.error("Archive could not be parsed: %s", name)
223                continue
224
225            for gcno_file_path in archive.files:
226                gcno_stream = io.BytesIO(archive.files[gcno_file_path])
227                gcno_file_parser = gcno_parser.GCNOParser(gcno_stream)
228                if gcno_file_parser.checksum in checksum_gcno_dict:
229                    checksum_gcno_dict[gcno_file_parser.checksum].append(
230                        gcno_file_parser)
231                else:
232                    checksum_gcno_dict[gcno_file_parser.checksum] = [
233                        gcno_file_parser
234                    ]
235        return checksum_gcno_dict
236
237    def _ClearTargetGcov(self, dut, serial, path_suffix=None):
238        """Removes gcov data from the device.
239
240        Finds and removes all gcda files relative to TARGET_COVERAGE_PATH.
241        Args:
242            dut: the device under test.
243            path_suffix: optional string path suffix.
244        """
245        path = TARGET_COVERAGE_PATH
246        if path_suffix:
247            path = path_utils.JoinTargetPath(path, path_suffix)
248        self._ExecuteOneAdbShellCommand(dut, serial, _CLEAN_TRACE_COMMAND)
249
250    def _GetHalPids(self, dut, hal_names):
251        """Get the process id for the given hal names.
252
253        Args:
254            dut: the device under test.
255            hal_names: list of strings for targeting hal names.
256
257        Returns:
258            list of strings for the corresponding pids.
259        """
260        logging.debug("hal_names: %s", str(hal_names))
261        searchString = "|".join(hal_names)
262        entries = []
263        try:
264            dut.rootAdb()
265            entries = dut.adb.shell(
266                "lshal -itp 2> /dev/null | grep -E \"{0}\"".format(
267                    searchString)).splitlines()
268        except AdbError as e:
269            logging.error("failed to get pid entries")
270
271        pids = set(pid.strip()
272                   for pid in map(lambda entry: entry.split()[-1], entries)
273                   if pid.isdigit())
274        return pids
275
276    def InitializeDeviceCoverage(self, dut=None, serial=None):
277        """Initializes the device for coverage before tests run.
278
279        Flushes, then finds and removes all gcda files under
280        TARGET_COVERAGE_PATH before tests run.
281
282        Args:
283            dut: the device under test.
284        """
285        self._ExecuteOneAdbShellCommand(dut, serial, "setenforce 0")
286        self._ExecuteOneAdbShellCommand(dut, serial, _FLUSH_COMMAND)
287        logging.debug("Removing existing gcda files.")
288        self._ClearTargetGcov(dut, serial)
289
290        # restart HALs to include coverage for initialization code.
291        if self._hal_names:
292            pids = self._GetHalPids(dut, self._hal_names)
293            for pid in pids:
294                cmd = "kill -9 " + pid
295                self._ExecuteOneAdbShellCommand(dut, serial, cmd)
296
297    def _GetGcdaDict(self, dut, serial):
298        """Retrieves GCDA files from device and creates a dictionary of files.
299
300        Find all GCDA files on the target device, copy them to the host using
301        adb, then return a dictionary mapping from the gcda basename to the
302        temp location on the host.
303
304        Args:
305            dut: the device under test.
306
307        Returns:
308            A dictionary with gcda basenames as keys and contents as the values.
309        """
310        logging.debug("Creating gcda dictionary")
311        gcda_dict = {}
312        logging.debug("Storing gcda tmp files to: %s",
313                      self.local_coverage_path)
314
315        self._ExecuteOneAdbShellCommand(dut, serial, _FLUSH_COMMAND)
316
317        gcda_files = set()
318        if self._hal_names:
319            pids = self._GetHalPids(dut, self._hal_names)
320            pids.add(_SP_COVERAGE_PATH)
321            for pid in pids:
322                path = path_utils.JoinTargetPath(TARGET_COVERAGE_PATH, pid)
323                try:
324                    files = dut.adb.shell("find %s -name \"*.gcda\"" % path)
325                    gcda_files.update(files.split("\n"))
326                except AdbError as e:
327                    logging.info("No gcda files found in path: \"%s\"", path)
328        else:
329            cmd = ("find %s -name \"*.gcda\"" % TARGET_COVERAGE_PATH)
330            result = self._ExecuteOneAdbShellCommand(dut, serial, cmd)
331            if result:
332                gcda_files.update(result.split("\n"))
333
334        for gcda in gcda_files:
335            if gcda:
336                basename = os.path.basename(gcda.strip())
337                file_name = os.path.join(self.local_coverage_path, basename)
338                if dut is None:
339                    results = cmd_utils.ExecuteShellCommand(
340                        "adb -s %s pull %s %s " % (serial, gcda, file_name))
341                    if (results[cmd_utils.EXIT_CODE][0]):
342                        logging.error(
343                            "Fail to execute command: %s. error: %s" %
344                            (cmd, str(results[cmd_utils.STDERR][0])))
345                else:
346                    dut.adb.pull("%s %s" % (gcda, file_name))
347                gcda_content = open(file_name, "rb").read()
348                gcda_dict[gcda.strip()] = gcda_content
349        self._ClearTargetGcov(dut, serial)
350        return gcda_dict
351
352    def _OutputCoverageReport(self, isGlobal, coverage_report_msg=None):
353        logging.info("Outputing coverage data")
354        timestamp_seconds = str(int(time.time() * 1000000))
355        coverage_report_file_name = "coverage_report_" + timestamp_seconds + ".txt"
356        if self._coverage_report_file_prefix:
357            coverage_report_file_name = "coverage_report_" + self._coverage_report_file_prefix + ".txt"
358
359        coverage_report_file = None
360        if (self._coverage_report_dir):
361            if not os.path.exists(self._coverage_report_dir):
362                os.makedirs(self._coverage_report_dir)
363            coverage_report_file = os.path.join(self._coverage_report_dir,
364                                                coverage_report_file_name)
365        else:
366            coverage_report_file = os.path.join(self.local_coverage_path,
367                                                coverage_report_file_name)
368
369        logging.info("Storing coverage report to: %s", coverage_report_file)
370        if self.web and self.web.enabled:
371            coverage_report_msg = ReportMsg.TestReportMessage()
372            if isGlobal:
373                for c in self.web.report_msg.coverage:
374                    coverage = coverage_report_msg.coverage.add()
375                    coverage.CopyFrom(c)
376            else:
377                for c in self.web.current_test_report_msg.coverage:
378                    coverage = coverage_report_msg.coverage.add()
379                    coverage.CopyFrom(c)
380        if coverage_report_msg is not None:
381            with open(coverage_report_file, "w+") as f:
382                f.write(str(coverage_report_msg))
383
384    def _AutoProcess(self, cov_zip, revision_dict, gcda_dict, isGlobal):
385        """Process coverage data and appends coverage reports to the report message.
386
387        Matches gcno files with gcda files and processes them into a coverage report
388        with references to the original source code used to build the system image.
389        Coverage information is appended as a CoverageReportMessage to the provided
390        report message.
391
392        Git project information is automatically extracted from the build info and
393        the source file name enclosed in each gcno file. Git project names must
394        resemble paths and may differ from the paths to their project root by at
395        most one. If no match is found, then coverage information will not be
396        be processed.
397
398        e.g. if the project path is test/vts, then its project name may be
399             test/vts or <some folder>/test/vts in order to be recognized.
400
401        Args:
402            cov_zip: the ZipFile object containing the gcno coverage artifacts.
403            revision_dict: the dictionary from project name to project version.
404            gcda_dict: the dictionary of gcda basenames to gcda content (binary string)
405            isGlobal: boolean, True if the coverage data is for the entire test, False if only for
406                      the current test case.
407        """
408        checksum_gcno_dict = self._GetChecksumGcnoDict(cov_zip)
409        output_coverage_report = getattr(
410            self, keys.ConfigKeys.IKEY_OUTPUT_COVERAGE_REPORT, False)
411        exclude_coverage_path = getattr(
412            self, keys.ConfigKeys.IKEY_EXCLUDE_COVERAGE_PATH, [])
413        for idx, path in enumerate(exclude_coverage_path):
414            base_name = os.path.basename(path)
415            if base_name and "." not in base_name:
416                path = path if path.endswith("/") else path + "/"
417                exclude_coverage_path[idx] = path
418        exclude_coverage_path.extend(self._DEFAULT_EXCLUDE_PATHS)
419
420        coverage_dict = dict()
421        coverage_report_message = ReportMsg.TestReportMessage()
422
423        for gcda_name in gcda_dict:
424            if GEN_TAG in gcda_name:
425                # skip coverage measurement for intermediate code.
426                logging.warn("Skip for gcda file: %s", gcda_name)
427                continue
428
429            gcda_stream = io.BytesIO(gcda_dict[gcda_name])
430            gcda_file_parser = gcda_parser.GCDAParser(gcda_stream)
431            file_name = gcda_name.rsplit(".", 1)[0]
432
433            if not gcda_file_parser.checksum in checksum_gcno_dict:
434                logging.info("No matching gcno file for gcda: %s", gcda_name)
435                continue
436            gcno_file_parsers = checksum_gcno_dict[gcda_file_parser.checksum]
437            gcno_summary = self._FindGcnoSummary(file_name, gcno_file_parsers)
438            if gcno_summary is None:
439                logging.error("No gcno file found for gcda %s.", gcda_name)
440                continue
441
442            # Process and merge gcno/gcda data
443            try:
444                gcda_file_parser.Parse(gcno_summary)
445            except FileFormatError:
446                logging.error("Error parsing gcda file %s", gcda_name)
447                continue
448
449            coverage_report.GenerateLineCoverageVector(
450                gcno_summary, exclude_coverage_path, coverage_dict)
451
452        for src_file_path in coverage_dict:
453            # Get the git project information
454            # Assumes that the project name and path to the project root are similar
455            revision = None
456            for project_name in revision_dict:
457                # Matches cases when source file root and project name are the same
458                if src_file_path.startswith(str(project_name)):
459                    git_project_name = str(project_name)
460                    git_project_path = str(project_name)
461                    revision = str(revision_dict[project_name])
462                    logging.debug("Source file '%s' matched with project '%s'",
463                                  src_file_path, git_project_name)
464                    break
465
466                parts = os.path.normpath(str(project_name)).split(os.sep, 1)
467                # Matches when project name has an additional prefix before the
468                # project path root.
469                if len(parts) > 1 and src_file_path.startswith(parts[-1]):
470                    git_project_name = str(project_name)
471                    git_project_path = parts[-1]
472                    revision = str(revision_dict[project_name])
473                    logging.debug("Source file '%s' matched with project '%s'",
474                                  src_file_path, git_project_name)
475                    break
476
477            if not revision:
478                logging.info("Could not find git info for %s", src_file_path)
479                continue
480
481            coverage_vec = coverage_dict[src_file_path]
482            total_count, covered_count = coverage_report.GetCoverageStats(
483                coverage_vec)
484            if self.web and self.web.enabled:
485                self.web.AddCoverageReport(coverage_vec, src_file_path,
486                                           git_project_name, git_project_path,
487                                           revision, covered_count,
488                                           total_count, isGlobal)
489            else:
490                coverage = coverage_report_message.coverage.add()
491                coverage.total_line_count = total_count
492                coverage.covered_line_count = covered_count
493                coverage.line_coverage_vector.extend(coverage_vec)
494
495                src_file_path = os.path.relpath(src_file_path,
496                                                git_project_path)
497                coverage.file_path = src_file_path
498                coverage.revision = revision
499                coverage.project_name = git_project_name
500
501        if output_coverage_report:
502            self._OutputCoverageReport(isGlobal, coverage_report_message)
503
504    # TODO: consider to deprecate the manual process.
505    def _ManualProcess(self, cov_zip, revision_dict, gcda_dict, isGlobal):
506        """Process coverage data and appends coverage reports to the report message.
507
508        Opens the gcno files in the cov_zip for the specified modules and matches
509        gcno/gcda files. Then, coverage vectors are generated for each set of matching
510        gcno/gcda files and appended as a CoverageReportMessage to the provided
511        report message. Unlike AutoProcess, coverage information is only processed
512        for the modules explicitly defined in 'modules'.
513
514        Args:
515            cov_zip: the ZipFile object containing the gcno coverage artifacts.
516            revision_dict: the dictionary from project name to project version.
517            gcda_dict: the dictionary of gcda basenames to gcda content (binary string)
518            isGlobal: boolean, True if the coverage data is for the entire test, False if only for
519                      the current test case.
520        """
521        output_coverage_report = getattr(
522            self, keys.ConfigKeys.IKEY_OUTPUT_COVERAGE_REPORT, True)
523        modules = getattr(self, keys.ConfigKeys.IKEY_MODULES, None)
524        covered_modules = set(cov_zip.namelist())
525        for module in modules:
526            if MODULE_NAME not in module or GIT_PROJECT not in module:
527                logging.error(
528                    "Coverage module must specify name and git project: %s",
529                    module)
530                continue
531            project = module[GIT_PROJECT]
532            if PATH not in project or NAME not in project:
533                logging.error("Project name and path not specified: %s",
534                              project)
535                continue
536
537            name = str(module[MODULE_NAME]) + COVERAGE_SUFFIX
538            git_project = str(project[NAME])
539            git_project_path = str(project[PATH])
540
541            if name not in covered_modules:
542                logging.error("No coverage information for module %s", name)
543                continue
544            if git_project not in revision_dict:
545                logging.error(
546                    "Git project not present in device revision dict: %s",
547                    git_project)
548                continue
549
550            revision = str(revision_dict[git_project])
551            archive = archive_parser.Archive(cov_zip.open(name).read())
552            try:
553                archive.Parse()
554            except ValueError:
555                logging.error("Archive could not be parsed: %s", name)
556                continue
557
558            for gcno_file_path in archive.files:
559                file_name_path = gcno_file_path.rsplit(".", 1)[0]
560                file_name = os.path.basename(file_name_path)
561                gcno_content = archive.files[gcno_file_path]
562                gcno_stream = io.BytesIO(gcno_content)
563                try:
564                    gcno_summary = gcno_parser.GCNOParser(gcno_stream).Parse()
565                except FileFormatError:
566                    logging.error("Error parsing gcno file %s", gcno_file_path)
567                    continue
568                src_file_path = None
569
570                # Match gcno file with gcda file
571                gcda_name = file_name + GCDA_SUFFIX
572                if gcda_name not in gcda_dict:
573                    logging.error("No gcda file found %s.", gcda_name)
574                    continue
575
576                src_file_path = self._ExtractSourceName(
577                    gcno_summary, file_name)
578
579                if not src_file_path:
580                    logging.error("No source file found for %s.",
581                                  gcno_file_path)
582                    continue
583
584                # Process and merge gcno/gcda data
585                gcda_content = gcda_dict[gcda_name]
586                gcda_stream = io.BytesIO(gcda_content)
587                try:
588                    gcda_parser.GCDAParser(gcda_stream).Parse(gcno_summary)
589                except FileFormatError:
590                    logging.error("Error parsing gcda file %s", gcda_content)
591                    continue
592
593                if self.web and self.web.enabled:
594                    coverage_vec = coverage_report.GenerateLineCoverageVector(
595                        src_file_path, gcno_summary)
596                    total_count, covered_count = coverage_report.GetCoverageStats(
597                        coverage_vec)
598                    self.web.AddCoverageReport(coverage_vec, src_file_path,
599                                               git_project, git_project_path,
600                                               revision, covered_count,
601                                               total_count, isGlobal)
602
603        if output_coverage_report:
604            self._OutputCoverageReport(isGlobal)
605
606    def SetCoverageData(self, dut=None, serial=None, isGlobal=False):
607        """Sets and processes coverage data.
608
609        Organizes coverage data and processes it into a coverage report in the
610        current test case
611
612        Requires feature to be enabled; no-op otherwise.
613
614        Args:
615            dut:  the device object for which to pull coverage data
616            isGlobal: True if the coverage data is for the entire test, False if
617                      if the coverage data is just for the current test case.
618        """
619        if not self.enabled:
620            return
621
622        if serial is None:
623            serial = "default" if dut is None else dut.adb.shell(
624                "getprop ro.serialno").strip()
625
626        if not serial in self._device_resource_dict:
627            logging.error("Invalid device provided: %s", serial)
628            return
629
630        resource_path = self._device_resource_dict[serial]
631        if not resource_path:
632            logging.error("Coverage resource path not found.")
633            return
634
635        gcda_dict = self._GetGcdaDict(dut, serial)
636        logging.debug("Coverage file paths %s", str([fp for fp in gcda_dict]))
637
638        cov_zip = zipfile.ZipFile(os.path.join(resource_path, _GCOV_ZIP))
639
640        revision_dict = json.load(
641            open(os.path.join(resource_path, _BUILD_INFO)))[_REPO_DICT]
642
643        if not hasattr(self, keys.ConfigKeys.IKEY_MODULES):
644            # auto-process coverage data
645            self._AutoProcess(cov_zip, revision_dict, gcda_dict, isGlobal)
646        else:
647            # explicitly process coverage data for the specified modules
648            self._ManualProcess(cov_zip, revision_dict, gcda_dict, isGlobal)
649
650        # cleanup the downloaded gcda files.
651        logging.debug("Cleaning up gcda files.")
652        files = os.listdir(self.local_coverage_path)
653        for item in files:
654            if item.endswith(".gcda"):
655                os.remove(os.path.join(self.local_coverage_path, item))
656
657    def SetHalNames(self, names=[]):
658        """Sets the HAL names for which to process coverage.
659
660        Args:
661            names: list of strings, names of hal (e.g. android.hardware.light@2.0)
662        """
663        self._hal_names = list(names)
664
665    def SetCoverageReportFilePrefix(self, prefix):
666        """Sets the prefix for outputting the coverage report file.
667
668        Args:
669            prefix: strings, prefix of the coverage report file.
670        """
671        self._coverage_report_file_prefix = prefix
672
673    def SetCoverageReportDirectory(self, corverage_report_dir):
674        """Sets the path for storing the coverage report file.
675
676        Args:
677            corverage_report_dir: strings, dir to store the coverage report file.
678        """
679        self._coverage_report_dir = corverage_report_dir
680
681    def _ExecuteOneAdbShellCommand(self, dut, serial, cmd):
682        """Helper method to execute a shell command and return results.
683
684        Args:
685            dut: the device under test.
686            cmd: string, command to execute.
687        Returns:
688            stdout result of the command, None if command fails.
689        """
690        if dut is None:
691            results = cmd_utils.ExecuteShellCommand("adb -s %s shell %s" %
692                                                    (serial, cmd))
693            if (results[cmd_utils.EXIT_CODE][0]):
694                logging.error("Fail to execute command: %s. error: %s" %
695                              (cmd, str(results[cmd_utils.STDERR][0])))
696                return None
697            else:
698                return results[cmd_utils.STDOUT][0]
699        else:
700            try:
701                return dut.adb.shell(cmd)
702            except AdbError as e:
703                logging.warn("Fail to execute command: %s. error: %s" %
704                             (cmd, str(e)))
705                return None
706
707
708if __name__ == '__main__':
709    """ Tools to process coverage data.
710
711    Usage:
712      python coverage_utils.py operation [--serial=device_serial_number]
713      [--report_prefix=prefix_of_coverage_report]
714
715    Example:
716      python coverage_utils.py init_coverage
717      python coverage_utils.py get_coverage --serial HT7821A00243
718      python coverage_utils.py get_coverage --serial HT7821A00243 --report_prefix=test
719    """
720    logging.basicConfig(level=logging.INFO)
721    parser = argparse.ArgumentParser(description="Coverage process tool.")
722    parser.add_argument(
723        "--report_prefix",
724        dest="report_prefix",
725        required=False,
726        help="Prefix of the coverage report.")
727    parser.add_argument(
728        "--report_path",
729        dest="report_path",
730        required=False,
731        help="directory to store the coverage reports.")
732    parser.add_argument(
733        "--serial", dest="serial", required=True, help="Device serial number.")
734    parser.add_argument(
735        "--gcov_rescource_path",
736        dest="gcov_rescource_path",
737        required=True,
738        help="Directory that stores gcov resource files.")
739    parser.add_argument(
740        "operation",
741        help=
742        "Operation for processing coverage data, e.g. 'init_coverage', get_coverage'"
743    )
744    args = parser.parse_args()
745
746    if args.operation != "init_coverage" and args.operation != "get_coverage":
747        print "Unsupported operation. Exiting..."
748        sys.exit(1)
749    user_params = {
750        keys.ConfigKeys.IKEY_ENABLE_COVERAGE:
751        True,
752        keys.ConfigKeys.IKEY_ANDROID_DEVICE: [{
753            keys.ConfigKeys.IKEY_SERIAL:
754            args.serial,
755            keys.ConfigKeys.IKEY_GCOV_RESOURCES_PATH:
756            args.gcov_rescource_path,
757        }],
758        keys.ConfigKeys.IKEY_OUTPUT_COVERAGE_REPORT:
759        True,
760        keys.ConfigKeys.IKEY_GLOBAL_COVERAGE:
761        True
762    }
763    coverage = CoverageFeature(user_params)
764    if args.operation == "init_coverage":
765        coverage.InitializeDeviceCoverage(serial=args.serial)
766    elif args.operation == "get_coverage":
767        if args.report_prefix:
768            coverage.SetCoverageReportFilePrefix(args.report_prefix)
769        if args.report_path:
770            coverage.SetCoverageReportDirectory(args.report_path)
771        coverage.SetCoverageData(serial=args.serial, isGlobal=True)
772