|  | #!/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()) |