xref: /aosp_15_r20/external/perfetto/tools/run_test_like_ci (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2# Copyright (C) 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
16from __future__ import print_function
17import argparse
18import distutils
19import errno
20import grp
21import os
22import readline
23import sys
24import shutil
25import subprocess
26from pipes import quote
27from subprocess import check_call
28
29try:
30  from shutil import which as find_executable
31except AttributeError:
32  from distutils.spawn import find_executable
33
34REPO_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
35sys.path.append(os.path.join(REPO_ROOT, 'infra', 'ci'))
36from config import JOB_CONFIGS, SANDBOX_IMG
37
38try:
39  input = raw_input
40except NameError:
41  pass
42
43
44def user_in_docker_group():
45  try:
46    group = grp.getgrnam('docker')
47  except KeyError:
48    return False
49  else:
50    return group.gr_gid in os.getgroups()
51
52
53def decision(question='Would you like to continue', confirm=True, default='n'):
54  default = default.lower().strip()
55  yes = default in {'y', 'yes'}
56  no = default in {'n', 'no'}
57  default = 'y' if yes else 'n'
58  prompt = '%s? [%s/%s]: ' % (question, 'Y' if yes else 'y', 'N' if no else 'n')
59  if not confirm:
60    print('%sy' % prompt)
61    return
62  while True:
63    choice = input(prompt).lower().strip()
64    if not choice:
65      choice = default
66    if choice in {'y', 'yes'}:
67      return
68    elif choice in {'n', 'no'}:
69      sys.exit(3)
70
71
72def main():
73  parser = argparse.ArgumentParser(
74      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
75  parser.add_argument('config', choices=JOB_CONFIGS.keys())
76  parser.add_argument(
77      '--runner',
78      help='The container runner executable to use',
79      choices=('podman', 'docker'),
80      default='podman' if find_executable('podman') else 'docker')
81  parser.add_argument(
82      '--build',
83      action='store_true',
84      help='Will perform a build of sandbox image')
85  group = parser.add_mutually_exclusive_group()
86  group.add_argument(
87      '--confirm',
88      action='store_true',
89      default=True,
90      help='User confirmation of decision prompts')
91  group.add_argument(
92      '--no-confirm',
93      dest='confirm',
94      action='store_false',
95      help='Forces confirmation of decision prompts')
96  args = parser.parse_args()
97
98  # Check that the directory is clean.
99  git_cmd = ['git', '-C', REPO_ROOT, 'status', '--porcelain']
100  modified_files = subprocess.check_output(git_cmd).decode()
101  if modified_files:
102    print('The current Git repo has modified/untracked files.')
103    print('The sandboxed VM will fetch the HEAD of your current git repo.')
104    print('This is probably not the state you want to be in.')
105    print('I suggest you stop, commit and then re-run this script')
106    print('Modified files:\n' + modified_files)
107    decision('Do you know what you are doing', confirm=args.confirm)
108
109  if args.build:
110    print('')
111    print('About to build %r locally with %r' % (args.image, args.runner))
112    decision(confirm=args.confirm)
113    check_call(('make', '-C', os.path.join(REPO_ROOT, 'infra', 'ci'),
114                'BUILDER=%s' % args.runner, 'build-sandbox'))
115
116  bundle_path = '/tmp/perfetto-ci.bundle'
117  check_call(['git', '-C', REPO_ROOT, 'bundle', 'create', bundle_path, 'HEAD'])
118  os.chmod(bundle_path, 0o664)
119  env = {
120      'PERFETTO_TEST_GIT_REF': bundle_path,
121  }
122  env.update(JOB_CONFIGS[args.config])
123
124  workdir = os.path.join(REPO_ROOT, 'out', 'tmp.ci')
125  cmd = []
126  if args.runner == 'docker' and not user_in_docker_group():
127    cmd += ['sudo', '--']
128  cmd += [
129      args.runner, 'run', '-it', '--name', 'perfetto_ci', '--cap-add',
130      'SYS_PTRACE', '--rm', '--volume',
131      '%s:/ci/ramdisk' % workdir, '--tmpfs', '/tmp:exec',
132      '--volume=%s:%s:ro' % (bundle_path, bundle_path)
133  ]
134  for kv in env.items():
135    cmd += ['--env', '%s=%s' % kv]
136  cmd += [SANDBOX_IMG]
137  cmd += [
138      'bash', '-c',
139      'cd /ci/ramdisk; bash /ci/init.sh || sudo -u perfetto -EH bash -i'
140  ]
141
142  print(
143      'About to run\n',
144      ' '.join('\n  ' + c if c.startswith('--') or c == 'bash' else quote(c)
145               for c in cmd))
146  print('')
147  print('The VM workdir /ci/ramdisk will be mounted into: %s' % workdir)
148  print('The contents of %s will be deleted before starting the VM' % workdir)
149  decision(confirm=args.confirm)
150
151  try:
152    shutil.rmtree(workdir)
153  except EnvironmentError as e:
154    if e.errno == errno.ENOENT:
155      pass
156    elif e.errno == errno.EACCES:
157      print('')
158      print('Removing previous volume %r' % workdir)
159      check_call(('sudo', 'rm', '-r', quote(workdir)))
160    else:
161      raise
162
163  os.makedirs(workdir)
164  os.execvp(cmd[0], cmd)
165
166
167if __name__ == '__main__':
168  sys.exit(main())
169