aboutsummaryrefslogtreecommitdiff
path: root/tools/run_android_test
blob: 5f3895171c6850ef8f08653b156c50fff6200fdc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python3
# Copyright (C) 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import os
import functools
import logging
import subprocess
import sys
import time
""" Runs a test executable on Android.

Takes care of pushing the extra shared libraries that might be required by
some sanitizers. Propagates the test return code to the host, exiting with
0 only if the test execution succeeds on the device.
"""

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ADB_PATH = os.path.join(ROOT_DIR, 'buildtools/android_sdk/platform-tools/adb')


def RetryOn(exc_type=(), returns_falsy=False, retries=5):
  """Decorator to retry a function in case of errors or falsy values.

  Implements exponential backoff between retries.

  Args:
    exc_type: Type of exceptions to catch and retry on. May also pass a tuple
      of exceptions to catch and retry on any of them. Defaults to catching no
      exceptions at all.
    returns_falsy: If True then the function will be retried until it stops
      returning a "falsy" value (e.g. None, False, 0, [], etc.). If equal to
      'raise' and the function keeps returning falsy values after all retries,
      then the decorator will raise a ValueError.
    retries: Max number of retry attempts. After exhausting that number of
      attempts the function will be called with no safeguards: any exceptions
      will be raised and falsy values returned to the caller (except when
      returns_falsy='raise').
  """

  def Decorator(f):

    @functools.wraps(f)
    def Wrapper(*args, **kwargs):
      wait = 1
      this_retries = kwargs.pop('retries', retries)
      for _ in range(this_retries):
        retry_reason = None
        try:
          value = f(*args, **kwargs)
        except exc_type as exc:
          retry_reason = 'raised %s' % type(exc).__name__
        if retry_reason is None:
          if returns_falsy and not value:
            retry_reason = 'returned %r' % value
          else:
            return value  # Success!
        print('{} {}, will retry in {} second{} ...'.format(
            f.__name__, retry_reason, wait, '' if wait == 1 else 's'))
        time.sleep(wait)
        wait *= 2
      value = f(*args, **kwargs)  # Last try to run with no safeguards.
      if returns_falsy == 'raise' and not value:
        raise ValueError('%s returned %r' % (f.__name__, value))
      return value

    return Wrapper

  return Decorator


def AdbCall(*args):
  cmd = [ADB_PATH] + list(args)
  print('> adb ' + ' '.join(args))
  return subprocess.check_call(cmd)


def AdbPush(host, device):
  if not os.path.exists(host):
    logging.fatal('Cannot find %s. Was it built?', host)
  cmd = [ADB_PATH, 'push', host, device]
  print('> adb push ' + ' '.join(cmd[2:]))
  with open(os.devnull, 'wb') as devnull:
    return subprocess.check_call(cmd, stdout=devnull)


def GetProp(prop):
  cmd = [ADB_PATH, 'shell', 'getprop', prop]
  print('> adb ' + ' '.join(cmd))
  output = subprocess.check_output(cmd).decode()
  lines = output.splitlines()
  assert len(lines) == 1, 'Expected output to have one line: {}'.format(output)
  print(lines[0])
  return lines[0]


@RetryOn([subprocess.CalledProcessError], returns_falsy=True, retries=10)
def WaitForBootCompletion():
  return GetProp('sys.boot_completed') == '1'


def EnumerateDataDeps():
  with open(os.path.join(ROOT_DIR, 'tools', 'test_data.txt')) as f:
    lines = f.readlines()
  for line in (line.strip() for line in lines if not line.startswith('#')):
    assert os.path.exists(line), line
    yield line


def Main():
  parser = argparse.ArgumentParser()
  parser.add_argument('--no-cleanup', '-n', action='store_true')
  parser.add_argument('--no-data-deps', '-x', action='store_true')
  parser.add_argument('--system-adb', action='store_true')
  parser.add_argument('--env', '-e', action='append')
  parser.add_argument('out_dir', help='out/android/')
  parser.add_argument('test_name', help='perfetto_unittests')
  parser.add_argument('cmd_args', nargs=argparse.REMAINDER)
  args = parser.parse_args()

  if args.system_adb:
    global ADB_PATH
    ADB_PATH = 'adb'

  test_bin = os.path.join(args.out_dir, args.test_name)
  assert os.path.exists(test_bin)

  print('Waiting for device ...')
  AdbCall('wait-for-device')
  # WaitForBootCompletion()
  AdbCall('root')
  AdbCall('wait-for-device')

  target_dir = '/data/local/tmp/perfetto_tests'
  if not args.no_cleanup:
    AdbCall('shell', 'rm -rf "%s"' % target_dir)
  AdbCall('shell', 'mkdir -p "%s"' % target_dir)
  # Some tests require the trace directory to exist, while true for android
  # devices in general some emulators might not have it set up. So we check to
  # see if it exists, and if not create it.
  trace_dir = '/data/misc/perfetto-traces/bugreport'
  AdbCall('shell', 'test -d "%s" || mkdir -p "%s"' % (2 * (trace_dir,)))
  AdbCall('shell', 'rm -rf "%s/*";  ' % trace_dir)
  AdbCall('shell', 'mkdir -p /data/nativetest')
  AdbCall('shell', 'echo 0 > /d/tracing/tracing_on')

  # This needs to go into /data/nativetest in order to have the system linker
  # namespace applied, which we need in order to link libdexfile.so.
  # This gets linked into our tests via libundwindstack.so.
  #
  # See https://android.googlesource.com/platform/system/core/+/master/rootdir/etc/ld.config.txt.
  AdbPush(test_bin, "/data/nativetest")

  # These two binaries are required to run perfetto_integrationtests.
  AdbPush(os.path.join(args.out_dir, "perfetto"), "/data/nativetest")
  AdbPush(os.path.join(args.out_dir, "trigger_perfetto"), "/data/nativetest")

  if not args.no_data_deps:
    for dep in EnumerateDataDeps():
      AdbPush(os.path.join(ROOT_DIR, dep), target_dir + '/' + dep)

  # LLVM sanitizers require to sideload a libclangrtXX.so on the device.
  sanitizer_libs = os.path.join(args.out_dir, 'sanitizer_libs')
  env = ' '.join(args.env if args.env is not None else []) + ' '
  if os.path.exists(sanitizer_libs):
    AdbPush(sanitizer_libs, target_dir)
    env += 'LD_LIBRARY_PATH="%s/sanitizer_libs" ' % (target_dir)
  cmd = 'cd %s;' % target_dir
  binary = env + '/data/nativetest/%s' % args.test_name
  cmd += binary
  if args.cmd_args:
    actual_args = [arg.replace(args.test_name, binary) for arg in args.cmd_args]
    cmd += ' ' + ' '.join(actual_args)
  print(cmd)
  retcode = subprocess.call([ADB_PATH, 'shell', '-tt', cmd])
  if not args.no_cleanup:
    AdbCall('shell', 'rm -rf "%s"' % target_dir)

  # Smoke test that adb shell is actually propagating retcode. adb has a history
  # of breaking this.
  test_code = subprocess.call([ADB_PATH, 'shell', '-tt', 'echo Done; exit 42'])
  if test_code != 42:
    logging.fatal('adb is incorrectly propagating the exit code')
    return 1

  return retcode


if __name__ == '__main__':
  sys.exit(Main())