1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7import re
8import shutil
9import tempfile
10import xml.etree.ElementTree as ET
11
12import common
13from autotest_lib.client.bin import test, utils
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib import file_utils
16from autotest_lib.client.cros import constants
17
18
19class ChromeBinaryTest(test.test):
20    """
21    Base class for tests to run chrome test binaries without signing in and
22    running Chrome.
23    """
24
25    CHROME_TEST_DEP = 'chrome_test'
26    CHROME_SANDBOX = '/opt/google/chrome/chrome-sandbox'
27    COMPONENT_LIB = '/opt/google/chrome/lib'
28    home_dir = None
29    cr_source_dir = None
30    test_binary_dir = None
31
32    def setup(self):
33        """
34        Sets up a test.
35        """
36        self.job.setup_dep([self.CHROME_TEST_DEP])
37
38    def initialize(self):
39        """
40        Initializes members after setup().
41        """
42        test_dep_dir = os.path.join(self.autodir, 'deps', self.CHROME_TEST_DEP)
43        self.job.install_pkg(self.CHROME_TEST_DEP, 'dep', test_dep_dir)
44
45        self.cr_source_dir = '%s/test_src' % test_dep_dir
46        self.test_binary_dir = '%s/out/Release' % self.cr_source_dir
47        # If chrome is a component build then need to create a symlink such
48        # that the _unittest binaries can find the chrome component libraries.
49        Release_lib = os.path.join(self.test_binary_dir, 'lib')
50        if os.path.isdir(self.COMPONENT_LIB):
51            logging.info('Detected component build. This assumes binary '
52                         'compatibility between chrome and *unittest.')
53            if not os.path.islink(Release_lib):
54                os.symlink(self.COMPONENT_LIB, Release_lib)
55        self.home_dir = tempfile.mkdtemp()
56
57    def cleanup(self):
58        """
59        Cleans up working directory after run.
60        """
61        if self.home_dir:
62            shutil.rmtree(self.home_dir, ignore_errors=True)
63
64    def get_chrome_binary_path(self, binary_to_run):
65        """
66        Gets test binary's full path.
67
68        @returns full path of the test binary to run.
69        """
70        return os.path.join(self.test_binary_dir, binary_to_run)
71
72    def parse_fail_reason(self, err, gtest_xml):
73        """
74        Parses reason of failure from CmdError and gtest result.
75
76        @param err: CmdError raised from utils.system().
77        @param gtest_xml: filename of gtest result xml.
78        @returns reason string
79        """
80        reasons = {}
81
82        # Parse gtest result.
83        if os.path.exists(gtest_xml):
84            tree = ET.parse(gtest_xml)
85            root = tree.getroot()
86            for suite in root.findall('testsuite'):
87                for case in suite.findall('testcase'):
88                    failure = case.find('failure')
89                    if failure is None:
90                        continue
91                    testname = '%s.%s' % (suite.get('name'), case.get('name'))
92                    reasons[testname] = failure.attrib['message']
93
94        # Parse messages from chrome's test_launcher.
95        # This provides some information not available from gtest, like timeout.
96        for line in err.result_obj.stdout.splitlines():
97            m = re.match(r'\[\d+/\d+\] (\S+) \(([A-Z ]+)\)$', line)
98            if not m:
99                continue
100            testname, reason = m.group(1, 2)
101            # Existing reason from gtest has more detail, don't overwrite.
102            if testname not in reasons:
103                reasons[testname] = reason
104
105        if reasons:
106            message = '%d failures' % len(reasons)
107            for testname, reason in sorted(reasons.items()):
108                message += '; <%s>: %s' % (testname, reason.replace('\n', '; '))
109            return message
110
111        return 'Unable to parse fail reason: ' + str(err)
112
113    def run_chrome_test_binary(self,
114                               binary_to_run,
115                               extra_params='',
116                               prefix='',
117                               as_chronos=True,
118                               timeout=None):
119        """
120        Runs chrome test binary.
121
122        @param binary_to_run: The name of the browser test binary.
123        @param extra_params: Arguments for the browser test binary.
124        @param prefix: Prefix to the command that invokes the test binary.
125        @param as_chronos: Boolean indicating if the tests should run in a
126            chronos shell.
127        @param timeout: timeout in seconds
128
129        @raises: error.TestFail if there is error running the command.
130        @raises: CmdTimeoutError: the command timed out and |timeout| is
131            specified and not None.
132        """
133        gtest_xml = tempfile.mktemp(prefix='gtest_xml', suffix='.xml')
134        binary_path = self.get_chrome_binary_path(binary_to_run)
135        env_vars = ' '.join([
136            'HOME=' + self.home_dir,
137            'CR_SOURCE_ROOT=' + self.cr_source_dir,
138            'CHROME_DEVEL_SANDBOX=' + self.CHROME_SANDBOX,
139            'GTEST_OUTPUT=xml:' + gtest_xml,
140            ])
141        cmd = ' '.join([env_vars, prefix, binary_path, extra_params])
142
143        try:
144            if as_chronos:
145                utils.system("su chronos -c '%s'" % cmd,
146                             timeout=timeout)
147            else:
148                utils.system(cmd, timeout=timeout)
149        except error.CmdError as e:
150            return_code = e.result_obj.exit_status
151            if return_code == 126:
152                path_permission = '; '.join(
153                    file_utils.recursive_path_permission(binary_path))
154                fail_reason = ('Cannot execute command %s. Permissions: %s' %
155                               (binary_path, path_permission))
156            elif return_code == 127:
157                fail_reason = ('Command not found: %s' % binary_path)
158            else:
159                fail_reason = self.parse_fail_reason(e, gtest_xml)
160
161            raise error.TestFail(fail_reason)
162
163
164def nuke_chrome(func):
165    """
166    Decorator to nuke the Chrome browser processes.
167    """
168
169    def wrapper(*args, **kargs):
170        """
171        Nukes Chrome browser processes before invoking func().
172
173        Also, restarts Chrome after func() returns.
174        """
175        open(constants.DISABLE_BROWSER_RESTART_MAGIC_FILE, 'w').close()
176        try:
177            try:
178                utils.nuke_process_by_name(name=constants.BROWSER,
179                                           with_prejudice=True)
180            except error.AutoservPidAlreadyDeadError:
181                pass
182            return func(*args, **kargs)
183        finally:
184            # Allow chrome to be restarted again later.
185            os.unlink(constants.DISABLE_BROWSER_RESTART_MAGIC_FILE)
186
187    return wrapper
188