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   AdbCall('push', test_bin, target_dir)
    135 
    136   if not args.no_data_deps:
    137     for dep in EnumerateDataDeps():
    138       AdbCall('push', os.path.join(ROOT_DIR, dep), target_dir + '/' + dep)
    139 
    140   # LLVM sanitizers require to sideload a libclangrtXX.so on the device.
    141   sanitizer_libs = os.path.join(args.out_dir, 'sanitizer_libs')
    142   env = ' '.join(args.env if args.env is not None else []) + ' '
    143   if os.path.exists(sanitizer_libs):
    144     AdbCall('push', sanitizer_libs, target_dir)
    145     env += 'LD_LIBRARY_PATH="%s/sanitizer_libs" ' % (target_dir)
    146   cmd = 'cd %s;' % target_dir;
    147   binary = env + './%s' % args.test_name
    148   cmd += binary
    149   if args.cmd_args:
    150     actual_args = [arg.replace(args.test_name, binary) for arg in args.cmd_args]
    151     cmd += ' ' + ' '.join(actual_args)
    152   cmd += ';echo -e "\\nTEST_RET_CODE=$?"'
    153   print cmd
    154   test_output = subprocess.check_output([ADB_PATH, 'shell', cmd])
    155   print test_output
    156   retcode = re.search(r'^TEST_RET_CODE=(\d)', test_output, re.MULTILINE)
    157   assert retcode, 'Could not find TEST_RET_CODE=N marker'
    158   retcode = int(retcode.group(1))
    159   if not args.no_cleanup:
    160     AdbCall('shell', 'rm -rf "%s"' % target_dir)
    161   return retcode
    162 
    163 
    164 if __name__ == '__main__':
    165   sys.exit(Main())
    166