1#!/usr/bin/env python
2
3"""
4Test that aidl generates functional code by running it on an Android device.
5"""
6
7import argparse
8import pipes
9import subprocess
10import shlex
11
12JAVA_OUTPUT_READER = 'aidl_test_sentinel_searcher'
13NATIVE_TEST_CLIENT = 'aidl_test_client'
14NATIVE_TEST_SERVICE = 'aidl_test_service'
15
16TEST_FILTER_ALL = 'all'
17TEST_FILTER_JAVA = 'java'
18TEST_FILTER_NATIVE = 'native'
19
20JAVA_CLIENT_TIMEOUT_SECONDS = 30
21JAVA_LOG_FILE = '/data/data/android.aidl.tests/files/test-client.log'
22JAVA_SUCCESS_SENTINEL = '>>> Java Client Success <<<'
23JAVA_FAILURE_SENTINEL = '>>> Java Client Failure <<<'
24
25class TestFail(Exception):
26    """Raised on test failures."""
27    pass
28
29
30class ShellResult(object):
31    """Represents the result of running a shell command."""
32
33    def __init__(self, exit_status, stdout, stderr):
34        """Construct an instance.
35
36        Args:
37            exit_status: integer exit code of shell command
38            stdout: string stdout of shell command
39            stderr: string stderr of shell command
40        """
41        self.stdout = stdout
42        self.stderr = stderr
43        self.exit_status = exit_status
44
45    def printable_string(self):
46        """Get a string we could print to the logs and understand."""
47        output = []
48        output.append('stdout:')
49        for line in self.stdout.splitlines():
50            output.append('  > %s' % line)
51        output.append('stderr:')
52        for line in self.stderr.splitlines():
53            output.append('  > %s' % line)
54        return '\n'.join(output)
55
56
57class AdbHost(object):
58    """Represents a device connected via ADB."""
59
60    def __init__(self, device_serial=None, verbose=None):
61        """Construct an instance.
62
63        Args:
64            device_serial: options string serial number of attached device.
65            verbose: True iff we should print out ADB commands we run.
66        """
67        self._device_serial = device_serial
68        self._verbose = verbose
69
70    def run(self, command, background=False, ignore_status=False):
71        """Run a command on the device via adb shell.
72
73        Args:
74            command: string containing a shell command to run.
75            background: True iff we should run this command in the background.
76            ignore_status: True iff we should ignore the command's exit code.
77
78        Returns:
79            instance of ShellResult.
80
81        Raises:
82            subprocess.CalledProcessError on command exit != 0.
83        """
84        if background:
85            command = '( %s ) </dev/null >/dev/null 2>&1 &' % command
86        return self.adb('shell %s' % pipes.quote(command),
87                        ignore_status=ignore_status)
88
89    def mktemp(self):
90        """Make a temp file on the device.
91
92        Returns:
93            path to created file as a string
94
95        Raises:
96            subprocess.CalledProcessError on failure.
97        """
98        # Work around b/19635681
99        result = self.run('source /system/etc/mkshrc && mktemp')
100        return result.stdout.strip()
101
102    def adb(self, command, ignore_status=False):
103        """Run an ADB command (e.g. `adb sync`).
104
105        Args:
106            command: string containing command to run
107            ignore_status: True iff we should ignore the command's exit code.
108
109        Returns:
110            instance of ShellResult.
111
112        Raises:
113            subprocess.CalledProcessError on command exit != 0.
114        """
115        command = 'adb %s' % command
116        if self._verbose:
117            print(command)
118        p = subprocess.Popen(command, shell=True, close_fds=True,
119                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
120                             universal_newlines=True)
121        stdout, stderr = p.communicate()
122        if not ignore_status and p.returncode:
123            raise subprocess.CalledProcessError(p.returncode, command)
124        return ShellResult(p.returncode, stdout, stderr)
125
126
127def run_test(host, test_native, test_java):
128    """Body of the test.
129
130    Args:
131        host: AdbHost object to run tests on
132        test_native: True iff we should test native Binder clients.
133        test_java: True iff we shoudl test Java Binder clients.
134    """
135
136    print('Starting aidl integration testing...')
137
138    # Kill any previous test context
139    host.run('rm -f %s' % JAVA_LOG_FILE, ignore_status=True)
140    host.run('pkill %s' % NATIVE_TEST_SERVICE, ignore_status=True)
141
142    # Start up a native server
143    host.run(NATIVE_TEST_SERVICE, background=True)
144
145    # Start up clients
146    if test_native:
147        host.run('pkill %s' % NATIVE_TEST_CLIENT, ignore_status=True)
148        result = host.run(NATIVE_TEST_CLIENT, ignore_status=True)
149        if result.exit_status:
150            print(result.printable_string())
151            raise TestFail('%s returned status code %d' %
152                           (NATIVE_TEST_CLIENT, result.exit_status))
153
154    if test_java:
155        host.run('am start -S -a android.intent.action.MAIN '
156                 '-n android.aidl.tests/.TestServiceClient '
157                 '--es sentinel.success "%s" '
158                 '--es sentinel.failure "%s"' %
159                 (JAVA_SUCCESS_SENTINEL, JAVA_FAILURE_SENTINEL))
160        result = host.run('%s %d %s "%s" "%s"' %
161                          (JAVA_OUTPUT_READER, JAVA_CLIENT_TIMEOUT_SECONDS,
162                           JAVA_LOG_FILE, JAVA_SUCCESS_SENTINEL,
163                           JAVA_FAILURE_SENTINEL),
164                          ignore_status=True)
165        if result.exit_status:
166            print(result.printable_string())
167            raise TestFail('Java client did not complete successfully.')
168
169    print('Success!')
170
171
172def main():
173    """Main entry point."""
174    parser = argparse.ArgumentParser(description=__doc__)
175    parser.add_argument(
176            '--test-filter', default=TEST_FILTER_ALL,
177            choices=[TEST_FILTER_ALL, TEST_FILTER_JAVA, TEST_FILTER_NATIVE])
178    parser.add_argument('--verbose', '-v', action='store_true', default=False)
179    args = parser.parse_args()
180    host = AdbHost(verbose=args.verbose)
181    try:
182        # Tragically, SELinux interferes with our testing
183        host.run('setenforce 0')
184        run_test(host,
185                 args.test_filter in (TEST_FILTER_ALL, TEST_FILTER_NATIVE),
186                 args.test_filter in (TEST_FILTER_ALL, TEST_FILTER_JAVA))
187    finally:
188        host.run('setenforce 1')
189
190
191if __name__ == '__main__':
192    main()
193