xref: /aosp_15_r20/external/autotest/site_utils/presubmit_hooks/check_control_files.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1#!/usr/bin/python3 -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 argparse
15import fnmatch
16import glob
17import os
18import re
19import subprocess
20
21import common
22from autotest_lib.client.common_lib import control_data
23from autotest_lib.server.cros.dynamic_suite import reporting_utils
24
25
26DEPENDENCY_ARC = 'arc'
27SUITES_NEED_RETRY = set(['bvt-arc', 'bvt-cq', 'bvt-inline'])
28TESTS_NEED_ARC = 'cheets_'
29BVT_ATTRS = set(
30    ['suite:smoke', 'suite:bvt-inline', 'suite:bvt-cq', 'suite:bvt-arc'])
31TAST_PSA_URL = (
32    'https://groups.google.com/a/chromium.org/d/topic/chromium-os-dev'
33    '/zH1nO7OjJ2M/discussion')
34
35
36class ControlFileCheckerError(Exception):
37    """Raised when a necessary condition of this checker isn't satisfied."""
38
39
40def IsInChroot():
41    """Return boolean indicating if we are running in the chroot."""
42    return os.path.exists("/etc/debian_chroot")
43
44
45def CommandPrefix():
46    """Return an argv list which must appear at the start of shell commands."""
47    if IsInChroot():
48        return []
49    else:
50        return ['cros_sdk', '--']
51
52
53def GetOverlayPath(overlay=None):
54    """
55    Return the path to the overlay directory.
56
57    If the overlay path is not given, the default chromiumos-overlay path
58    will be returned instead.
59
60    @param overlay: The overlay repository path for autotest ebuilds.
61
62    @return normalized absolutized path of the overlay repository.
63    """
64    if not overlay:
65        ourpath = os.path.abspath(__file__)
66        overlay = os.path.join(os.path.dirname(ourpath),
67                               "../../../../chromiumos-overlay/")
68    return os.path.normpath(overlay)
69
70
71def GetAutotestTestPackages(overlay=None):
72    """
73    Return a list of ebuilds which should be checked for test existance.
74
75    @param overlay: The overlay repository path for autotest ebuilds.
76
77    @return autotest packages in overlay repository.
78    """
79    overlay = GetOverlayPath(overlay)
80    packages = glob.glob(os.path.join(overlay, "chromeos-base/autotest-*"))
81    # Return the packages list with the leading overlay path removed.
82    return [x[(len(overlay) + 1):] for x in packages]
83
84
85def GetEqueryWrappers():
86    """Return a list of all the equery variants that should be consulted."""
87    # Note that we can't just glob.glob('/usr/local/bin/equery-*'), because
88    # we might be running outside the chroot.
89    pattern = '/usr/local/bin/equery-*'
90    cmd = CommandPrefix() + ['sh', '-c', 'echo %s' % pattern]
91    wrappers = subprocess.check_output(cmd).split()
92    # If there was no match, we get the literal pattern string echoed back.
93    if wrappers and wrappers[0] == pattern:
94        wrappers = []
95    return ['equery'] + wrappers
96
97
98def GetUseFlags(overlay=None):
99    """Get the set of all use flags from autotest packages.
100
101    @param overlay: The overlay repository path for autotest ebuilds.
102
103    @returns: useflags
104    """
105    useflags = set()
106    for equery in GetEqueryWrappers():
107        cmd_args = (CommandPrefix() + [equery, '-qC', 'uses'] +
108                    GetAutotestTestPackages(overlay))
109        child = subprocess.Popen(cmd_args, stdout=subprocess.PIPE,
110                                 stderr=subprocess.PIPE)
111        # [bytes] ==> [str]
112        new_useflags = [
113                c.decode() if isinstance(c, bytes) else c
114                for c in child.communicate()[0].splitlines()
115        ]
116        if child.returncode == 0:
117            useflags = useflags.union(new_useflags)
118    return useflags
119
120
121def CheckSuites(ctrl_data, test_name, useflags):
122    """
123    Check that any test in a SUITE is also in an ebuild.
124
125    Throws a ControlFileCheckerError if a test within a SUITE
126    does not appear in an ebuild. For purposes of this check,
127    the psuedo-suite "manual" does not require a test to be
128    in an ebuild.
129
130    @param ctrl_data: The control_data object for a test.
131    @param test_name: A string with the name of the test.
132    @param useflags: Set of all use flags from autotest packages.
133
134    @returns: None
135    """
136    if (hasattr(ctrl_data, 'suite') and ctrl_data.suite and
137        ctrl_data.suite != 'manual'):
138        # To handle the case where a developer has cros_workon'd
139        # e.g. autotest-tests on one particular board, and has the
140        # test listed only in the -9999 ebuild, we have to query all
141        # the equery-* board-wrappers until we find one. We ALSO have
142        # to check plain 'equery', to handle the case where e.g. a
143        # developer who has never run setup_board, and has no
144        # wrappers, is making a quick edit to some existing control
145        # file already enabled in the stable ebuild.
146        for flag in useflags:
147            if flag.startswith('-') or flag.startswith('+'):
148                flag = flag[1:]
149            if flag == 'tests_%s' % test_name:
150                return
151        raise ControlFileCheckerError(
152                'No ebuild entry for %s. To fix, please do the following: 1. '
153                'Add your new test to one of the ebuilds referenced by '
154                'autotest-all. 2. cros_workon --board=<board> start '
155                '<your_ebuild>. 3. emerge-<board> <your_ebuild>' % test_name)
156
157
158def CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name):
159    """
160    Check whether ATTRIBUTES are in the allowlist.
161
162    Throw a ControlFileCheckerError if tags in ATTRIBUTES don't exist in the
163    allowlist.
164
165    @param ctrl_data: The control_data object for a test.
166    @param attr_allowlist: allowlist set parsed from the attribute_allowlist.
167    @param bvt_allowlist: allowlist set parsed from the bvt_allowlist.
168    @param test_name: A string with the name of the test.
169
170    @returns: None
171    """
172    if not (attr_allowlist >= ctrl_data.attributes):
173        attribute_diff = ctrl_data.attributes - attr_allowlist
174        raise ControlFileCheckerError(
175                'Attribute(s): %s not in the allowlist in control file for test '
176                'named %s. If this is a new attribute, please add it into '
177                'AUTOTEST_DIR/site_utils/attribute_allowlist.txt file' %
178                (attribute_diff, test_name))
179    if ctrl_data.attributes & BVT_ATTRS:
180        for pattern in bvt_allowlist:
181            if fnmatch.fnmatch(test_name, pattern):
182                break
183        else:
184            raise ControlFileCheckerError(
185                    '%s not in the BVT allowlist. New BVT tests should be written '
186                    'in Tast, not in Autotest. See: %s' %
187                    (test_name, TAST_PSA_URL))
188
189
190def CheckSuiteLineRemoved(ctrl_file_path):
191    """
192    Check whether the SUITE line has been removed since it is obsolete.
193
194    @param ctrl_file_path: The path to the control file.
195
196    @raises: ControlFileCheckerError if check fails.
197    """
198    with open(ctrl_file_path, 'r') as f:
199        for line in f.readlines():
200            if line.startswith('SUITE'):
201                raise ControlFileCheckerError(
202                    'SUITE is an obsolete variable, please remove it from %s. '
203                    'Instead, add suite:<your_suite> to the ATTRIBUTES field.'
204                    % ctrl_file_path)
205
206
207def CheckRetry(ctrl_data, test_name):
208    """
209    Check that any test in SUITES_NEED_RETRY has turned on retry.
210
211    @param ctrl_data: The control_data object for a test.
212    @param test_name: A string with the name of the test.
213
214    @raises: ControlFileCheckerError if check fails.
215    """
216    if hasattr(ctrl_data, 'suite') and ctrl_data.suite:
217        suites = set(x.strip() for x in ctrl_data.suite.split(',') if x.strip())
218        if ctrl_data.job_retries < 2 and SUITES_NEED_RETRY.intersection(suites):
219            raise ControlFileCheckerError(
220                'Setting JOB_RETRIES to 2 or greater for test in '
221                '%s is recommended. Please set it in the control '
222                'file for %s.' % (' or '.join(SUITES_NEED_RETRY), test_name))
223
224
225def CheckDependencies(ctrl_data, test_name):
226    """
227    Check if any dependencies of a test is required
228
229    @param ctrl_data: The control_data object for a test.
230    @param test_name: A string with the name of the test.
231
232    @raises: ControlFileCheckerError if check fails.
233    """
234    if test_name.startswith(TESTS_NEED_ARC):
235        if not DEPENDENCY_ARC in ctrl_data.dependencies:
236            raise ControlFileCheckerError(
237                    'DEPENDENCIES = \'arc\' for %s is needed' % test_name)
238
239
240def main():
241    """
242    Checks if all control files that are a part of this commit conform to the
243    ChromeOS autotest guidelines.
244    """
245    parser = argparse.ArgumentParser(description='Process overlay arguments.')
246    parser.add_argument('--overlay', default=None, help='the overlay directory path')
247    args = parser.parse_args()
248    file_list = os.environ.get('PRESUBMIT_FILES')
249    if file_list is None:
250        raise ControlFileCheckerError('Expected a list of presubmit files in '
251            'the PRESUBMIT_FILES environment variable.')
252
253    # Parse the allowlist set from file, hardcode the filepath to the allowlist.
254    path_attr_allowlist = os.path.join(common.autotest_dir,
255                                       'site_utils/attribute_allowlist.txt')
256    with open(path_attr_allowlist, 'r') as f:
257        attr_allowlist = {
258                line.strip()
259                for line in f.readlines() if line.strip()
260        }
261
262    path_bvt_allowlist = os.path.join(common.autotest_dir,
263                                      'site_utils/bvt_allowlist.txt')
264    with open(path_bvt_allowlist, 'r') as f:
265        bvt_allowlist = {
266                line.strip()
267                for line in f.readlines() if line.strip()
268        }
269
270    # Delay getting the useflags. The call takes long time, so init useflags
271    # only when needed, i.e., the script needs to check any control file.
272    useflags = None
273    for file_path in file_list.split('\n'):
274        control_file = re.search(r'.*/control(?:\..+)?$', file_path)
275        if control_file:
276            ctrl_file_path = control_file.group(0)
277            CheckSuiteLineRemoved(ctrl_file_path)
278            ctrl_data = control_data.parse_control(ctrl_file_path,
279                                                   raise_warnings=True)
280            test_name = os.path.basename(os.path.split(file_path)[0])
281            try:
282                reporting_utils.BugTemplate.validate_bug_template(
283                        ctrl_data.bug_template)
284            except AttributeError:
285                # The control file may not have bug template defined.
286                pass
287
288            if not useflags:
289                useflags = GetUseFlags(args.overlay)
290            CheckSuites(ctrl_data, test_name, useflags)
291            CheckValidAttr(ctrl_data, attr_allowlist, bvt_allowlist, test_name)
292            CheckRetry(ctrl_data, test_name)
293            CheckDependencies(ctrl_data, test_name)
294
295if __name__ == '__main__':
296    main()
297