Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 # Copyright (C) 2017 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 import argparse
     17 import os
     18 import re
     19 import functools
     20 import logging
     21 import subprocess
     22 import sys
     23 import tempfile
     24 import time
     25 
     26 
     27 """ Runs a test executable on Android.
     28 
     29 Takes care of pushing the extra shared libraries that might be required by
     30 some sanitizers. Propagates the test return code to the host, exiting with
     31 0 only if the test execution succeeds on the device.
     32 """
     33 
     34 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     35 ADB_PATH = os.path.join(ROOT_DIR, 'buildtools/android_sdk/platform-tools/adb')
     36 
     37 
     38 def RetryOn(exc_type=(), returns_falsy=False, retries=5):
     39   """Decorator to retry a function in case of errors or falsy values.
     40 
     41   Implements exponential backoff between retries.
     42 
     43   Args:
     44     exc_type: Type of exceptions to catch and retry on. May also pass a tuple
     45       of exceptions to catch and retry on any of them. Defaults to catching no
     46       exceptions at all.
     47     returns_falsy: If True then the function will be retried until it stops
     48       returning a "falsy" value (e.g. None, False, 0, [], etc.). If equal to
     49       'raise' and the function keeps returning falsy values after all retries,
     50       then the decorator will raise a ValueError.
     51     retries: Max number of retry attempts. After exhausting that number of
     52       attempts the function will be called with no safeguards: any exceptions
     53       will be raised and falsy values returned to the caller (except when
     54       returns_falsy='raise').
     55   """
     56   def Decorator(f):
     57     @functools.wraps(f)
     58     def Wrapper(*args, **kwargs):
     59       wait = 1
     60       this_retries = kwargs.pop('retries', retries)
     61       for _ in range(this_retries):
     62         retry_reason = None
     63         try:
     64           value = f(*args, **kwargs)
     65         except exc_type as exc:
     66           retry_reason = 'raised %s' % type(exc).__name__
     67         if retry_reason is None:
     68           if returns_falsy and not value:
     69             retry_reason = 'returned %r' % value
     70           else:
     71             return value  # Success!
     72         print('{} {}, will retry in {} second{} ...'.format(
     73             f.__name__, retry_reason, wait, '' if wait == 1 else 's'))
     74         time.sleep(wait)
     75         wait *= 2
     76       value = f(*args, **kwargs)  # Last try to run with no safeguards.
     77       if returns_falsy == 'raise' and not value:
     78         raise ValueError('%s returned %r' % (f.__name__, value))
     79       return value
     80     return Wrapper
     81   return Decorator
     82 
     83 
     84 def AdbCall(*args):
     85   cmd = [ADB_PATH] + list(args)
     86   print '> adb ' + ' '.join(args)
     87   return subprocess.check_call(cmd)
     88 
     89 
     90 def GetProp(prop):
     91   cmd = [ADB_PATH, 'shell', 'getprop', prop]
     92   print '> adb ' + ' '.join(cmd)
     93   output = subprocess.check_output(cmd)
     94   lines = output.splitlines()
     95   assert len(lines) == 1, 'Expected output to have one line: {}'.format(output)
     96   print lines[0]
     97   return lines[0]
     98 
     99 
    100 @RetryOn([subprocess.CalledProcessError], returns_falsy=True, retries=10)
    101 def WaitForBootCompletion():
    102   return GetProp('sys.boot_completed') == '1'
    103 
    104 
    105 def EnumerateDataDeps():
    106   with open(os.path.join(ROOT_DIR, 'tools', 'test_data.txt')) as f:
    107     lines = f.readlines()
    108   for line in (line.strip() for line in lines if not line.startswith('#')):
    109     assert os.path.exists(line), line
    110     yield line
    111 
    112 
    113 def Main():
    114   parser = argparse.ArgumentParser()
    115   parser.add_argument('--no-cleanup', '-n', action='store_true')
    116   parser.add_argument('--no-data-deps', '-x', action='store_true')
    117   parser.add_argument('--env', '-e', action='append')
    118   parser.add_argument('out_dir', help='out/android/')
    119   parser.add_argument('test_name', help='perfetto_unittests')
    120   parser.add_argument('cmd_args', nargs=argparse.REMAINDER)
    121   args = parser.parse_args()
    122 
    123   test_bin = os.path.join(args.out_dir, args.test_name)
    124   assert os.path.exists(test_bin)
    125 
    126   print 'Waiting for device ...'
    127   AdbCall('wait-for-device')
    128   # WaitForBootCompletion()
    129   AdbCall('root')
    130   AdbCall('wait-for-device')
    131 
    132   target_dir = '/data/local/tmp/' + args.test_name
    133   AdbCall('shell', 'rm -rf "%s"; mkdir -p "%s"' % (2 * (target_dir,)))
    134   # Some tests require the trace directory to exist, while true for android
    135   # devices in general some emulators might not have it set up. So we check to
    136   # see if it exists, and if not create it.
    137   trace_dir = '/data/misc/perfetto-traces'
    138   AdbCall('shell', 'test -d "%s" || mkdir -p "%s"' % (2 * (trace_dir,)))
    139   AdbCall('shell', 'rm -rf "%s/*";  ' % trace_dir)
    140   AdbCall('shell', 'mkdir -p /data/nativetest')
    141   # This needs to go into /data/nativetest in order to have the system linker
    142   # namespace applied, which we need in order to link libdexfile_external.so.
    143   # This gets linked into our tests via libundwindstack.so.
    144   #
    145   # See https://android.googlesource.com/platform/system/core/+/master/rootdir/etc/ld.config.txt.
    146   AdbCall('push', test_bin, "/data/nativetest")
    147 
    148   if not args.no_data_deps:
    149     for dep in EnumerateDataDeps():
    150       AdbCall('push', os.path.join(ROOT_DIR, dep), target_dir + '/' + dep)
    151 
    152   # LLVM sanitizers require to sideload a libclangrtXX.so on the device.
    153   sanitizer_libs = os.path.join(args.out_dir, 'sanitizer_libs')
    154   env = ' '.join(args.env if args.env is not None else []) + ' '
    155   if os.path.exists(sanitizer_libs):
    156     AdbCall('push', sanitizer_libs, target_dir)
    157     env += 'LD_LIBRARY_PATH="%s/sanitizer_libs" ' % (target_dir)
    158   cmd = 'cd %s;' % target_dir;
    159   binary = env + '/data/nativetest/%s' % args.test_name
    160   cmd += binary
    161   if args.cmd_args:
    162     actual_args = [arg.replace(args.test_name, binary) for arg in args.cmd_args]
    163     cmd += ' ' + ' '.join(actual_args)
    164   cmd += ';echo -e "\\nTEST_RET_CODE=$?"'
    165   print cmd
    166   test_output = subprocess.check_output([ADB_PATH, 'shell', cmd])
    167   print test_output
    168   retcode = re.search(r'^TEST_RET_CODE=(\d)', test_output, re.MULTILINE)
    169   assert retcode, 'Could not find TEST_RET_CODE=N marker'
    170   retcode = int(retcode.group(1))
    171   if not args.no_cleanup:
    172     AdbCall('shell', 'rm -rf "%s"' % target_dir)
    173   return retcode
    174 
    175 
    176 if __name__ == '__main__':
    177   sys.exit(Main())
    178