#!/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,))) AdbCall('push', test_bin, target_dir) 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 + './%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())