1#!/usr/bin/python -u
2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Check an autotest control file for required variables.
8
9This wrapper is invoked through autotest's PRESUBMIT.cfg for every commit
10that edits a control file.
11"""
12
13
14import glob, os, re, subprocess
15import common
16from autotest_lib.client.common_lib import control_data
17from autotest_lib.server.cros.dynamic_suite import reporting_utils
18
19
20SUITES_NEED_RETRY = set(['bvt-cq', 'bvt-inline'])
21
22
23class ControlFileCheckerError(Exception):
24    """Raised when a necessary condition of this checker isn't satisfied."""
25
26
27def IsInChroot():
28    """Return boolean indicating if we are running in the chroot."""
29    return os.path.exists("/etc/debian_chroot")
30
31
32def CommandPrefix():
33    """Return an argv list which must appear at the start of shell commands."""
34    if IsInChroot():
35        return []
36    else:
37        return ['cros_sdk', '--']
38
39
40def GetOverlayPath():
41    """Return the path to the chromiumos-overlay directory."""
42    ourpath = os.path.abspath(__file__)
43    overlay = os.path.join(os.path.dirname(ourpath),
44                           "../../../../chromiumos-overlay/")
45    return os.path.normpath(overlay)
46
47
48def GetAutotestTestPackages():
49    """Return a list of ebuilds which should be checked for test existance."""
50    overlay = GetOverlayPath()
51    packages = glob.glob(os.path.join(overlay, "chromeos-base/autotest-*"))
52    # Return the packages list with the leading overlay path removed.
53    return [x[(len(overlay) + 1):] for x in packages]
54
55
56def GetEqueryWrappers():
57    """Return a list of all the equery variants that should be consulted."""
58    # Note that we can't just glob.glob('/usr/local/bin/equery-*'), because
59    # we might be running outside the chroot.
60    pattern = '/usr/local/bin/equery-*'
61    cmd = CommandPrefix() + ['sh', '-c', 'echo %s' % pattern]
62    wrappers = subprocess.check_output(cmd).split()
63    # If there was no match, we get the literal pattern string echoed back.
64    if wrappers and wrappers[0] == pattern:
65        wrappers = []
66    return ['equery'] + wrappers
67
68
69def GetUseFlags():
70    """Get the set of all use flags from autotest packages."""
71    useflags = set()
72    for equery in GetEqueryWrappers():
73        cmd_args = (CommandPrefix() + [equery, '-qC', 'uses'] +
74                    GetAutotestTestPackages())
75        child = subprocess.Popen(cmd_args, stdout=subprocess.PIPE,
76                                 stderr=subprocess.PIPE)
77        new_useflags = child.communicate()[0].splitlines()
78        if child.returncode == 0:
79            useflags = useflags.union(new_useflags)
80    return useflags
81
82
83def CheckSuites(ctrl_data, test_name, useflags):
84    """
85    Check that any test in a SUITE is also in an ebuild.
86
87    Throws a ControlFileCheckerError if a test within a SUITE
88    does not appear in an ebuild. For purposes of this check,
89    the psuedo-suite "manual" does not require a test to be
90    in an ebuild.
91
92    @param ctrl_data: The control_data object for a test.
93    @param test_name: A string with the name of the test.
94    @param useflags: Set of all use flags from autotest packages.
95
96    @returns: None
97    """
98    if (hasattr(ctrl_data, 'suite') and ctrl_data.suite and
99        ctrl_data.suite != 'manual'):
100        # To handle the case where a developer has cros_workon'd
101        # e.g. autotest-tests on one particular board, and has the
102        # test listed only in the -9999 ebuild, we have to query all
103        # the equery-* board-wrappers until we find one. We ALSO have
104        # to check plain 'equery', to handle the case where e.g. a
105        # developer who has never run setup_board, and has no
106        # wrappers, is making a quick edit to some existing control
107        # file already enabled in the stable ebuild.
108        for flag in useflags:
109            if flag.startswith('-') or flag.startswith('+'):
110                flag = flag[1:]
111            if flag == 'tests_%s' % test_name:
112                return
113        raise ControlFileCheckerError(
114                'No ebuild entry for %s. To fix, please do the following: 1. '
115                'Add your new test to one of the ebuilds referenced by '
116                'autotest-all. 2. cros_workon start --board=<board> '
117                '<your_ebuild>. 3. emerge-<board> <your_ebuild>' % test_name)
118
119
120def CheckSuitesAttrMatch(ctrl_data, whitelist, test_name):
121    """
122    Check whether ATTRIBUTES match to SUITE and also in the whitelist.
123
124    Throw a ControlFileCheckerError if suite tags in ATTRIBUTES doesn't match to
125    SUITE. This check is needed until SUITE is eliminated from control files.
126
127    @param ctrl_data: The control_data object for a test.
128    @param whitelist: whitelist set parsed from the attribute_whitelist file.
129    @param test_name: A string with the name of the test.
130
131    @returns: None
132    """
133    # unmatch case 1: attributes not in the whitelist.
134    if not (whitelist >= ctrl_data.attributes):
135        attribute_diff = ctrl_data.attributes - whitelist
136        raise ControlFileCheckerError(
137            'Attribute(s): %s not in the whitelist in control file for test'
138            'named %s.' % (attribute_diff, test_name))
139    suite_in_attr = set(
140            [a for a in ctrl_data.attributes if a.startswith('suite:')])
141    # unmatch case 2: ctrl_data has suite, but not match to attributes.
142    if hasattr(ctrl_data, 'suite'):
143        target_attrs = set(
144            'suite:' + x.strip() for x in ctrl_data.suite.split(',')
145            if x.strip())
146        if target_attrs != suite_in_attr:
147            raise ControlFileCheckerError(
148                'suite tags in ATTRIBUTES : %s does not match to SUITE : %s in '
149                'the control file for %s.' % (suite_in_attr, ctrl_data.suite,
150                                              test_name))
151    # unmatch case 3: ctrl_data doesn't have suite, suite_in_attr is not empty.
152    elif suite_in_attr:
153        raise ControlFileCheckerError(
154            'SUITE does not exist in the control file %s, ATTRIBUTES = %s'
155            'should not have suite tags.' % (test_name, ctrl_data.attributes))
156
157
158def CheckRetry(ctrl_data, test_name):
159    """
160    Check that any test in SUITES_NEED_RETRY has turned on retry.
161
162    @param ctrl_data: The control_data object for a test.
163    @param test_name: A string with the name of the test.
164
165    @raises: ControlFileCheckerError if check fails.
166    """
167    if hasattr(ctrl_data, 'suite') and ctrl_data.suite:
168        suites = set(x.strip() for x in ctrl_data.suite.split(',') if x.strip())
169        if ctrl_data.job_retries < 2 and SUITES_NEED_RETRY.intersection(suites):
170            raise ControlFileCheckerError(
171                'Setting JOB_RETRIES to 2 or greater for test in '
172                'bvt-cq or bvt-inline is recommended. Please '
173                'set it in the control file for %s.' % test_name)
174
175
176def main():
177    """
178    Checks if all control files that are a part of this commit conform to the
179    ChromeOS autotest guidelines.
180    """
181    file_list = os.environ.get('PRESUBMIT_FILES')
182    if file_list is None:
183        raise ControlFileCheckerError('Expected a list of presubmit files in '
184            'the PRESUBMIT_FILES environment variable.')
185
186    # Parse the whitelist set from file, hardcode the filepath to the whitelist.
187    path_whitelist = os.path.join(common.autotest_dir,
188                                  'site_utils/attribute_whitelist.txt')
189    with open(path_whitelist, 'r') as f:
190        whitelist = {line.strip() for line in f.readlines() if line.strip()}
191
192    # Delay getting the useflags. The call takes long time, so init useflags
193    # only when needed, i.e., the script needs to check any control file.
194    useflags = None
195    for file_path in file_list.split('\n'):
196        control_file = re.search(r'.*/control(?:\.\w+)?$', file_path)
197        if control_file:
198            ctrl_data = control_data.parse_control(control_file.group(0),
199                                                   raise_warnings=True)
200            test_name = os.path.basename(os.path.split(file_path)[0])
201            try:
202                reporting_utils.BugTemplate.validate_bug_template(
203                        ctrl_data.bug_template)
204            except AttributeError:
205                # The control file may not have bug template defined.
206                pass
207
208            if not useflags:
209                useflags = GetUseFlags()
210            CheckSuites(ctrl_data, test_name, useflags)
211            CheckSuitesAttrMatch(ctrl_data, whitelist, test_name)
212            CheckRetry(ctrl_data, test_name)
213
214
215if __name__ == '__main__':
216    main()
217