xref: /aosp_15_r20/platform_testing/tests/automotive/preupload_hook_script.py (revision dd0948b35e70be4c0246aabd6c72554a5eb8b22a)
1#!/usr/bin/env python3
2# Copyright 2024, 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"""Preupload hook to perform necessary checks and formatting on python files."""
17
18import argparse
19import concurrent.futures
20import multiprocessing
21import pathlib
22import shlex
23import subprocess
24import sys
25
26
27def _filter_python_files(files: list[pathlib.Path]) -> list[pathlib.Path]:
28  """Filter a list of files and return a new list of python files only."""
29  return [file for file in files if file.suffix == '.py']
30
31
32def _check_run_shell_command(cmd: str, cwd: str = None) -> None:
33  """Run a shell command and raise error if failed."""
34  if subprocess.run(shlex.split(cmd), cwd=cwd, check=False).returncode:
35    print('Preupload files did not pass Asuite preupload hook script.')
36    sys.exit(1)
37
38
39def _run_python_lint(lint_bin: str, files: list[pathlib.Path]) -> None:
40  """Run python lint binary on python files."""
41  run_lint_on_file = lambda file: subprocess.run(
42      shlex.split(f'{lint_bin} {file.as_posix()}'),
43      check=False,
44      capture_output=True,
45  )
46
47  cpu_count = multiprocessing.cpu_count()
48  with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count) as executor:
49    completed_processes = executor.map(
50        run_lint_on_file, _filter_python_files(files)
51    )
52
53  has_format_issue = False
54  for process in completed_processes:
55    if not process.returncode:
56      continue
57    print(process.stdout.decode())
58    has_format_issue = True
59
60  if has_format_issue:
61    sys.exit(1)
62
63
64def _run_pylint(files: list[pathlib.Path]) -> None:
65  """Run pylint on python files."""
66  _run_python_lint('pylint', files)
67
68
69def _run_gpylint(files: list[pathlib.Path]) -> None:
70  """Run gpylint on python files if gpylint is available."""
71  if subprocess.run(
72      shlex.split('which gpylint'),
73      check=False,
74  ).returncode:
75    print('gpylint not available. Will use pylint instead.')
76    _run_pylint(files)
77    return
78
79  _run_python_lint('gpylint', files)
80
81
82def _run_pyformat(files: list[pathlib.Path]) -> None:
83  """Run pyformat on certain projects."""
84  if subprocess.run(
85      shlex.split('which pyformat'),
86      check=False,
87  ).returncode:
88    print('pyformat not available. Will skip auto formatting.')
89    return
90
91  def _run_pyformat_on_file(file):
92    completed_process = subprocess.run(
93        shlex.split('pyformat --force_quote_type single ' + file.as_posix()),
94        capture_output=True,
95        check=False,
96    )
97
98    if completed_process.stdout:
99      subprocess.run(
100          shlex.split(
101              'pyformat -i --force_quote_type single ' + file.as_posix()
102          ),
103          check=False,
104      )
105      return True
106    return False
107
108  cpu_count = multiprocessing.cpu_count()
109  with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count) as executor:
110    need_reformat = executor.map(
111        _run_pyformat_on_file, _filter_python_files(files)
112    )
113
114  if any(need_reformat):
115    print(
116        'Reformatting completed. Please add the modified files to git and rerun'
117        ' the repo preupload hook.'
118    )
119    sys.exit(1)
120
121
122def get_preupload_files() -> list[pathlib.Path]:
123  """Get the list of files to be uploaded."""
124  parser = argparse.ArgumentParser()
125  parser.add_argument('preupload_files', nargs='*', help='Files to upload.')
126  args = parser.parse_args()
127  files_to_upload = args.preupload_files
128  if not files_to_upload:
129    # When running by users directly, only consider:
130    # added(A), renamed(R) and modified(M) files
131    # and store them in files_to_upload.
132    cmd = "git status --short | egrep [ARM] | awk '{print $NF}'"
133    files_to_upload = subprocess.check_output(
134        cmd, shell=True, encoding='utf-8'
135    ).splitlines()
136    if files_to_upload:
137      print('Modified files: %s' % files_to_upload)
138  file_paths_to_upload = [
139      pathlib.Path(file).resolve() for file in files_to_upload
140  ]
141  return [file for file in file_paths_to_upload if file.exists()]
142
143
144if __name__ == '__main__':
145  preupload_files = get_preupload_files()
146  _run_pyformat(preupload_files)
147  _run_gpylint(preupload_files)
148