aboutsummaryrefslogtreecommitdiff
path: root/tools/run_android_test
blob: f22138dc0732231f1a7e2f40fa41ef86dae08acd (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
#!/usr/bin/env python
# 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 re
import functools
import logging
import subprocess
import sys
import tempfile
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 GetProp(prop):
  cmd = [ADB_PATH, 'shell', 'getprop', prop]
  print '> adb ' + ' '.join(cmd)
  output = subprocess.check_output(cmd)
  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('--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()

  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/' + args.test_name
  AdbCall('shell', 'rm -rf "%s"; mkdir -p "%s"' % (2 * (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'
  AdbCall('shell', 'test -d "%s" || mkdir -p "%s"' % (2 * (trace_dir,)))
  AdbCall('shell', 'rm -rf "%s/*";  ' % trace_dir)
  AdbCall('shell', 'mkdir -p /data/nativetest')
  # This needs to go into /data/nativetest in order to have the system linker
  # namespace applied, which we need in order to link libdexfile_external.so.
  # This gets linked into our tests via libundwindstack.so.
  #
  # See https://android.googlesource.com/platform/system/core/+/master/rootdir/etc/ld.config.txt.
  AdbCall('push', test_bin, "/data/nativetest")

  if not args.no_data_deps:
    for dep in EnumerateDataDeps():
      AdbCall('push', 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):
    AdbCall('push', 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)
  cmd += ';echo -e "\\nTEST_RET_CODE=$?"'
  print cmd
  test_output = subprocess.check_output([ADB_PATH, 'shell', cmd])
  print test_output
  retcode = re.search(r'^TEST_RET_CODE=(\d)', test_output, re.MULTILINE)
  assert retcode, 'Could not find TEST_RET_CODE=N marker'
  retcode = int(retcode.group(1))
  if not args.no_cleanup:
    AdbCall('shell', 'rm -rf "%s"' % target_dir)
  return retcode


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