#!/usr/bin/env python

"""
Test that aidl generates functional code by running it on an Android device.
"""

import argparse
import pipes
import subprocess
import shlex

JAVA_OUTPUT_READER = 'aidl_test_sentinel_searcher'
NATIVE_TEST_CLIENT = 'aidl_test_client'
NATIVE_TEST_SERVICE = 'aidl_test_service'

TEST_FILTER_ALL = 'all'
TEST_FILTER_JAVA = 'java'
TEST_FILTER_NATIVE = 'native'

JAVA_CLIENT_TIMEOUT_SECONDS = 30
JAVA_LOG_FILE = '/data/data/android.aidl.tests/files/test-client.log'
JAVA_SUCCESS_SENTINEL = '>>> Java Client Success <<<'
JAVA_FAILURE_SENTINEL = '>>> Java Client Failure <<<'

class TestFail(Exception):
    """Raised on test failures."""
    pass


class ShellResult(object):
    """Represents the result of running a shell command."""

    def __init__(self, exit_status, stdout, stderr):
        """Construct an instance.

        Args:
            exit_status: integer exit code of shell command
            stdout: string stdout of shell command
            stderr: string stderr of shell command
        """
        self.stdout = stdout
        self.stderr = stderr
        self.exit_status = exit_status

    def printable_string(self):
        """Get a string we could print to the logs and understand."""
        output = []
        output.append('stdout:')
        for line in self.stdout.splitlines():
            output.append('  > %s' % line)
        output.append('stderr:')
        for line in self.stderr.splitlines():
            output.append('  > %s' % line)
        return '\n'.join(output)


class AdbHost(object):
    """Represents a device connected via ADB."""

    def __init__(self, device_serial=None, verbose=None):
        """Construct an instance.

        Args:
            device_serial: options string serial number of attached device.
            verbose: True iff we should print out ADB commands we run.
        """
        self._device_serial = device_serial
        self._verbose = verbose

    def run(self, command, background=False, ignore_status=False):
        """Run a command on the device via adb shell.

        Args:
            command: string containing a shell command to run.
            background: True iff we should run this command in the background.
            ignore_status: True iff we should ignore the command's exit code.

        Returns:
            instance of ShellResult.

        Raises:
            subprocess.CalledProcessError on command exit != 0.
        """
        if background:
            command = '( %s ) </dev/null >/dev/null 2>&1 &' % command
        return self.adb('shell %s' % pipes.quote(command),
                        ignore_status=ignore_status)

    def mktemp(self):
        """Make a temp file on the device.

        Returns:
            path to created file as a string

        Raises:
            subprocess.CalledProcessError on failure.
        """
        # Work around b/19635681
        result = self.run('source /system/etc/mkshrc && mktemp')
        return result.stdout.strip()

    def adb(self, command, ignore_status=False):
        """Run an ADB command (e.g. `adb sync`).

        Args:
            command: string containing command to run
            ignore_status: True iff we should ignore the command's exit code.

        Returns:
            instance of ShellResult.

        Raises:
            subprocess.CalledProcessError on command exit != 0.
        """
        command = 'adb %s' % command
        if self._verbose:
            print(command)
        p = subprocess.Popen(command, shell=True, close_fds=True,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                             universal_newlines=True)
        stdout, stderr = p.communicate()
        if not ignore_status and p.returncode:
            raise subprocess.CalledProcessError(p.returncode, command)
        return ShellResult(p.returncode, stdout, stderr)


def run_test(test_native, test_java, apk_path=None, refresh_binaries=False,
             device_serial=None, verbose=False):
    """Body of the test.

    Args:
        test_native: True iff we should test native Binder clients.
        test_java: True iff we shoudl test Java Binder clients.
        apk_path: Optional path to an APK to install via `adb install`
        refresh_binaries: True iff we should `adb sync` new binaries to the
                device.
        device_serial: Optional string containing the serial number of the
                device under test.
        verbose: True iff we should enable verbose output during the test.
    """

    print('Starting aidl integration testing...')
    host = AdbHost(device_serial=device_serial, verbose=verbose)
    if apk_path is not None:
        host.adb('install -r %s' % apk_path)
    if refresh_binaries:
        host.adb('remount')
        host.adb('sync')
    host.run('setenforce 0')
    # Kill any previous test context
    host.run('rm -f %s' % JAVA_LOG_FILE, ignore_status=True)
    host.run('pkill %s' % NATIVE_TEST_SERVICE, ignore_status=True)

    # Start up a native server
    host.run(NATIVE_TEST_SERVICE, background=True)

    # Start up clients
    if test_native:
        host.run('pkill %s' % NATIVE_TEST_CLIENT, ignore_status=True)
        result = host.run(NATIVE_TEST_CLIENT, ignore_status=True)
        if result.exit_status:
            print(result.printable_string())
            raise TestFail('%s returned status code %d' %
                           (NATIVE_TEST_CLIENT, result.exit_status))

    if test_java:
        host.run('am start -S -a android.intent.action.MAIN '
                 '-n android.aidl.tests/.TestServiceClient '
                 '--es sentinel.success "%s" '
                 '--es sentinel.failure "%s"' %
                 (JAVA_SUCCESS_SENTINEL, JAVA_FAILURE_SENTINEL))
        result = host.run('%s %d %s "%s" "%s"' %
                          (JAVA_OUTPUT_READER, JAVA_CLIENT_TIMEOUT_SECONDS,
                           JAVA_LOG_FILE, JAVA_SUCCESS_SENTINEL,
                           JAVA_FAILURE_SENTINEL),
                          ignore_status=True)
        if result.exit_status:
            print(result.printable_string())
            raise TestFail('Java client did not complete successfully.')

    print('Success!')


def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--apk', dest='apk_path', type=str, default=None,
                        help='Path to an APK to install on the device.')
    parser.add_argument('--refresh-bins', action='store_true', default=False,
                        help='Pass this flag to have the test run adb sync')
    parser.add_argument('--serial', '-s', type=str, default=None,
                        help='Serial number of device to test against')
    parser.add_argument(
            '--test-filter', default=TEST_FILTER_ALL,
            choices=[TEST_FILTER_ALL, TEST_FILTER_JAVA, TEST_FILTER_NATIVE])
    parser.add_argument('--verbose', '-v', action='store_true', default=False)
    args = parser.parse_args()
    run_test(args.test_filter in (TEST_FILTER_ALL, TEST_FILTER_NATIVE),
             args.test_filter in (TEST_FILTER_ALL, TEST_FILTER_JAVA),
             apk_path=args.apk_path, refresh_binaries=args.refresh_bins,
             device_serial=args.serial, verbose=args.verbose)


if __name__ == '__main__':
    main()
