xref: /aosp_15_r20/prebuilts/asuite/release_checker.py (revision b3adc34a6be591bf5fd4943dfdce52f3982696f0)
1#!/usr/bin/env python3
2# Copyright 2019, 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
17import argparse
18import os
19import shlex
20import subprocess
21import sys
22
23
24PREBUILTS_DIR = os.path.dirname(__file__)
25PROJECTS = {'acloud', 'aidegen', 'atest'}
26ARCHS = {'linux-x86', 'darwin-x86'}
27SMOKE_TEST = 'smoke_tests'
28EXIT_TEST_PASS = 0
29EXIT_TEST_FAIL = 1
30EXIT_INVALID_BINS = 2
31
32
33def _get_prebuilt_bins():
34    """Get asuite prebuilt binaries.
35
36    Returns:
37        A set of prebuilt binaries.
38    """
39    bins = {os.path.join(prj, arch, prj) for prj in PROJECTS for arch in ARCHS}
40    bins.add('atest/linux-x86/atest-py3')
41    return bins
42
43
44def _get_prebuilt_dirs():
45    """Get asuite prebuilt directories.
46
47    Returns:
48        A set of prebuilt paths of binaries.
49    """
50    return {os.path.dirname(bin) for bin in _get_prebuilt_bins()}
51
52
53def _get_smoke_tests_bins():
54    """Get asuite smoke test scripts.
55
56    Returns:
57        A dict of project and smoke test script paths.
58    """
59    return {prj: os.path.join(prj, SMOKE_TEST) for prj in PROJECTS}
60
61
62def _is_executable(bin_path):
63    """Check if the given file is executable.
64
65    Args:
66        bin_path: a string of a file path.
67
68    Returns:
69        True if it is executable, false otherwise.
70    """
71    return os.access(bin_path, os.X_OK)
72
73
74def check_uploaded_bins(preupload_files):
75    """This method validates the uploaded files.
76
77    If the uploaded file is in prebuilt_bins, ensure:
78        - it is executable.
79        - only one at a time.
80    If the uploaded file is a smoke_test script, ensure:
81        - it is executable.
82    If the uploaded file is placed in prebuilt_dirs, ensure:
83        - it is not executable.
84        (It is to ensure PATH is not contaminated.
85         e.g. atest/linux-x86/atest-dev will override $OUT/bin/atest-dev, or
86         atest/linux-x86/rm does fraud/harmful things.)
87
88    Args:
89        preupload_files: A list of preuploaded files.
90
91    Returns:
92        True is the above criteria are all fulfilled, otherwise None.
93    """
94    prebuilt_bins = _get_prebuilt_bins()
95    prebuilt_dirs = _get_prebuilt_dirs()
96    smoke_tests_bins = _get_smoke_tests_bins().values()
97    # Store valid executables.
98    target_bins = set()
99    # Unexpected executable files which may cause issues(they are in $PATH).
100    illegal_bins = set()
101    # Store prebuilts or smoke test script that are inexecutable.
102    insufficient_perm_bins = set()
103    for f in preupload_files:
104        # Ensure target_bins are executable.
105        if f in prebuilt_bins:
106            if _is_executable(f):
107                target_bins.add(f)
108            else:
109                insufficient_perm_bins.add(f)
110        # Ensure smoke_tests scripts are executable.
111        elif f in smoke_tests_bins and not _is_executable(f):
112            insufficient_perm_bins.add(f)
113        # Avoid fraud commands in $PATH. e.g. atest/linux-x86/rm.
114        # must not be executable.
115        elif os.path.dirname(f) in prebuilt_dirs and _is_executable(f):
116            illegal_bins.add(f)
117    if len(target_bins) > 1:
118        print('\nYou\'re uploading multiple binaries: %s'
119              % ' '.join(target_bins))
120        print('\nPlease upload one prebuilt at a time.')
121        return False
122    if insufficient_perm_bins:
123        print('\nInsufficient permission found: %s'
124              % ' '.join(insufficient_perm_bins))
125        print('\nPlease run:\n\tchmod 0755 %s\nand try again.'
126              % ' '.join(insufficient_perm_bins))
127        return False
128    if illegal_bins:
129        illegal_dirs = {os.path.dirname(bin) for bin in illegal_bins}
130        print('\nIt is forbidden to upload executable file: %s'
131              % '\n - %s\n' % '\n - '.join(illegal_bins))
132        print('Because they are in the project paths: %s'
133              % '\n - %s\n' % '\n - '.join(illegal_dirs))
134        print('Please remove the binaries or make the files non-executable.')
135        return False
136    return True
137
138
139def run_smoke_tests_pass(files_to_check):
140    """Run smoke tests.
141
142    Args:
143        files_to_check: A list of preuploaded files to check.
144
145    Returns:
146        True when test passed or no need to test.
147        False when test failed.
148    """
149    for target in files_to_check:
150        if target in _get_prebuilt_bins():
151            project = target.split(os.path.sep)[0]
152            test_file = _get_smoke_tests_bins().get(project)
153            if os.path.exists(test_file):
154                try:
155                    print(f'Running test binary {test_file} which may take'
156                          ' several minutes...')
157                    subprocess.check_output(test_file, encoding='utf-8',
158                                            stderr=subprocess.STDOUT)
159                except subprocess.CalledProcessError as error:
160                    print('Smoke tests failed at:\n\n%s' % error.output)
161                    return False
162                except OSError as oserror:
163                    print('%s: Missing the header of the script.' % oserror)
164                    print('Please define shebang like:\n')
165                    print('#!/usr/bin/env bash\nor')
166                    print('#!/usr/bin/env python3\n')
167                    return False
168    return True
169
170
171if __name__ == '__main__':
172    parser = argparse.ArgumentParser()
173    parser.add_argument('--skip-smoke-test', '-s', action="store_true",
174                        help='Disable smoke testing.')
175    parser.add_argument('preupload_files', nargs='*', help='Files to be uploaded.')
176    args = parser.parse_args()
177    files_to_check = args.preupload_files
178    # Pre-process files_to_check(run directly by users.)
179    if not files_to_check:
180        # Only consider added(A), renamed(R) and modified(M) files.
181        cmd = "git status --short | egrep ^[ARM] | awk '{print $NF}'"
182        preupload_files = subprocess.check_output(cmd, shell=True,
183                                                 encoding='utf-8').splitlines()
184        if preupload_files:
185            print('validating: %s' % preupload_files)
186            files_to_check = preupload_files
187    # Validating uploaded files and run smoke test script(run by repohook).
188    if not check_uploaded_bins(files_to_check):
189        sys.exit(EXIT_INVALID_BINS)
190    if not args.skip_smoke_test and not run_smoke_tests_pass(files_to_check):
191        sys.exit(EXIT_TEST_FAIL)
192    sys.exit(EXIT_TEST_PASS)
193