diff options
author | Bill Napier <napier@google.com> | 2010-07-27 16:18:34 -0700 |
---|---|---|
committer | Bill Napier <napier@google.com> | 2010-07-27 16:40:25 -0700 |
commit | 7169aa30eeaac89c49984bb9d061ca152d43391a (patch) | |
tree | 8c4d6380f0a6c0e6069afe9ea4bd215919c36683 | |
parent | 2e43b58d4e4bf4a2dfbf2a605c8c309a0cfd01b6 (diff) | |
download | sdk-7169aa30eeaac89c49984bb9d061ca152d43391a.zip sdk-7169aa30eeaac89c49984bb9d061ca152d43391a.tar.gz sdk-7169aa30eeaac89c49984bb9d061ca152d43391a.tar.bz2 |
Release MonkeyRunner into open source.
Change-Id: Ie08e493e700e3e4c85270629f68547a08b7457d4
53 files changed, 5464 insertions, 0 deletions
diff --git a/monkeyrunner/Android.mk b/monkeyrunner/Android.mk new file mode 100644 index 0000000..21cf67a --- /dev/null +++ b/monkeyrunner/Android.mk @@ -0,0 +1,19 @@ +# +# Copyright (C) 2009 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. +# +MONKEYRUNNER_LOCAL_DIR := $(call my-dir) +include $(MONKEYRUNNER_LOCAL_DIR)/etc/Android.mk +include $(MONKEYRUNNER_LOCAL_DIR)/src/Android.mk +include $(MONKEYRUNNER_LOCAL_DIR)/test/Android.mk diff --git a/monkeyrunner/MODULE_LICENSE_APACHE2 b/monkeyrunner/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/monkeyrunner/MODULE_LICENSE_APACHE2 diff --git a/monkeyrunner/NOTICE b/monkeyrunner/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/monkeyrunner/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/monkeyrunner/etc/Android.mk b/monkeyrunner/etc/Android.mk new file mode 100644 index 0000000..2d757fd --- /dev/null +++ b/monkeyrunner/etc/Android.mk @@ -0,0 +1,20 @@ +# +# Copyright (C) 2009 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PREBUILT_EXECUTABLES := monkeyrunner +include $(BUILD_HOST_PREBUILT) diff --git a/monkeyrunner/etc/manifest.txt b/monkeyrunner/etc/manifest.txt new file mode 100644 index 0000000..706842e --- /dev/null +++ b/monkeyrunner/etc/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.monkeyrunner.MonkeyRunnerStarter diff --git a/monkeyrunner/etc/monkeyrunner b/monkeyrunner/etc/monkeyrunner new file mode 100755 index 0000000..364be2a --- /dev/null +++ b/monkeyrunner/etc/monkeyrunner @@ -0,0 +1,74 @@ +#!/bin/sh +# Copyright 2005-2007, 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. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/`basename "${prog}"` +cd "${oldwd}" + +jarfile=monkeyrunner.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo `basename "$prog"`": can't find $jarfile" + exit 1 +fi + + +# Check args. +if [ debug = "$1" ]; then + # add this in for debugging + java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y + shift 1 +else + java_debug= +fi + +if [ "$OSTYPE" = "cygwin" ] ; then + jarpath=`cygpath -w "$frameworkdir/$jarfile"` + progdir=`cygpath -w "$progdir"` +else + jarpath="$frameworkdir/$jarfile" +fi + +# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored +# might need more memory, e.g. -Xmx128M +exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/monkeyrunner/jython/test/MonkeyRunner_test.py b/monkeyrunner/jython/test/MonkeyRunner_test.py new file mode 100644 index 0000000..cc4d1f2 --- /dev/null +++ b/monkeyrunner/jython/test/MonkeyRunner_test.py @@ -0,0 +1,54 @@ +#!/usr/bin/python2.4 +# +# Copyright 2010, 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. + +"""Test cases for com.android.monkeyrunner.MonkeyRunner.""" + +import time +import unittest + +from com.android.monkeyrunner import MonkeyRunner + + +class TestMonkeyRunnerArgParsing(unittest.TestCase): + """Test ArgParsing for the MonkeyRunner methods.""" + def testWaitForConnectionNoArgs(self): + MonkeyRunner.waitForConnection() + + def testWaitForConnectionSingleArg(self): + MonkeyRunner.waitForConnection(2) + + def testWaitForConnectionDoubleArg(self): + MonkeyRunner.waitForConnection(2, '*') + + def testWaitForConnectionKeywordArg(self): + MonkeyRunner.waitForConnection(timeout=2, deviceId='foo') + + def testWaitForConnectionKeywordArgTooMany(self): + try: + MonkeyRunner.waitForConnection(timeout=2, deviceId='foo', extra='fail') + except TypeError: + return + self.fail('Should have raised TypeError') + + def testSleep(self): + start = time.time() + MonkeyRunner.sleep(1.5) + end = time.time() + + self.assertTrue(end - start >= 1.5) + +if __name__ == '__main__': + unittest.main() diff --git a/monkeyrunner/jython/test/all_tests.py b/monkeyrunner/jython/test/all_tests.py new file mode 100644 index 0000000..2dd0ab4 --- /dev/null +++ b/monkeyrunner/jython/test/all_tests.py @@ -0,0 +1,49 @@ +#!/usr/bin/python2.4 +# +# Copyright 2010, 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. + +"""Test runner to run all the tests in this package.""" + +import os +import re +import sys +import unittest + + +TESTCASE_RE = re.compile('_test\.py$') + + +def AllTestFilesInDir(path): + """Finds all the unit test files in the given path.""" + return filter(TESTCASE_RE.search, os.listdir(path)) + + +def suite(loader=unittest.defaultTestLoader): + """Creates the all_tests TestSuite.""" + script_parent_path = os.path.abspath(os.path.dirname(sys.argv[0])) + # Find all the _test.py files in the same directory we are in + test_files = AllTestFilesInDir(script_parent_path) + # Convert them into module names + module_names = [os.path.splitext(f)[0] for f in test_files] + # And import them + modules = map(__import__, module_names) + # And create the test suite for all these modules + return unittest.TestSuite([loader.loadTestsFromModule(m) for m in modules]) + +if __name__ == '__main__': + result = unittest.TextTestRunner().run(suite()) + if not result.wasSuccessful(): + # On failure return an error code + sys.exit(1) diff --git a/monkeyrunner/scripts/help.py b/monkeyrunner/scripts/help.py new file mode 100644 index 0000000..832d2cb --- /dev/null +++ b/monkeyrunner/scripts/help.py @@ -0,0 +1,48 @@ +#!/usr/bin/env monkeyrunner +# Copyright 2010, 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. +from com.android.monkeyrunner import MonkeyRunner as mr + +import os +import sys + +supported_formats = ['html', 'text'] + +if len(sys.argv) != 3: + print 'help.py: format output' + sys.exit(1) + +(format, saveto_path) = sys.argv[1:] + +if not format.lower() in supported_formats: + print 'format %s is not a supported format' % format + sys.exit(2) + +output = mr.help(format=format) +if not output: + print 'Error generating help format' + sys.exit(3) + +dirname = os.path.dirname(saveto_path) +try: + os.makedirs(dirname) +except: + print 'oops' + pass # It already existed + +fp = open(saveto_path, 'w') +fp.write(output) +fp.close() + +sys.exit(0) diff --git a/monkeyrunner/src/Android.mk b/monkeyrunner/src/Android.mk new file mode 100644 index 0000000..59337c6 --- /dev/null +++ b/monkeyrunner/src/Android.mk @@ -0,0 +1,32 @@ +# +# Copyright (C) 2009 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAR_MANIFEST := ../etc/manifest.txt +LOCAL_JAVA_LIBRARIES := \ + ddmlib \ + jython \ + guavalib \ + clearsilver +LOCAL_SHARED_LIBRARIES := libclearsilver-jni +LOCAL_JAVA_RESOURCE_DIRS := resources + +LOCAL_MODULE := monkeyrunner + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java b/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java new file mode 100644 index 0000000..258261b --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableMap.Builder; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import org.python.core.ArgParser; +import org.python.core.Py; +import org.python.core.PyDictionary; +import org.python.core.PyFloat; +import org.python.core.PyInteger; +import org.python.core.PyList; +import org.python.core.PyNone; +import org.python.core.PyObject; +import org.python.core.PyString; +import org.python.core.PyTuple; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Collection of useful utilities function for interacting with the Jython interpreter. + */ +public final class JythonUtils { + private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName()); + private JythonUtils() { } + + /** + * Mapping of PyObject classes to the java class we want to convert them to. + */ + private static final Map<Class<? extends PyObject>, Class<?>> PYOBJECT_TO_JAVA_OBJECT_MAP; + static { + Builder<Class<? extends PyObject>, Class<?>> builder = ImmutableMap.builder(); + + builder.put(PyString.class, String.class); + // What python calls float, most people call double + builder.put(PyFloat.class, Double.class); + builder.put(PyInteger.class, Integer.class); + + PYOBJECT_TO_JAVA_OBJECT_MAP = builder.build(); + } + + /** + * Utility method to be called from Jython bindings to give proper handling of keyword and + * positional arguments. + * + * @param args the PyObject arguments from the binding + * @param kws the keyword arguments from the binding + * @return an ArgParser for this binding, or null on error + */ + public static ArgParser createArgParser(PyObject[] args, String[] kws) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + // Up 2 levels in the current stack to give us the calling function + StackTraceElement element = stackTrace[2]; + + String methodName = element.getMethodName(); + String className = element.getClassName(); + + Class<?> clz; + try { + clz = Class.forName(className); + } catch (ClassNotFoundException e) { + LOG.log(Level.SEVERE, "Got exception: ", e); + return null; + } + + Method m; + + try { + m = clz.getMethod(methodName, PyObject[].class, String[].class); + } catch (SecurityException e) { + LOG.log(Level.SEVERE, "Got exception: ", e); + return null; + } catch (NoSuchMethodException e) { + LOG.log(Level.SEVERE, "Got exception: ", e); + return null; + } + + MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); + return new ArgParser(methodName, args, kws, + annotation.args()); + } + + /** + * Get a python floating point value from an ArgParser. + * + * @param ap the ArgParser to get the value from. + * @param position the position in the parser + * @return the double value + */ + public static double getFloat(ArgParser ap, int position) { + PyObject arg = ap.getPyObject(position); + + if (Py.isInstance(arg, PyFloat.TYPE)) { + return ((PyFloat) arg).asDouble(); + } + if (Py.isInstance(arg, PyInteger.TYPE)) { + return ((PyInteger) arg).asDouble(); + } + throw Py.TypeError("Unable to parse argument: " + position); + } + + /** + * Get a python floating point value from an ArgParser. + * + * @param ap the ArgParser to get the value from. + * @param position the position in the parser + * @param defaultValue the default value to return if the arg isn't specified. + * @return the double value + */ + public static double getFloat(ArgParser ap, int position, double defaultValue) { + PyObject arg = ap.getPyObject(position, new PyFloat(defaultValue)); + + if (Py.isInstance(arg, PyFloat.TYPE)) { + return ((PyFloat) arg).asDouble(); + } + if (Py.isInstance(arg, PyInteger.TYPE)) { + return ((PyInteger) arg).asDouble(); + } + throw Py.TypeError("Unable to parse argument: " + position); + } + + /** + * Get a list of arguments from an ArgParser. + * + * @param ap the ArgParser + * @param position the position in the parser to get the argument from + * @return a list of those items + */ + @SuppressWarnings("unchecked") + public static List<Object> getList(ArgParser ap, int position) { + PyObject arg = ap.getPyObject(position, Py.None); + if (Py.isInstance(arg, PyNone.TYPE)) { + return Collections.emptyList(); + } + + List<Object> ret = Lists.newArrayList(); + PyList array = (PyList) arg; + for (int x = 0; x < array.__len__(); x++) { + PyObject item = array.__getitem__(x); + + Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(item.getClass()); + if (javaClass != null) { + ret.add(item.__tojava__(javaClass)); + } + } + return ret; + } + + /** + * Get a dictionary from an ArgParser. For ease of use, key types are always coerced to + * strings. If key type cannot be coeraced to string, an exception is raised. + * + * @param ap the ArgParser to work with + * @param position the position in the parser to get. + * @return a Map mapping the String key to the value + */ + public static Map<String, Object> getMap(ArgParser ap, int position) { + PyObject arg = ap.getPyObject(position, Py.None); + if (Py.isInstance(arg, PyNone.TYPE)) { + return Collections.emptyMap(); + } + + Map<String, Object> ret = Maps.newHashMap(); + // cast is safe as getPyObjectbyType ensures it + PyDictionary dict = (PyDictionary) arg; + PyList items = dict.items(); + for (int x = 0; x < items.__len__(); x++) { + // It's a list of tuples + PyTuple item = (PyTuple) items.__getitem__(x); + // We call str(key) on the key to get the string and then convert it to the java string. + String key = (String) item.__getitem__(0).__str__().__tojava__(String.class); + PyObject value = item.__getitem__(1); + + // Look up the conversion type and convert the value + Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(value.getClass()); + if (javaClass != null) { + ret.put(key, value.__tojava__(javaClass)); + } + } + return ret; + } + + private static PyObject convertObject(Object o) { + if (o instanceof String) { + return new PyString((String) o); + } else if (o instanceof Double) { + return new PyFloat((Double) o); + } else if (o instanceof Integer) { + return new PyInteger((Integer) o); + } else if (o instanceof Float) { + float f = (Float) o; + return new PyFloat(f); + } + return Py.None; + } + + /** + * Convert the given Java Map into a PyDictionary. + * + * @param map the map to convert + * @return the python dictionary + */ + public static PyDictionary convertMapToDict(Map<String, Object> map) { + Map<PyObject, PyObject> resultMap = Maps.newHashMap(); + + for (Entry<String, Object> entry : map.entrySet()) { + resultMap.put(new PyString(entry.getKey()), + convertObject(entry.getValue())); + } + return new PyDictionary(resultMap); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java new file mode 100644 index 0000000..87c54c2 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import org.python.core.ArgParser; +import org.python.core.Py; +import org.python.core.PyDictionary; +import org.python.core.PyException; +import org.python.core.PyObject; +import org.python.core.PyTuple; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import javax.annotation.Nullable; + +/* + * Abstract base class that represents a single connected Android + * Device and provides MonkeyRunner API methods for interacting with + * that device. Each backend will need to create a concrete + * implementation of this class. + */ +public abstract class MonkeyDevice { + /** + * Create a MonkeyMananger for talking to this device. + * + * NOTE: This is not part of the jython API. + * + * @return the MonkeyManager + */ + public abstract MonkeyManager getManager(); + + /** + * Dispose of any native resoureces this device may have taken hold of. + * + * NOTE: This is not part of the jython API. + */ + public abstract void dispose(); + + @MonkeyRunnerExported(doc = "Fetch the screenbuffer from the device and return it.", + returns = "The captured snapshot.") + public abstract MonkeyImage takeSnapshot(); + + @MonkeyRunnerExported(doc = "Get a MonkeyRunner property (like build.fingerprint)", + args = {"key"}, + argDocs = {"The key of the property to return"}, + returns = "The value of the property") + public String getProperty(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + return getProperty(ap.getString(0)); + } + + @MonkeyRunnerExported(doc = "Get a system property (returns the same value as getprop).", + args = {"key"}, + argDocs = {"The key of the property to return"}, + returns = "The value of the property") + public String getSystemProperty(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + return getSystemProperty(ap.getString(0)); + } + + @MonkeyRunnerExported(doc = "Enumeration of possible touch and press event types. This gets " + + "passed into a press or touch call to specify the event type.", + argDocs = {"Indicates the down part of a touch/press event", + "Indicates the up part of a touch/press event.", + "Indicates that the monkey should send a down event immediately " + + "followed by an up event"}) + public enum TouchPressType { + DOWN, UP, DOWN_AND_UP + } + + @MonkeyRunnerExported(doc = "Send a touch event at the specified location", + args = { "x", "y", "type" }, + argDocs = { "x coordinate", "y coordinate", "the type of touch event to send"}) + public void touch(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + int x = ap.getInt(0); + int y = ap.getInt(1); + + // Default + MonkeyDevice.TouchPressType type = MonkeyDevice.TouchPressType.DOWN_AND_UP; + try { + PyObject pyObject = ap.getPyObject(2); + type = (TouchPressType) pyObject.__tojava__(MonkeyDevice.TouchPressType.class); + } catch (PyException e) { + // bad stuff was passed in, just use the already specified default value + type = MonkeyDevice.TouchPressType.DOWN_AND_UP; + } + touch(x, y, type); + } + + @MonkeyRunnerExported(doc = "Simulate a drag on the screen.", + args = { "start", "end", "duration", "steps"}, + argDocs = { "The starting point for the drag (a tuple of x,y)", + "The end point for the drag (a tuple of x,y)", + "How long (in seconds) should the drag take (default is 1.0 seconds)", + "The number of steps to take when interpolating points. (default is 10)"}) + public void drag(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + PyObject startObject = ap.getPyObject(0); + if (!(startObject instanceof PyTuple)) { + throw Py.TypeError("Agrument 0 is not a tuple"); + } + PyObject endObject = ap.getPyObject(1); + if (!(endObject instanceof PyTuple)) { + throw Py.TypeError("Agrument 1 is not a tuple"); + } + + PyTuple start = (PyTuple) startObject; + PyTuple end = (PyTuple) endObject; + + int startx = (Integer) start.__getitem__(0).__tojava__(Integer.class); + int starty = (Integer) start.__getitem__(1).__tojava__(Integer.class); + int endx = (Integer) end.__getitem__(0).__tojava__(Integer.class); + int endy = (Integer) end.__getitem__(1).__tojava__(Integer.class); + + double seconds = JythonUtils.getFloat(ap, 2, 1.0); + long ms = (long) (seconds * 1000.0); + + int steps = ap.getInt(3, 10); + + drag(startx, starty, endx, endy, steps, ms); + } + + @MonkeyRunnerExported(doc = "Send a key press event to the specified button", + args = { "name", "type" }, + argDocs = { "the name of the key to press", "the type of touch event to send"}) + public void press(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String name = ap.getString(0); + + // Default + MonkeyDevice.TouchPressType type = MonkeyDevice.TouchPressType.DOWN_AND_UP; + try { + PyObject pyObject = ap.getPyObject(1); + type = (TouchPressType) pyObject.__tojava__(MonkeyDevice.TouchPressType.class); + } catch (PyException e) { + // bad stuff was passed in, just use the already specified default value + type = MonkeyDevice.TouchPressType.DOWN_AND_UP; + } + press(name, type); + } + + @MonkeyRunnerExported(doc = "Type the specified string on the keyboard.", + args = { "message" }, + argDocs = { "the message to type." }) + public void type(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String message = ap.getString(0); + type(message); + } + + @MonkeyRunnerExported(doc = "Execute the given command on the shell.", + args = { "cmd"}, + argDocs = { "The command to execute" }, + returns = "The output of the command") + public String shell(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String cmd = ap.getString(0); + return shell(cmd); + } + + @MonkeyRunnerExported(doc = "Reboot the specified device", + args = { "into" }, + argDocs = { "the bootloader to reboot into (bootloader, recovery, or None)"}) + public void reboot(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String into = ap.getString(0, null); + + reboot(into); + } + + @MonkeyRunnerExported(doc = "Install the specified apk onto the device.", + args = { "path" }, + argDocs = { "The path on the host filesystem to the APK to install." }, + returns = "True if install succeeded") + public boolean installPackage(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String path = ap.getString(0); + return installPackage(path); + } + + @MonkeyRunnerExported(doc = "Remove the specified package from the device.", + args = { "package"}, + argDocs = { "The name of the package to uninstall"}, + returns = "'True if remove succeeded") + public boolean removePackage(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String packageName = ap.getString(0); + return removePackage(packageName); + } + + @MonkeyRunnerExported(doc = "Start the Activity specified by the intent.", + args = { "uri", "action", "data", "mimetype", "categories", "extras", + "component", "flags" }, + argDocs = { "The URI for the intent", + "The action for the intent", + "The data URI for the intent", + "The mime type for the intent", + "The list of category names for the intent", + "A dictionary of extras to add to the intent. Types of these extras " + + "are inferred from the python types of the values", + "The component of the intent", + "A list of flags for the intent" }) + public void startActivity(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String uri = ap.getString(0, null); + String action = ap.getString(1, null); + String data = ap.getString(2, null); + String mimetype = ap.getString(3, null); + Collection<String> categories = Collections2.transform(JythonUtils.getList(ap, 4), + Functions.toStringFunction()); + Map<String, Object> extras = JythonUtils.getMap(ap, 5); + String component = ap.getString(6, null); + int flags = ap.getInt(7, 0); + + startActivity(uri, action, data, mimetype, categories, extras, component, flags); + } + + @MonkeyRunnerExported(doc = "Start the specified broadcast intent on the device.", + args = { "uri", "action", "data", "mimetype", "categories", "extras", + "component", "flags" }, + argDocs = { "The URI for the intent", + "The action for the intent", + "The data URI for the intent", + "The mime type for the intent", + "The list of category names for the intent", + "A dictionary of extras to add to the intent. Types of these extras " + + "are inferred from the python types of the values", + "The component of the intent", + "A list of flags for the intent" }) + public void broadcastIntent(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String uri = ap.getString(0, null); + String action = ap.getString(1, null); + String data = ap.getString(2, null); + String mimetype = ap.getString(3, null); + Collection<String> categories = Collections2.transform(JythonUtils.getList(ap, 4), + Functions.toStringFunction()); + Map<String, Object> extras = JythonUtils.getMap(ap, 5); + String component = ap.getString(6, null); + int flags = ap.getInt(7, 0); + + broadcastIntent(uri, action, data, mimetype, categories, extras, component, flags); + } + + @MonkeyRunnerExported(doc = "Instrument the specified package and return the results from it.", + args = { "className", "args" }, + argDocs = { "The class name to instrument (like com.android.test/.TestInstrument)", + "A Map of String to Objects for the aruments to pass to this " + + "instrumentation (default value is None)" }, + returns = "A map of string to objects for the results this instrumentation returned") + public PyDictionary instrument(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String packageName = ap.getString(0); + Map<String, Object> instrumentArgs = JythonUtils.getMap(ap, 1); + if (instrumentArgs == null) { + instrumentArgs = Collections.emptyMap(); + } + + Map<String, Object> result = instrument(packageName, instrumentArgs); + return JythonUtils.convertMapToDict(result); + } + + @MonkeyRunnerExported(doc = "Wake up the screen on the device") + public void wake(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + wake(); + } + + /** + * Reboot the device. + * + * @param into which bootloader to boot into. Null means default reboot. + */ + protected abstract void reboot(@Nullable String into); + + protected abstract String getProperty(String key); + protected abstract String getSystemProperty(String key); + protected abstract void touch(int x, int y, TouchPressType type); + protected abstract void press(String keyName, TouchPressType type); + protected abstract void drag(int startx, int starty, int endx, int endy, int steps, long ms); + protected abstract void type(String string); + protected abstract String shell(String cmd); + protected abstract boolean installPackage(String path); + protected abstract boolean removePackage(String packageName); + protected abstract void startActivity(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimetype, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + protected abstract void broadcastIntent(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimetype, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + protected abstract Map<String, Object> instrument(String packageName, + Map<String, Object> args); + protected abstract void wake(); +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java new file mode 100644 index 0000000..c4a5362 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.collect.Maps; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/* + * Custom Logging Formatter for MonkeyRunner that generates all log + * messages on a single line. + */ +public class MonkeyFormatter extends Formatter { + public static final Formatter DEFAULT_INSTANCE = new MonkeyFormatter(); + + private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyMMdd HH:mm:ss.SSS"); + + private static Map<Level, String> LEVEL_TO_STRING_CACHE = Maps.newHashMap(); + + private static final String levelToString(Level level) { + String levelName = LEVEL_TO_STRING_CACHE.get(level); + if (levelName == null) { + levelName = level.getName().substring(0, 1); + LEVEL_TO_STRING_CACHE.put(level, levelName); + } + return levelName; + } + + private static String getHeader(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + sb.append(FORMAT.format(new Date(record.getMillis()))).append(":"); + sb.append(levelToString(record.getLevel())).append(" "); + + sb.append("[").append(Thread.currentThread().getName()).append("] "); + + String loggerName = record.getLoggerName(); + if (loggerName != null) { + sb.append("[").append(loggerName).append("]"); + } + return sb.toString(); + } + + private class PrintWriterWithHeader extends PrintWriter { + private final ByteArrayOutputStream out; + private final String header; + + public PrintWriterWithHeader(String header) { + this(header, new ByteArrayOutputStream()); + } + + public PrintWriterWithHeader(String header, ByteArrayOutputStream out) { + super(out, true); + this.header = header; + this.out = out; + } + + @Override + public void println(Object x) { + print(header); + super.println(x); + } + + @Override + public void println(String x) { + print(header); + super.println(x); + } + + @Override + public String toString() { + return out.toString(); + } + } + + @Override + public String format(LogRecord record) { + Throwable thrown = record.getThrown(); + String header = getHeader(record); + + StringBuilder sb = new StringBuilder(); + sb.append(header); + sb.append(" ").append(formatMessage(record)); + sb.append("\n"); + + // Print the exception here if we caught it + if (thrown != null) { + + PrintWriter pw = new PrintWriterWithHeader(header); + thrown.printStackTrace(pw); + sb.append(pw.toString()); + } + + return sb.toString(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java new file mode 100644 index 0000000..7cff67f --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Preconditions; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import org.python.core.ArgParser; +import org.python.core.PyInteger; +import org.python.core.PyObject; +import org.python.core.PyTuple; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +/** + * Jython object to encapsulate images that have been taken. + */ +public abstract class MonkeyImage { + /** + * Convert the MonkeyImage into a BufferedImage. + * + * @return a BufferedImage for this MonkeyImage. + */ + public abstract BufferedImage createBufferedImage(); + + // Cache the BufferedImage so we don't have to generate it every time. + private WeakReference<BufferedImage> cachedBufferedImage = null; + + /** + * Utility method to handle getting the BufferedImage and managing the cache. + * + * @return the BufferedImage for this image. + */ + private BufferedImage getBufferedImage() { + // Check the cache first + if (cachedBufferedImage != null) { + BufferedImage img = cachedBufferedImage.get(); + if (img != null) { + return img; + } + } + + // Not in the cache, so create it and cache it. + BufferedImage img = createBufferedImage(); + cachedBufferedImage = new WeakReference<BufferedImage>(img); + return img; + } + + @MonkeyRunnerExported(doc = "Encode the image into a format and return the bytes.", + args = {"format"}, + argDocs = { "The (optional) format in which to encode the image (PNG for example)" }, + returns = "A String containing the bytes.") + public byte[] convertToBytes(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String format = ap.getString(0, "png"); + + BufferedImage argb = convertSnapshot(); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ImageIO.write(argb, format, os); + } catch (IOException e) { + return new byte[0]; + } + return os.toByteArray(); + } + + @MonkeyRunnerExported(doc = "Write out the file to the specified location. If no " + + "format is specified, this function tries to guess at the output format " + + "depending on the file extension given. If unable to determine, it uses PNG.", + args = {"path", "format"}, + argDocs = {"Where to write out the file", + "The format in which to encode the image (PNG for example)"}, + returns = "True if writing succeeded.") + public boolean writeToFile(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String path = ap.getString(0); + String format = ap.getString(1, null); + + if (format != null) { + return writeToFile(path, format); + } + int offset = path.lastIndexOf('.'); + if (offset < 0) { + return writeToFile(path, "png"); + } + String ext = path.substring(offset + 1); + Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext); + if (!writers.hasNext()) { + return writeToFile(path, "png"); + } + ImageWriter writer = writers.next(); + BufferedImage image = getBufferedImage(); + try { + File f = new File(path); + f.delete(); + + ImageOutputStream outputStream = ImageIO.createImageOutputStream(f); + writer.setOutput(outputStream); + + try { + writer.write(image); + } finally { + writer.dispose(); + outputStream.flush(); + } + } catch (IOException e) { + return false; + } + return true; + } + + @MonkeyRunnerExported(doc = "Get a single ARGB pixel from the image", + args = { "x", "y" }, + argDocs = { "the x offset of the pixel", "the y offset of the pixel" }, + returns = "A tuple of (A, R, G, B) for the pixel") + public PyObject getRawPixel(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + int x = ap.getInt(0); + int y = ap.getInt(1); + int pixel = getPixel(x, y); + PyInteger a = new PyInteger((pixel & 0xFF000000) >> 24); + PyInteger r = new PyInteger((pixel & 0x00FF0000) >> 16); + PyInteger g = new PyInteger((pixel & 0x0000FF00) >> 8); + PyInteger b = new PyInteger((pixel & 0x000000FF) >> 0); + return new PyTuple(a, r, g ,b); + } + + @MonkeyRunnerExported(doc = "Get a single ARGB pixel from the image", + args = { "x", "y" }, + argDocs = { "the x offset of the pixel", "the y offset of the pixel" }, + returns = "An integer for the ARGB pixel") + public int getRawPixelInt(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + int x = ap.getInt(0); + int y = ap.getInt(1); + return getPixel(x, y); + } + + private int getPixel(int x, int y) { + BufferedImage image = getBufferedImage(); + return image.getRGB(x, y); + } + + private BufferedImage convertSnapshot() { + BufferedImage image = getBufferedImage(); + + // Convert the image to ARGB so ImageIO writes it out nicely + BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics g = argb.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return argb; + } + + public boolean writeToFile(String path, String format) { + BufferedImage argb = convertSnapshot(); + + try { + ImageIO.write(argb, format, new File(path)); + } catch (IOException e) { + return false; + } + return true; + } + + @MonkeyRunnerExported(doc = "Compare this image to the other image.", + args = {"other", "percent"}, + argDocs = {"The other image.", + "A float from 0.0 to 1.0 indicating the percentage " + + "of pixels that need to be the same. Defaults to 1.0"}, + returns = "True if they are the same image.") + public boolean sameAs(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + PyObject otherObject = ap.getPyObject(0); + MonkeyImage other = (MonkeyImage) otherObject.__tojava__(MonkeyImage.class); + + double percent = JythonUtils.getFloat(ap, 1, 1.0); + + BufferedImage otherImage = other.getBufferedImage(); + BufferedImage myImage = getBufferedImage(); + + // Easy size check + if (otherImage.getWidth() != myImage.getWidth()) { + return false; + } + if (otherImage.getHeight() != myImage.getHeight()) { + return false; + } + + int[] otherPixel = new int[1]; + int[] myPixel = new int[1]; + + int width = myImage.getWidth(); + int height = myImage.getHeight(); + + int numDiffPixels = 0; + // Now, go through pixel-by-pixel and check that the images are the same; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { + numDiffPixels++; + } + } + } + double numberPixels = (height * width); + double diffPercent = numDiffPixels / numberPixels; + return percent <= 1.0 - diffPercent; + } + + private static class BufferedImageMonkeyImage extends MonkeyImage { + private final BufferedImage image; + + public BufferedImageMonkeyImage(BufferedImage image) { + this.image = image; + } + + @Override + public BufferedImage createBufferedImage() { + return image; + } + + } + + @MonkeyRunnerExported(doc = "Get a sub-image of this image.", + args = {"rect"}, + argDocs = {"A Tuple of (x, y, w, h) representing the area of the image to extract."}, + returns = "The newly extracted image.") + public MonkeyImage getSubImage(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + PyTuple rect = (PyTuple) ap.getPyObjectByType(0, PyTuple.TYPE); + int x = rect.__getitem__(0).asInt(); + int y = rect.__getitem__(1).asInt(); + int w = rect.__getitem__(2).asInt(); + int h = rect.__getitem__(3).asInt(); + + BufferedImage image = getBufferedImage(); + return new BufferedImageMonkeyImage(image.getSubimage(x, y, w, h)); + } +}
\ No newline at end of file diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java new file mode 100644 index 0000000..11a2dd4 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.collect.Lists; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.util.Collection; +import java.util.Collections; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides a nicer interface to interacting with the low-level network access protocol for talking + * to the monkey. + * + * This class is thread-safe and can handle being called from multiple threads. + */ +public class MonkeyManager { + private static Logger LOG = Logger.getLogger(MonkeyManager.class.getName()); + + private Socket monkeySocket; + private BufferedWriter monkeyWriter; + private BufferedReader monkeyReader; + + /** + * Create a new MonkeyMananger to talk to the specified device. + * + * @param monkeySocket the already connected socket on which to send protocol messages. + */ + public MonkeyManager(Socket monkeySocket) { + try { + this.monkeySocket = monkeySocket; + monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); + monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Send a touch down event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchDown(int x, int y) throws IOException { + return sendMonkeyEvent("touch down " + x + " " + y); + } + + /** + * Send a touch down event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchUp(int x, int y) throws IOException { + return sendMonkeyEvent("touch up " + x + " " + y); + } + + /** + * Send a touch move event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touchMove(int x, int y) throws IOException { + return sendMonkeyEvent("touch move " + x + " " + y); + } + + /** + * Send a touch (down and then up) event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean touch(int x, int y) throws IOException { + return sendMonkeyEvent("tap " + x + " " + y); + } + + /** + * Press a physical button on the device. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean press(String name) throws IOException { + return sendMonkeyEvent("press " + name); + } + + /** + * Send a Key Down event for the specified button. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean keyDown(String name) throws IOException { + return sendMonkeyEvent("key down " + name); + } + + /** + * Send a Key Up event for the specified button. + * + * @param name the name of the button (As specified in the protocol) + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean keyUp(String name) throws IOException { + return sendMonkeyEvent("key up " + name); + } + + /** + * Press a physical button on the device. + * + * @param button the button to press + * @return success or not + * @throws IOException on error communicating with the device + */ + public boolean press(PhysicalButton button) throws IOException { + return press(button.getKeyName()); + } + + /** + * This function allows the communication bridge between the host and the device + * to be invisible to the script for internal needs. + * It splits a command into monkey events and waits for responses for each over an adb tcp socket. + * Returns on an error, else continues and sets up last response. + * + * @param command the monkey command to send to the device + * @return the (unparsed) response returned from the monkey. + */ + private String sendMonkeyEventAndGetResponse(String command) throws IOException { + command = command.trim(); + LOG.info("Monkey Command: " + command + "."); + + // send a single command and get the response + monkeyWriter.write(command + "\n"); + monkeyWriter.flush(); + return monkeyReader.readLine(); + } + + /** + * Parse a monkey response string to see if the command succeeded or not. + * + * @param monkeyResponse the response + * @return true if response code indicated success. + */ + private boolean parseResponseForSuccess(String monkeyResponse) { + if (monkeyResponse == null) { + return false; + } + // return on ok + if(monkeyResponse.startsWith("OK")) { + return true; + } + + return false; + } + + /** + * Parse a monkey response string to get the extra data returned. + * + * @param monkeyResponse the response + * @return any extra data that was returned, or empty string if there was nothing. + */ + private String parseResponseForExtra(String monkeyResponse) { + int offset = monkeyResponse.indexOf(':'); + if (offset < 0) { + return ""; + } + return monkeyResponse.substring(offset + 1); + } + + /** + * This function allows the communication bridge between the host and the device + * to be invisible to the script for internal needs. + * It splits a command into monkey events and waits for responses for each over an + * adb tcp socket. + * + * @param command the monkey command to send to the device + * @return true on success. + */ + private boolean sendMonkeyEvent(String command) throws IOException { + synchronized (this) { + String monkeyResponse = sendMonkeyEventAndGetResponse(command); + return parseResponseForSuccess(monkeyResponse); + } + } + + /** + * Close all open resources related to this device. + */ + public void close() { + try { + monkeySocket.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeySocket", e); + } + try { + monkeyReader.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeyReader", e); + } + try { + monkeyWriter.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e); + } + } + + /** + * Function to get a static variable from the device. + * + * @param name name of static variable to get + * @return the value of the variable, or null if there was an error + */ + public String getVariable(String name) throws IOException { + synchronized (this) { + String response = sendMonkeyEventAndGetResponse("getvar " + name); + if (!parseResponseForSuccess(response)) { + return null; + } + return parseResponseForExtra(response); + } + } + + /** + * Function to get the list of static variables from the device. + */ + public Collection<String> listVariable() throws IOException { + synchronized (this) { + String response = sendMonkeyEventAndGetResponse("listvar"); + if (!parseResponseForSuccess(response)) { + Collections.emptyList(); + } + String extras = parseResponseForExtra(response); + return Lists.newArrayList(extras.split(" ")); + } + } + + /** + * Tells the monkey that we are done for this session. + * @throws IOException + */ + public void done() throws IOException { + // this command just drops the connection, so handle it here + synchronized (this) { + sendMonkeyEventAndGetResponse("done"); + } + } + + /** + * Tells the monkey that we are done forever. + * @throws IOException + */ + public void quit() throws IOException { + // this command drops the connection, so handle it here + synchronized (this) { + sendMonkeyEventAndGetResponse("quit"); + } + } + + /** + * Send a tap event at the specified location. + * + * @param x the x coordinate of where to click + * @param y the y coordinate of where to click + * @return success or not + * @throws IOException + * @throws IOException on error communicating with the device + */ + public boolean tap(int x, int y) throws IOException { + return sendMonkeyEvent("tap " + x + " " + y); + } + + /** + * Type the following string to the monkey. + * + * @param text the string to type + * @return success + * @throws IOException + */ + public boolean type(String text) throws IOException { + // The network protocol can't handle embedded line breaks, so we have to handle it + // here instead + StringTokenizer tok = new StringTokenizer(text, "\n", true); + while (tok.hasMoreTokens()) { + String line = tok.nextToken(); + if ("\n".equals(line)) { + boolean success = press(PhysicalButton.ENTER); + if (!success) { + return false; + } + } else { + boolean success = sendMonkeyEvent("type " + line); + if (!success) { + return false; + } + } + } + return true; + } + + /** + * Type the character to the monkey. + * + * @param keyChar the character to type. + * @return success + * @throws IOException + */ + public boolean type(char keyChar) throws IOException { + return type(Character.toString(keyChar)); + } + + /** + * Wake the device up from sleep. + * @throws IOException + */ + public void wake() throws IOException { + sendMonkeyEvent("wake"); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java new file mode 100644 index 0000000..cdab926 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import org.python.core.ArgParser; +import org.python.core.PyException; +import org.python.core.PyObject; + +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.JOptionPane; + +/** + * This is the main interface class into the jython bindings. + */ +public class MonkeyRunner { + private static final Logger LOG = Logger.getLogger(MonkeyRunner.class.getCanonicalName()); + private static MonkeyRunnerBackend backend; + + /** + * Set the backend MonkeyRunner is using. + * + * @param backend the backend to use. + */ + /* package */ static void setBackend(MonkeyRunnerBackend backend) { + MonkeyRunner.backend = backend; + } + + @MonkeyRunnerExported(doc = "Wait for the specified device to connect.", + args = {"timeout", "deviceId"}, + argDocs = {"The timeout in seconds to wait for the device to connect. (default " + + "is to wait forever)", + "A regular expression that specifies the device of for valid devices" + + " to wait for."}, + returns = "A MonkeyDevice representing the connected device.") + public static MonkeyDevice waitForConnection(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + long timeoutMs; + try { + double timeoutInSecs = JythonUtils.getFloat(ap, 0); + timeoutMs = (long) (timeoutInSecs * 1000.0); + } catch (PyException e) { + timeoutMs = Long.MAX_VALUE; + } + + return backend.waitForConnection(timeoutMs, + ap.getString(1, ".*")); + } + + @MonkeyRunnerExported(doc = "Pause script processing for the specified number of seconds", + args = {"seconds"}, + argDocs = {"The number of seconds to pause processing"}) + public static void sleep(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + double seconds = JythonUtils.getFloat(ap, 0); + + long ms = (long) (seconds * 1000.0); + + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + } + + @MonkeyRunnerExported(doc = "Simple help command to dump the MonkeyRunner supported " + + "commands", + args = { "format" }, + argDocs = {"The format to return the help text in. (default is text)"}, + returns = "The help text") + public static String help(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String format = ap.getString(0, "text"); + + return MonkeyRunnerHelp.helpString(format); + } + + @MonkeyRunnerExported(doc = "Put up an alert dialog to inform the user of something that " + + "happened. This is modal dialog and will stop processing of " + + "the script until the user acknowledges the alert message", + args = { "message", "title", "okTitle" }, + argDocs = { + "The contents of the message of the dialog box", + "The title to display for the dialog box. (default value is \"Alert\")", + "The title to use for the acknowledgement button (default value is \"OK\")" + }) + public static void alert(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String message = ap.getString(0); + String title = ap.getString(1, "Alert"); + String buttonTitle = ap.getString(2, "OK"); + + alert(message, title, buttonTitle); + } + + @MonkeyRunnerExported(doc = "Put up an input dialog that allows the user to input a string." + + " This is a modal dialog that will stop processing of the script until the user " + + "inputs the requested information.", + args = {"message", "initialValue", "title", "okTitle", "cancelTitle"}, + argDocs = { + "The message to display for the input.", + "The initial value to supply the user (default is empty string)", + "The title of the dialog box to display. (default is \"Input\")" + }, + returns = "The test entered by the user, or None if the user canceled the input;" + ) + public static String input(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String message = ap.getString(0); + String initialValue = ap.getString(1, ""); + String title = ap.getString(2, "Input"); + + return input(message, initialValue, title); + } + + @MonkeyRunnerExported(doc = "Put up a choice dialog that allows the user to select a single " + + "item from a list of items that were presented.", + args = {"message", "choices", "title"}, + argDocs = { + "The message to display for the input.", + "The list of choices to display.", + "The title of the dialog box to display. (default is \"Input\")" }, + returns = "The numeric offset of the choice selected.") + public static int choice(PyObject[] args, String kws[]) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + + String message = ap.getString(0); + Collection<String> choices = Collections2.transform(JythonUtils.getList(ap, 1), + Functions.toStringFunction()); + String title = ap.getString(2, "Input"); + + return choice(message, title, choices); + } + + /** + * Display an alert dialog. + * + * @param message the message to show. + * @param title the title of the dialog box. + * @param okTitle the title of the button. + */ + private static void alert(String message, String title, String okTitle) { + Object[] options = { okTitle }; + JOptionPane.showOptionDialog(null, message, title, JOptionPane.DEFAULT_OPTION, + JOptionPane.INFORMATION_MESSAGE, null, options, options[0]); + } + + /** + * Display a dialog allow the user to pick a choice from a list of choices. + * + * @param message the message to show. + * @param title the title of the dialog box. + * @param choices the list of the choices to display. + * @return the index of the selected choice, or -1 if nothing was chosen. + */ + private static int choice(String message, String title, Collection<String> choices) { + Object[] possibleValues = choices.toArray(); + Object selectedValue = JOptionPane.showInputDialog(null, message, title, + JOptionPane.QUESTION_MESSAGE, null, possibleValues, possibleValues[0]); + + for (int x = 0; x < possibleValues.length; x++) { + if (possibleValues[x].equals(selectedValue)) { + return x; + } + } + // Error + return -1; + } + + /** + * Display a dialog that allows the user to input a text string. + * + * @param message the message to show. + * @param initialValue the initial value to display in the dialog + * @param title the title of the dialog box. + * @return the entered string, or null if cancelled + */ + private static String input(String message, String initialValue, String title) { + return (String) JOptionPane.showInputDialog(null, message, title, + JOptionPane.QUESTION_MESSAGE, null, null, initialValue); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java new file mode 100644 index 0000000..216d214 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +/** + * Interface between MonkeyRunner common code and the MonkeyRunner backend. The backend is + * responsible for communicating between the host and the device. + */ +public interface MonkeyRunnerBackend { + /** + * Wait for a device to connect to the backend. + * + * @param timeoutMs how long (in ms) to wait + * @param deviceIdRegex the regular expression to specify which device to wait for. + * @return the connected device (or null if timeout); + */ + MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex); + + /** + * Shutdown the backend and cleanup any resources it was using. + */ + void shutdown(); +}
\ No newline at end of file diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java new file mode 100644 index 0000000..8dbe85b --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.Resources; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import org.clearsilver.CS; +import org.clearsilver.CSFileLoader; +import org.clearsilver.HDF; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +/** + * Utility class for generating inline help documentation + */ +public final class MonkeyRunnerHelp { + private MonkeyRunnerHelp() { } + + private static final String HELP = "help"; + private static final String NAME = "name"; + private static final String DOC = "doc"; + private static final String ARGUMENT = "argument"; + private static final String RETURNS = "returns"; + private static final String TYPE = "type"; + + // Enum used to describe documented types. + private enum Type { + ENUM, FIELD, METHOD + } + + private static void getAllExportedClasses(Set<Field> fields, + Set<Method> methods, + Set<Constructor<?>> constructors, + Set<Class<?>> enums) { + final Set<Class<?>> classesVisited = Sets.newHashSet(); + Set<Class<?>> classesToVisit = Sets.newHashSet(); + classesToVisit.add(MonkeyRunner.class); + + Predicate<Class<?>> haventSeen = new Predicate<Class<?>>() { + public boolean apply(Class<?> clz) { + return !classesVisited.contains(clz); + } + }; + + while (!classesToVisit.isEmpty()) { + classesVisited.addAll(classesToVisit); + + List<Class<?>> newClasses = Lists.newArrayList(); + for (Class<?> clz : classesToVisit) { + // See if the class itself is annotated and is an enum + if (clz.isEnum() && clz.isAnnotationPresent(MonkeyRunnerExported.class)) { + enums.add(clz); + } + + // Constructors + for (Constructor<?> c : clz.getConstructors()) { + newClasses.addAll(Collections2.filter(Arrays.asList(c.getParameterTypes()), + haventSeen)); + if (c.isAnnotationPresent(MonkeyRunnerExported.class)) { + constructors.add(c); + } + } + + // Fields + for (Field f : clz.getFields()) { + if (haventSeen.apply(f.getClass())) { + newClasses.add(f.getClass()); + } + if (f.isAnnotationPresent(MonkeyRunnerExported.class)) { + fields.add(f); + } + } + + // Methods + for (Method m : clz.getMethods()) { + newClasses.addAll(Collections2.filter(Arrays.asList(m.getParameterTypes()), + haventSeen)); + if (haventSeen.apply(m.getReturnType())) { + newClasses.add(m.getReturnType()); + } + + if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { + methods.add(m); + } + } + + // Containing classes + for (Class<?> toAdd : clz.getClasses()) { + if (haventSeen.apply(toAdd)) { + newClasses.add(toAdd); + } + } + } + + classesToVisit.clear(); + classesToVisit.addAll(newClasses); + } + } + + private static Comparator<Member> MEMBER_SORTER = new Comparator<Member>() { + public int compare(Member o1, Member o2) { + return o1.getName().compareTo(o2.getName()); + } + }; + + private static Comparator<Class<?>> CLASS_SORTER = new Comparator<Class<?>>() { + public int compare(Class<?> o1, Class<?> o2) { + return o1.getName().compareTo(o2.getName()); + } + }; + + public static String helpString(String format) { + // Quick check for support formats + if ("html".equals(format) || "text".equals(format)) { + HDF hdf = buildHelpHdf(); + CS clearsilver = new CS(hdf); + // Set a custom file loader to load requested files from resources relative to this class. + clearsilver.setFileLoader(new CSFileLoader() { + public String load(HDF hdf, String filename) throws IOException { + return Resources.toString(Resources.getResource(MonkeyRunnerHelp.class, filename), + Charset.defaultCharset()); + } + }); + + // Load up the CS template file + clearsilver.parseFile(format.toLowerCase() + ".cs"); + // And render the output + return clearsilver.render(); + } else if ("hdf".equals(format)) { + HDF hdf = buildHelpHdf(); + return hdf.writeString(); + } + return ""; + } + + private static HDF buildHelpHdf() { + HDF hdf = new HDF(); + + int outputItemCount = 0; + + Set<Field> fields = Sets.newTreeSet(MEMBER_SORTER); + Set<Method> methods = Sets.newTreeSet(MEMBER_SORTER); + Set<Constructor<?>> constructors = Sets.newTreeSet(MEMBER_SORTER); + Set<Class<?>> classes = Sets.newTreeSet(CLASS_SORTER); + getAllExportedClasses(fields, methods, constructors, classes); + + for (Class<?> clz : classes) { + String prefix = HELP + "." + outputItemCount + "."; + + hdf.setValue(prefix + NAME, clz.getCanonicalName()); + MonkeyRunnerExported annotation = clz.getAnnotation(MonkeyRunnerExported.class); + hdf.setValue(prefix + DOC, annotation.doc()); + hdf.setValue(prefix + TYPE, Type.ENUM.name()); + + // Now go through the enumeration constants + Object[] constants = clz.getEnumConstants(); + String[] argDocs = annotation.argDocs(); + if (constants.length > 0) { + for (int x = 0; x < constants.length; x++) { + String argPrefix = prefix + ARGUMENT + "." + x + "."; + hdf.setValue(argPrefix + NAME, constants[x].toString()); + if (argDocs.length > x) { + hdf.setValue(argPrefix + DOC, argDocs[x]); + } + } + } + outputItemCount++; + } + + for (Method m : methods) { + String prefix = HELP + "." + outputItemCount + "."; + + MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); + String className = m.getDeclaringClass().getCanonicalName(); + String methodName = className + "." + m.getName(); + hdf.setValue(prefix + NAME, methodName); + hdf.setValue(prefix + DOC, annotation.doc()); + if (annotation.args().length > 0) { + String[] argDocs = annotation.argDocs(); + String[] aargs = annotation.args(); + for (int x = 0; x < aargs.length; x++) { + String argPrefix = prefix + ARGUMENT + "." + x + "."; + + hdf.setValue(argPrefix + NAME, aargs[x]); + if (argDocs.length > x) { + hdf.setValue(argPrefix + DOC, argDocs[x]); + } + } + } + if (!"".equals(annotation.returns())) { + hdf.setValue(prefix + RETURNS, annotation.returns()); + } + outputItemCount++; + } + + return hdf; + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java new file mode 100644 index 0000000..cf193c2 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.collect.ImmutableList; + +import java.io.File; +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MonkeyRunnerOptions { + private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName()); + private static String DEFAULT_MONKEY_SERVER_ADDRESS = "127.0.0.1"; + private static int DEFAULT_MONKEY_PORT = 12345; + + private final int port; + private final String hostname; + private final File scriptFile; + private final String backend; + private final Collection<File> plugins; + private final Collection<String> arguments; + + private MonkeyRunnerOptions(String hostname, int port, File scriptFile, String backend, + Collection<File> plugins, Collection<String> arguments) { + this.hostname = hostname; + this.port = port; + this.scriptFile = scriptFile; + this.backend = backend; + this.plugins = plugins; + this.arguments = arguments; + } + + public int getPort() { + return port; + } + + public String getHostname() { + return hostname; + } + + public File getScriptFile() { + return scriptFile; + } + + public String getBackendName() { + return backend; + } + + public Collection<File> getPlugins() { + return plugins; + } + + public Collection<String> getArguments() { + return arguments; + } + + private static void printUsage(String message) { + System.out.println(message); + System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE"); + System.out.println(""); + System.out.println(" -s MonkeyServer IP Address."); + System.out.println(" -p MonkeyServer TCP Port."); + System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)"); + System.out.println(""); + System.out.println(""); + } + + /** + * Process the command-line options + * + * @return the parsed options, or null if there was an error. + */ + public static MonkeyRunnerOptions processOptions(String[] args) { + // parse command line parameters. + int index = 0; + + String hostname = DEFAULT_MONKEY_SERVER_ADDRESS; + File scriptFile = null; + int port = DEFAULT_MONKEY_PORT; + String backend = "adb"; + + ImmutableList.Builder<File> pluginListBuilder = ImmutableList.builder(); + ImmutableList.Builder<String> argumentBuilder = ImmutableList.builder(); + while (index < args.length) { + String argument = args[index++]; + + if ("-s".equals(argument)) { + if (index == args.length) { + printUsage("Missing Server after -s"); + return null; + } + hostname = args[index++]; + + } else if ("-p".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + printUsage("Missing Server port after -p"); + return null; + } + port = Integer.parseInt(args[index++]); + + } else if ("-v".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + printUsage("Missing Log Level after -v"); + return null; + } + + Level level = Level.parse(args[index++]); + LOG.setLevel(level); + level = LOG.getLevel(); + System.out.println("Log level set to: " + level + "(" + level.intValue() + ")."); + System.out.println("Warning: Log levels below INFO(800) not working currently... parent issues"); + } else if ("-be".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + printUsage("Missing backend name after -be"); + return null; + } + backend = args[index++]; + } else if ("-plugin".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + printUsage("Missing plugin path after -plugin"); + return null; + } + File plugin = new File(args[index++]); + if (!plugin.exists()) { + printUsage("Plugin file doesn't exist"); + return null; + } + + if (!plugin.canRead()) { + printUsage("Can't read plugin file"); + return null; + } + + pluginListBuilder.add(plugin); + } else if (argument.startsWith("-") && + // Once we have the scriptfile, the rest of the arguments go to jython. + scriptFile == null) { + // we have an unrecognized argument. + printUsage("Unrecognized argument: " + argument + "."); + return null; + } else { + if (scriptFile == null) { + // get the filepath of the script to run. This will be the last undashed argument. + scriptFile = new File(argument); + if (!scriptFile.exists()) { + printUsage("Can't open specified script file"); + return null; + } + if (!scriptFile.canRead()) { + printUsage("Can't open specified script file"); + return null; + } + } else { + argumentBuilder.add(argument); + } + } + }; + + return new MonkeyRunnerOptions(hostname, port, scriptFile, backend, + pluginListBuilder.build(), argumentBuilder.build()); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java new file mode 100644 index 0000000..1f539ba --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; + +import com.android.monkeyrunner.adb.AdbBackend; +import com.android.monkeyrunner.stub.StubBackend; + +import org.python.util.PythonInterpreter; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * MonkeyRunner is a host side application to control a monkey instance on a + * device. MonkeyRunner provides some useful helper functions to control the + * device as well as various other methods to help script tests. This class bootstraps + * MonkeyRunner. + */ +public class MonkeyRunnerStarter { + private static final Logger LOG = Logger.getLogger(MonkeyRunnerStarter.class.getName()); + private static final String MONKEY_RUNNER_MAIN_MANIFEST_NAME = "MonkeyRunnerStartupRunner"; + + private final MonkeyRunnerBackend backend; + private final MonkeyRunnerOptions options; + + public MonkeyRunnerStarter(MonkeyRunnerOptions options) { + this.options = options; + this.backend = MonkeyRunnerStarter.createBackendByName(options.getBackendName()); + if (this.backend == null) { + throw new RuntimeException("Unknown backend"); + } + } + + + /** + * Creates a specific backend by name. + * + * @param backendName the name of the backend to create + * @return the new backend, or null if none were found. + */ + public static MonkeyRunnerBackend createBackendByName(String backendName) { + if ("adb".equals(backendName)) { + return new AdbBackend(); + } else if ("stub".equals(backendName)) { + return new StubBackend(); + } else { + return null; + } + } + + private int run() { + MonkeyRunner.setBackend(backend); + Map<String, Predicate<PythonInterpreter>> plugins = handlePlugins(); + if (options.getScriptFile() == null) { + ScriptRunner.console(); + return 0; + } else { + int error = ScriptRunner.run(options.getScriptFile().getAbsolutePath(), + options.getArguments(), plugins); + backend.shutdown(); + MonkeyRunner.setBackend(null); + return error; + } + } + + private Predicate<PythonInterpreter> handlePlugin(File f) { + JarFile jarFile; + try { + jarFile = new JarFile(f); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to open plugin file. Is it a jar file? " + + f.getAbsolutePath(), e); + return Predicates.alwaysFalse(); + } + Manifest manifest; + try { + manifest = jarFile.getManifest(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to get manifest file from jar: " + + f.getAbsolutePath(), e); + return Predicates.alwaysFalse(); + } + Attributes mainAttributes = manifest.getMainAttributes(); + String pluginClass = mainAttributes.getValue(MONKEY_RUNNER_MAIN_MANIFEST_NAME); + if (pluginClass == null) { + // No main in this plugin, so it always succeeds. + return Predicates.alwaysTrue(); + } + URL url; + try { + url = f.toURI().toURL(); + } catch (MalformedURLException e) { + LOG.log(Level.SEVERE, "Unable to convert file to url " + f.getAbsolutePath(), + e); + return Predicates.alwaysFalse(); + } + URLClassLoader classLoader = new URLClassLoader(new URL[] { url }, + ClassLoader.getSystemClassLoader()); + Class<?> clz; + try { + clz = Class.forName(pluginClass, true, classLoader); + } catch (ClassNotFoundException e) { + LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e); + return Predicates.alwaysFalse(); + } + Object loadedObject; + try { + loadedObject = clz.newInstance(); + } catch (InstantiationException e) { + LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e); + return Predicates.alwaysFalse(); + } catch (IllegalAccessException e) { + LOG.log(Level.SEVERE, "Unable to load the specified plugin " + + "(did you make it public?): " + pluginClass, e); + return Predicates.alwaysFalse(); + } + // Cast it to the right type + if (loadedObject instanceof Runnable) { + final Runnable run = (Runnable) loadedObject; + return new Predicate<PythonInterpreter>() { + public boolean apply(PythonInterpreter i) { + run.run(); + return true; + } + }; + } else if (loadedObject instanceof Predicate<?>) { + return (Predicate<PythonInterpreter>) loadedObject; + } else { + LOG.severe("Unable to coerce object into correct type: " + pluginClass); + return Predicates.alwaysFalse(); + } + } + + private Map<String, Predicate<PythonInterpreter>> handlePlugins() { + ImmutableMap.Builder<String, Predicate<PythonInterpreter>> builder = ImmutableMap.builder(); + for (File f : options.getPlugins()) { + builder.put(f.getAbsolutePath(), handlePlugin(f)); + } + return builder.build(); + } + + + + private static final void replaceAllLogFormatters(Formatter form) { + LogManager mgr = LogManager.getLogManager(); + Enumeration<String> loggerNames = mgr.getLoggerNames(); + while (loggerNames.hasMoreElements()) { + String loggerName = loggerNames.nextElement(); + Logger logger = mgr.getLogger(loggerName); + for (Handler handler : logger.getHandlers()) { + handler.setFormatter(form); + handler.setLevel(Level.INFO); + } + } + } + + public static void main(String[] args) { + MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args); + + // logging property files are difficult + replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE); + + if (options == null) { + return; + } + + MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options); + int error = runner.run(); + + // This will kill any background threads as well. + System.exit(error); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java b/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java new file mode 100644 index 0000000..f0525a0 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +public enum PhysicalButton { + HOME("home"), + SEARCH("search"), + MENU("menu"), + BACK("back"), + DPAD_UP("DPAD_UP"), + DPAD_DOWN("DPAD_DOWN"), + DPAD_LEFT("DPAD_LEFT"), + DPAD_RIGHT("DPAD_RIGHT"), + DPAD_CENTER("DPAD_CENTER"), + ENTER("enter"); + + private String keyName; + + private PhysicalButton(String keyName) { + this.keyName = keyName; + } + + public String getKeyName() { + return keyName; + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java b/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java new file mode 100644 index 0000000..c247a5f --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.ImmutableMap.Builder; + +import org.python.core.Py; +import org.python.core.PyException; +import org.python.core.PyObject; +import org.python.util.InteractiveConsole; +import org.python.util.JLineConsole; +import org.python.util.PythonInterpreter; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Runs Jython based scripts. + */ +public class ScriptRunner { + private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName()); + + /** The "this" scope object for scripts. */ + private final Object scope; + private final String variable; + + /** Private constructor. */ + private ScriptRunner(Object scope, String variable) { + this.scope = scope; + this.variable = variable; + } + + /** Creates a new instance for the given scope object. */ + public static ScriptRunner newInstance(Object scope, String variable) { + return new ScriptRunner(scope, variable); + } + + /** + * Runs the specified Jython script. First runs the initialization script to + * preload the appropriate client library version. + * + * @param scriptfilename the name of the file to run. + * @param args the arguments passed in (excluding the filename). + * @param plugins a list of plugins to load. + * @return the error code from running the script. + */ + public static int run(String scriptfilename, Collection<String> args, + Map<String, Predicate<PythonInterpreter>> plugins) { + // Add the current directory of the script to the python.path search path. + File f = new File(scriptfilename); + + // Adjust the classpath so jython can access the classes in the specified classpath. + Collection<String> classpath = Lists.newArrayList(f.getParent()); + classpath.addAll(plugins.keySet()); + + String[] argv = new String[args.size() + 1]; + argv[0] = f.getAbsolutePath(); + int x = 1; + for (String arg : args) { + argv[x++] = arg; + } + + initPython(classpath, argv); + + PythonInterpreter python = new PythonInterpreter(); + + // Now let the mains run. + for (Map.Entry<String, Predicate<PythonInterpreter>> entry : plugins.entrySet()) { + boolean success; + try { + success = entry.getValue().apply(python); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Plugin Main through an exception.", e); + continue; + } + if (!success) { + LOG.severe("Plugin Main returned error for: " + entry.getKey()); + } + } + + // Bind __name__ to __main__ so mains will run + python.set("__name__", "__main__"); + + try { + python.execfile(scriptfilename); + } catch (PyException e) { + if (Py.SystemExit.equals(e.type)) { + // Then recover the error code so we can pass it on + return (Integer) e.value.__tojava__(Integer.class); + } + // Then some other kind of exception was thrown. Log it and return error; + LOG.log(Level.SEVERE, "Script terminated due to an exception", e); + return 1; + } + return 0; + } + + public static void runString(String script) { + initPython(); + PythonInterpreter python = new PythonInterpreter(); + python.exec(script); + } + + public static Map<String, PyObject> runStringAndGet(String script, String... names) { + return runStringAndGet(script, Arrays.asList(names)); + } + + public static Map<String, PyObject> runStringAndGet(String script, Collection<String> names) { + initPython(); + final PythonInterpreter python = new PythonInterpreter(); + python.exec(script); + + Builder<String, PyObject> builder = ImmutableMap.builder(); + for (String name : names) { + builder.put(name, python.get(name)); + } + return builder.build(); + } + + private static void initPython() { + List<String> arg = Collections.emptyList(); + initPython(arg, new String[] {""}); + } + + private static void initPython(Collection<String> pythonPath, + String[] argv) { + Properties props = new Properties(); + + // Build up the python.path + StringBuilder sb = new StringBuilder(); + sb.append(System.getProperty("java.class.path")); + for (String p : pythonPath) { + sb.append(":").append(p); + } + props.setProperty("python.path", sb.toString()); + + /** Initialize the python interpreter. */ + // Default is 'message' which displays sys-package-mgr bloat + // Choose one of error,warning,message,comment,debug + props.setProperty("python.verbose", "error"); + + PythonInterpreter.initialize(System.getProperties(), props, argv); + } + + /** + * Start an interactive python interpreter. + */ + public static void console() { + initPython(); + InteractiveConsole python = new JLineConsole(); + python.interact(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java new file mode 100644 index 0000000..63badf5 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.google.common.collect.Lists; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.MonkeyRunnerBackend; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * Backend implementation that works over ADB to talk to the device. + */ +public class AdbBackend implements MonkeyRunnerBackend { + private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName()); + // How long to wait each time we check for the device to be connected. + private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200; + private final List<AdbMonkeyDevice> devices = Lists.newArrayList(); + + private final AndroidDebugBridge bridge; + + public AdbBackend() { + AndroidDebugBridge.init(false /* debugger support */); + + bridge = AndroidDebugBridge.createBridge( + "adb", true /* forceNewBridge */); + } + + /** + * Checks the attached devices looking for one whose device id matches the specified regex. + * + * @param deviceIdRegex the regular expression to match against + * @return the Device (if found), or null (if not found). + */ + private IDevice findAttacedDevice(String deviceIdRegex) { + Pattern pattern = Pattern.compile(deviceIdRegex); + for (IDevice device : bridge.getDevices()) { + String serialNumber = device.getSerialNumber(); + if (pattern.matcher(serialNumber).matches()) { + return device; + } + } + return null; + } + + public MonkeyDevice waitForConnection() { + return waitForConnection(Integer.MAX_VALUE, ".*"); + } + + public MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex) { + do { + IDevice device = findAttacedDevice(deviceIdRegex); + if (device != null) { + AdbMonkeyDevice amd = new AdbMonkeyDevice(device); + devices.add(amd); + return amd; + } + + try { + Thread.sleep(CONNECTION_ITERATION_TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + timeoutMs -= CONNECTION_ITERATION_TIMEOUT_MS; + } while (timeoutMs > 0); + + // Timeout. Give up. + return null; + } + + public void shutdown() { + for (AdbMonkeyDevice device : devices) { + device.dispose(); + } + AndroidDebugBridge.terminate(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java new file mode 100644 index 0000000..dedc1ea --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.InstallException; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.MonkeyImage; +import com.android.monkeyrunner.MonkeyManager; +import com.android.monkeyrunner.adb.LinearInterpolator.Point; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +public class AdbMonkeyDevice extends MonkeyDevice { + private static final Logger LOG = Logger.getLogger(AdbMonkeyDevice.class.getName()); + + private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0]; + private static final long MANAGER_CREATE_TIMEOUT_MS = 5 * 1000; // 5 seconds + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final IDevice device; + private MonkeyManager manager; + + public AdbMonkeyDevice(IDevice device) { + this.device = device; + this.manager = createManager("127.0.0.1", 12345); + + Preconditions.checkNotNull(this.manager); + } + + @Override + public MonkeyManager getManager() { + return manager; + } + + @Override + public void dispose() { + try { + manager.quit(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error getting the manager to quit", e); + } + manager = null; + } + + private void executeAsyncCommand(final String command, + final LoggingOutputReceiver logger) { + executor.submit(new Runnable() { + public void run() { + try { + device.executeShellCommand(command, logger); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (ShellCommandUnresponsiveException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error starting command: " + command, e); + throw new RuntimeException(e); + } + } + }); + } + + private MonkeyManager createManager(String address, int port) { + try { + device.createForward(port, port); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e); + return null; + } + + String command = "monkey --port " + port; + executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE)); + + // Sleep for a second to give the command time to execute. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Unable to sleep", e); + } + + InetAddress addr; + try { + addr = InetAddress.getByName(address); + } catch (UnknownHostException e) { + LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e); + return null; + } + + // We have a tough problem to solve here. "monkey" on the device gives us no indication + // when it has started up and is ready to serve traffic. If you try too soon, commands + // will fail. To remedy this, we will keep trying until a single command (in this case, + // wake) succeeds. + boolean success = false; + MonkeyManager mm = null; + long start = System.currentTimeMillis(); + + while (!success) { + long now = System.currentTimeMillis(); + long diff = now - start; + if (diff > MANAGER_CREATE_TIMEOUT_MS) { + LOG.severe("Timeout while trying to create monkey mananger"); + return null; + } + + Socket monkeySocket; + try { + monkeySocket = new Socket(addr, port); + } catch (IOException e) { + LOG.log(Level.FINE, "Unable to connect socket", e); + success = false; + continue; + } + + mm = new MonkeyManager(monkeySocket); + + try { + mm.wake(); + } catch (IOException e) { + LOG.log(Level.FINE, "Unable to wake up device", e); + success = false; + continue; + } + success = true; + } + + return mm; + } + + @Override + public MonkeyImage takeSnapshot() { + try { + return new AdbMonkeyImage(device.getScreenshot()); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to take snapshot", e); + return null; + } + } + + @Override + protected String getSystemProperty(String key) { + return device.getProperty(key); + } + + @Override + protected String getProperty(String key) { + try { + return manager.getVariable(key); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to get variable: " + key, e); + return null; + } + } + + @Override + protected void wake() { + try { + manager.wake(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e); + } + } + + private String shell(String... args) { + StringBuilder cmd = new StringBuilder(); + for (String arg : args) { + cmd.append(arg).append(" "); + } + return shell(cmd.toString()); + } + + @Override + protected String shell(String cmd) { + CommandOutputCapture capture = new CommandOutputCapture(); + try { + device.executeShellCommand(cmd, capture); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (ShellCommandUnresponsiveException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); + return null; + } + return capture.toString(); + } + + @Override + protected boolean installPackage(String path) { + try { + String result = device.installPackage(path, true); + if (result != null) { + LOG.log(Level.SEVERE, "Got error installing package: "+ result); + return false; + } + return true; + } catch (InstallException e) { + LOG.log(Level.SEVERE, "Error installing package: " + path, e); + return false; + } + } + + @Override + protected boolean removePackage(String packageName) { + try { + String result = device.uninstallPackage(packageName); + if (result != null) { + LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " + + result); + return false; + } + return true; + } catch (InstallException e) { + LOG.log(Level.SEVERE, "Error installing package: " + packageName, e); + return false; + } + } + + @Override + protected void press(String keyName, TouchPressType type) { + try { + switch (type) { + case DOWN_AND_UP: + manager.press(keyName); + break; + case DOWN: + manager.keyDown(keyName); + break; + case UP: + manager.keyUp(keyName); + break; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e); + } + } + + @Override + protected void type(String string) { + try { + manager.type(string); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error Typing: " + string, e); + } + } + + @Override + protected void touch(int x, int y, TouchPressType type) { + try { + switch (type) { + case DOWN: + manager.touchDown(x, y); + break; + case UP: + manager.touchUp(x, y); + break; + case DOWN_AND_UP: + manager.tap(x, y); + break; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e); + } + } + + @Override + protected void reboot(String into) { + try { + device.reboot(into); + } catch (TimeoutException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } catch (AdbCommandRejectedException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Unable to reboot device", e); + } + } + + @Override + protected void startActivity(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, + extras, component, flags); + shell(Lists.asList("am", "start", + intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); + } + + @Override + protected void broadcastIntent(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, + extras, component, flags); + shell(Lists.asList("am", "broadcast", + intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); + } + + private static boolean isNullOrEmpty(@Nullable String string) { + return string == null || string.length() == 0; + } + + private List<String> buildIntentArgString(String uri, String action, String data, String mimetype, + Collection<String> categories, Map<String, Object> extras, String component, + int flags) { + List<String> parts = Lists.newArrayList(); + + // from adb docs: + //<INTENT> specifications include these flags: + // [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>] + // [-c <CATEGORY> [-c <CATEGORY>] ...] + // [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...] + // [--esn <EXTRA_KEY> ...] + // [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...] + // [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...] + // [-n <COMPONENT>] [-f <FLAGS>] + // [<URI>] + + if (!isNullOrEmpty(action)) { + parts.add("-a"); + parts.add(action); + } + + if (!isNullOrEmpty(data)) { + parts.add("-d"); + parts.add(data); + } + + if (!isNullOrEmpty(mimetype)) { + parts.add("-t"); + parts.add(mimetype); + } + + // Handle categories + for (String category : categories) { + parts.add("-c"); + parts.add(category); + } + + // Handle extras + for (Entry<String, Object> entry : extras.entrySet()) { + // Extras are either boolean, string, or int. See which we have + Object value = entry.getValue(); + String valueString; + String arg; + if (value instanceof Integer) { + valueString = Integer.toString((Integer) value); + arg = "--ei"; + } else if (value instanceof Boolean) { + valueString = Boolean.toString((Boolean) value); + arg = "--ez"; + } else { + // treat is as a string. + valueString = value.toString(); + arg = "--esmake"; + } + parts.add(arg); + parts.add(valueString); + } + + if (!isNullOrEmpty(component)) { + parts.add("-n"); + parts.add(component); + } + + if (flags != 0) { + parts.add("-f"); + parts.add(Integer.toString(flags)); + } + + if (!isNullOrEmpty(uri)) { + parts.add(uri); + } + + return parts; + } + + @Override + protected Map<String, Object> instrument(String packageName, Map<String, Object> args) { + List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r", packageName); + String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY)); + return convertInstrumentResult(result); + } + + /** + * Convert the instrumentation result into it's Map representation. + * + * @param result the result string + * @return the new map + */ + @VisibleForTesting + /* package */ static Map<String, Object> convertInstrumentResult(String result) { + Map<String, Object> map = Maps.newHashMap(); + Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE); + Matcher matcher = pattern.matcher(result); + + int previousEnd = 0; + String previousWhich = null; + + while (matcher.find()) { + if ("RESULT".equals(previousWhich)) { + String resultLine = result.substring(previousEnd, matcher.start()).trim(); + // Look for the = in the value, and split there + int splitIndex = resultLine.indexOf("="); + String key = resultLine.substring(0, splitIndex); + String value = resultLine.substring(splitIndex + 1); + + map.put(key, value); + } + + previousEnd = matcher.end(); + previousWhich = matcher.group(1); + } + if ("RESULT".equals(previousWhich)) { + String resultLine = result.substring(previousEnd, matcher.start()).trim(); + // Look for the = in the value, and split there + int splitIndex = resultLine.indexOf("="); + String key = resultLine.substring(0, splitIndex); + String value = resultLine.substring(splitIndex + 1); + + map.put(key, value); + } + return map; + } + + @Override + protected void drag(int startx, int starty, int endx, int endy, int steps, long ms) { + final long iterationTime = ms / steps; + + LinearInterpolator lerp = new LinearInterpolator(steps); + LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty); + LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy); + lerp.interpolate(start, end, new LinearInterpolator.Callback() { + public void step(Point point) { + try { + manager.touchMove(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag start event", e); + } + + try { + Thread.sleep(iterationTime); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + } + + public void start(Point point) { + try { + manager.touchDown(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag start event", e); + } + + try { + Thread.sleep(iterationTime); + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Error sleeping", e); + } + } + + public void end(Point point) { + try { + manager.touchUp(point.getX(), point.getY()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending drag end event", e); + } + } + }); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java new file mode 100644 index 0000000..fc32600 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.android.ddmlib.RawImage; +import com.android.monkeyrunner.MonkeyImage; +import com.android.monkeyrunner.adb.image.ImageUtils; + +import java.awt.image.BufferedImage; + +/** + * ADB implementation of the MonkeyImage class. + */ +public class AdbMonkeyImage extends MonkeyImage { + private final RawImage image; + + /** + * Create a new AdbMonkeyImage. + * + * @param image the image from adb. + */ + AdbMonkeyImage(RawImage image) { + this.image = image; + } + + @Override + public BufferedImage createBufferedImage() { + return ImageUtils.convertImage(image); + } + + public RawImage getRawImage() { + return image; + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java b/monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java new file mode 100644 index 0000000..9f99a7a --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.android.ddmlib.IShellOutputReceiver; + +/** + * Shell Output Receiver that captures shell output into a String for + * later retrieval. + */ +public class CommandOutputCapture implements IShellOutputReceiver { + private final StringBuilder builder = new StringBuilder(); + + public void flush() { } + + public boolean isCancelled() { + return false; + } + + public void addOutput(byte[] data, int offset, int length) { + String message = new String(data, offset, length); + builder.append(message); + } + + @Override + public String toString() { + return builder.toString(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java b/monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java new file mode 100644 index 0000000..e39fefd --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + + + +/** + * Linear Interpolation class. + */ +public class LinearInterpolator { + private final int steps; + + /** + * Use our own Point class so we don't pull in java.awt.* just for this simple class. + */ + public static class Point { + private final int x; + private final int y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return new StringBuilder(). + append("("). + append(x). + append(","). + append(y). + append(")").toString(); + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof Point) { + Point that = (Point) obj; + return this.x == that.x && this.y == that.y; + } + return false; + } + + @Override + public int hashCode() { + return 0x43125315 + x + y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + } + + /** + * Callback interface to recieve interpolated points. + */ + public interface Callback { + /** + * Called once to inform of the start point. + */ + void start(Point point); + /** + * Called once to inform of the end point. + */ + void end(Point point); + /** + * Called at every step in-between start and end. + */ + void step(Point point); + } + + /** + * Create a new linear Interpolator. + * + * @param steps How many steps should be in a single run. This counts the intervals + * in-between points, so the actual number of points generated will be steps + 1. + */ + public LinearInterpolator(int steps) { + this.steps = steps; + } + + // Copied from android.util.MathUtils since we couldn't link it in on the host. + private static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + /** + * Calculate the interpolated points. + * + * @param start The starting point + * @param end The ending point + * @param callback the callback to call with each calculated points. + */ + public void interpolate(Point start, Point end, Callback callback) { + int xDistance = Math.abs(end.getX() - start.getX()); + int yDistance = Math.abs(end.getY() - start.getY()); + float amount = (float) (1.0 / steps); + + + callback.start(start); + for (int i = 1; i < steps; i++) { + float newX = lerp(start.getX(), end.getX(), amount * i); + float newY = lerp(start.getY(), end.getY(), amount * i); + + callback.step(new Point(Math.round(newX), Math.round(newY))); + } + // Generate final point + callback.end(end); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java b/monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java new file mode 100644 index 0000000..b78aff3 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.android.ddmlib.IShellOutputReceiver; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Shell Output Receiver that sends shell output to a Logger. + */ +public class LoggingOutputReceiver implements IShellOutputReceiver { + private final Logger log; + private final Level level; + + public LoggingOutputReceiver(Logger log, Level level) { + this.log = log; + this.level = level; + } + + public void addOutput(byte[] data, int offset, int length) { + String message = new String(data, offset, length); + for (String line : message.split("\n")) { + log.log(level, line); + } + } + + public void flush() { } + + public boolean isCancelled() { + return false; + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java new file mode 100644 index 0000000..7e31ea5 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb.image; + +import com.android.ddmlib.RawImage; +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.adb.AdbBackend; +import com.android.monkeyrunner.adb.AdbMonkeyImage; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * Utility program to capture raw and converted images from a device and write them to a file. + * This is used to generate the test data for ImageUtilsTest. + */ +public class CaptureRawAndConvertedImage { + public static class MonkeyRunnerRawImage implements Serializable { + public int version; + public int bpp; + public int size; + public int width; + public int height; + public int red_offset; + public int red_length; + public int blue_offset; + public int blue_length; + public int green_offset; + public int green_length; + public int alpha_offset; + public int alpha_length; + + public byte[] data; + + public MonkeyRunnerRawImage(RawImage rawImage) { + version = rawImage.version; + bpp = rawImage.bpp; + size = rawImage.size; + width = rawImage.width; + height = rawImage.height; + red_offset = rawImage.red_offset; + red_length = rawImage.red_length; + blue_offset = rawImage.blue_offset; + blue_length = rawImage.blue_length; + green_offset = rawImage.green_offset; + green_length = rawImage.green_length; + alpha_offset = rawImage.alpha_offset; + alpha_length = rawImage.alpha_length; + + data = rawImage.data; + } + + public RawImage toRawImage() { + RawImage rawImage = new RawImage(); + + rawImage.version = version; + rawImage.bpp = bpp; + rawImage.size = size; + rawImage.width = width; + rawImage.height = height; + rawImage.red_offset = red_offset; + rawImage.red_length = red_length; + rawImage.blue_offset = blue_offset; + rawImage.blue_length = blue_length; + rawImage.green_offset = green_offset; + rawImage.green_length = green_length; + rawImage.alpha_offset = alpha_offset; + rawImage.alpha_length = alpha_length; + + rawImage.data = data; + return rawImage; + } + } + + private static void writeOutImage(RawImage screenshot, String name) throws IOException { + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name)); + out.writeObject(new MonkeyRunnerRawImage(screenshot)); + out.close(); + } + + public static void main(String[] args) throws IOException { + AdbBackend backend = new AdbBackend(); + MonkeyDevice device = backend.waitForConnection(); + AdbMonkeyImage snapshot = (AdbMonkeyImage) device.takeSnapshot(); + + // write out to a file + snapshot.writeToFile("output.png", "png"); + writeOutImage(snapshot.getRawImage(), "output.raw"); + System.exit(0); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java new file mode 100644 index 0000000..c3eaf01 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Hashtable; +/** + * Useful image related functions. + */ +public class ImageUtils { + // Utility class + private ImageUtils() { } + + private static Hashtable<?,?> EMPTY_HASH = new Hashtable(); + private static int[] BAND_OFFSETS_32 = { 0, 1, 2, 3 }; + private static int[] BAND_OFFSETS_16 = { 0, 1 }; + + /** + * Convert a raw image into a buffered image. + * + * @param rawImage the raw image to convert + * @param image the old image to (possibly) recycle + * @return the converted image + */ + public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) { + switch (rawImage.bpp) { + case 16: + return rawImage16toARGB(image, rawImage); + case 32: + return rawImage32toARGB(rawImage); + } + return null; + } + + /** + * Convert a raw image into a buffered image. + * + * @param rawImage the image to convert. + * @return the converted image. + */ + public static BufferedImage convertImage(RawImage rawImage) { + return convertImage(rawImage, null); + } + + static int getMask(int length) { + int res = 0; + for (int i = 0 ; i < length ; i++) { + res = (res << 1) + 1; + } + + return res; + } + + private static BufferedImage rawImage32toARGB(RawImage rawImage) { + // Do as much as we can to not make an extra copy of the data. This is just a bunch of + // classes that wrap's the raw byte array of the image data. + DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); + + PixelInterleavedSampleModel sampleModel = + new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, + 4, rawImage.width * 4, BAND_OFFSETS_32); + WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, + new Point(0, 0)); + return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, false, EMPTY_HASH); + } + + private static BufferedImage rawImage16toARGB(BufferedImage image, RawImage rawImage) { + // Do as much as we can to not make an extra copy of the data. This is just a bunch of + // classes that wrap's the raw byte array of the image data. + DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); + + PixelInterleavedSampleModel sampleModel = + new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, + 2, rawImage.width * 2, BAND_OFFSETS_16); + WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, + new Point(0, 0)); + return new BufferedImage(new SixteenBitColorModel(rawImage), raster, false, EMPTY_HASH); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java new file mode 100644 index 0000000..06ab939 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; + +/** + * Internal color model used to do conversion of 16bpp RawImages. + */ +class SixteenBitColorModel extends ColorModel { + private static final int[] BITS = { + 8, 8, 8, 8 + }; + public SixteenBitColorModel(RawImage rawImage) { + super(32 + , BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), + true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + } + + @Override + public boolean isCompatibleRaster(Raster raster) { + return true; + } + + private int getPixel(Object inData) { + byte[] data = (byte[]) inData; + int value = data[0] & 0x00FF; + value |= (data[1] << 8) & 0x0FF00; + + return value; + } + + @Override + public int getAlpha(Object inData) { + return 0xff; + } + + @Override + public int getBlue(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 0) & 0x01F) << 3; + } + + @Override + public int getGreen(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 5) & 0x03F) << 2; + } + + @Override + public int getRed(Object inData) { + int pixel = getPixel(inData); + return ((pixel >> 11) & 0x01F) << 3; + } + + @Override + public int getAlpha(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getBlue(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getGreen(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRed(int pixel) { + throw new UnsupportedOperationException(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java new file mode 100644 index 0000000..d4e47ea --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb.image; + +import com.android.ddmlib.RawImage; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; + +/** + * Internal color model used to do conversion of 32bpp RawImages. + */ +class ThirtyTwoBitColorModel extends ColorModel { + private static final int[] BITS = { + 8, 8, 8, 8, + }; + private final int alphaLength; + private final int alphaMask; + private final int alphaOffset; + private final int blueMask; + private final int blueLength; + private final int blueOffset; + private final int greenMask; + private final int greenLength; + private final int greenOffset; + private final int redMask; + private final int redLength; + private final int redOffset; + + public ThirtyTwoBitColorModel(RawImage rawImage) { + super(32, BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), + true, false, Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + redOffset = rawImage.red_offset; + redLength = rawImage.red_length; + redMask = ImageUtils.getMask(redLength); + greenOffset = rawImage.green_offset; + greenLength = rawImage.green_length; + greenMask = ImageUtils.getMask(greenLength); + blueOffset = rawImage.blue_offset; + blueLength = rawImage.blue_length; + blueMask = ImageUtils.getMask(blueLength); + alphaLength = rawImage.alpha_length; + alphaOffset = rawImage.alpha_offset; + alphaMask = ImageUtils.getMask(alphaLength); + } + + @Override + public boolean isCompatibleRaster(Raster raster) { + return true; + } + + private int getPixel(Object inData) { + byte[] data = (byte[]) inData; + int value = data[0] & 0x00FF; + value |= (data[1] & 0x00FF) << 8; + value |= (data[2] & 0x00FF) << 16; + value |= (data[3] & 0x00FF) << 24; + + return value; + } + + @Override + public int getAlpha(Object inData) { + int pixel = getPixel(inData); + if(alphaLength == 0) { + return 0xff; + } + return ((pixel >>> alphaOffset) & alphaMask) << (8 - alphaLength); + } + + @Override + public int getBlue(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> blueOffset) & blueMask) << (8 - blueLength); + } + + @Override + public int getGreen(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> greenOffset) & greenMask) << (8 - greenLength); + } + + @Override + public int getRed(Object inData) { + int pixel = getPixel(inData); + return ((pixel >>> redOffset) & redMask) << (8 - redLength); + } + + @Override + public int getAlpha(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getBlue(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getGreen(int pixel) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRed(int pixel) { + throw new UnsupportedOperationException(); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java new file mode 100644 index 0000000..e199a75 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.controller; + +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.adb.AdbBackend; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.logging.Logger; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +/** + * Application that can control an attached device using the network monkey. It has a window + * that shows what the current screen looks like and allows the user to click in it. Clicking in + * the window sends touch events to the attached device. It also supports keyboard input for + * typing and has buttons to press to simulate physical buttons on the device. + */ +public class MonkeyController extends JFrame { + private static final Logger LOG = Logger.getLogger(MonkeyController.class.getName()); + + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + AdbBackend adb = new AdbBackend(); + final MonkeyDevice device = adb.waitForConnection(); + MonkeyControllerFrame mf = new MonkeyControllerFrame(device); + mf.setVisible(true); + mf.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + device.dispose(); + } + }); + } + }); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java new file mode 100644 index 0000000..7f5a7d8 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.controller; + +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.MonkeyImage; +import com.android.monkeyrunner.MonkeyManager; +import com.android.monkeyrunner.PhysicalButton; + +import java.awt.KeyEventDispatcher; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.logging.Logger; + +import javax.swing.AbstractAction; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +/** + * Main window for MonkeyController. + */ +public class MonkeyControllerFrame extends JFrame { + private static final Logger LOG = Logger.getLogger(MonkeyControllerFrame.class.getName()); + + private final JButton refreshButton = new JButton("Refresh"); + private final JButton variablesButton = new JButton("Variable"); + private final JLabel imageLabel = new JLabel(); + private final VariableFrame variableFrame; + + private MonkeyManager monkeyManager; + private BufferedImage currentImage; + + private final Timer timer = new Timer(1000, new ActionListener() { + public void actionPerformed(ActionEvent e) { + updateScreen(); + } + }); + + private final MonkeyDevice device; + + private class PressAction extends AbstractAction { + private final PhysicalButton button; + + public PressAction(PhysicalButton button) { + this.button = button; + } + public void actionPerformed(ActionEvent event) { + try { + monkeyManager.press(button); + } catch (IOException e) { + throw new RuntimeException(e); + } + updateScreen(); + } + } + + private JButton createToolbarButton(PhysicalButton hardButton) { + JButton button = new JButton(new PressAction(hardButton)); + button.setText(hardButton.getKeyName()); + return button; + } + + public MonkeyControllerFrame(MonkeyDevice device) { + super("MonkeyController"); + this.device = device; + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); + JToolBar toolbar = new JToolBar(); + + toolbar.add(createToolbarButton(PhysicalButton.HOME)); + toolbar.add(createToolbarButton(PhysicalButton.BACK)); + toolbar.add(createToolbarButton(PhysicalButton.SEARCH)); + toolbar.add(createToolbarButton(PhysicalButton.MENU)); + + add(toolbar); + add(refreshButton); + add(variablesButton); + add(imageLabel); + + refreshButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + updateScreen(); + } + }); + + variableFrame = new VariableFrame(); + variablesButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + variableFrame.setVisible(true); + } + }); + + imageLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + try { + monkeyManager.touch(event.getX(), event.getY()); + } catch (IOException e) { + throw new RuntimeException(e); + } + updateScreen(); + } + + }); + + KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); + focusManager.addKeyEventDispatcher(new KeyEventDispatcher() { + public boolean dispatchKeyEvent(KeyEvent event) { + if (KeyEvent.KEY_TYPED == event.getID()) { + try { + monkeyManager.type(event.getKeyChar()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return false; + } + }); + + SwingUtilities.invokeLater(new Runnable(){ + public void run() { + init(); + variableFrame.init(monkeyManager); + } + }); + + pack(); + } + + private void updateScreen() { + MonkeyImage snapshot = device.takeSnapshot(); + currentImage = snapshot.createBufferedImage(); + imageLabel.setIcon(new ImageIcon(currentImage)); + + pack(); + } + + private void init() { + monkeyManager = device.getManager(); + if (monkeyManager == null) { + throw new RuntimeException("Unable to create monkey manager"); + } + updateScreen(); + timer.start(); + } + +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java b/monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java new file mode 100644 index 0000000..9015b5d --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.controller; + +import com.google.common.collect.Sets; + +import com.android.monkeyrunner.MonkeyManager; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.table.AbstractTableModel; + +/** + * Swing Frame that displays all the variables that the monkey exposes on the device. + */ +public class VariableFrame extends JFrame { + private static final Logger LOG = Logger.getLogger(VariableFrame.class.getName()); + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); + private MonkeyManager monkeyManager; + + private static class VariableHolder implements Comparable<VariableHolder> { + private final String key; + private final String value; + + public VariableHolder(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public int compareTo(VariableHolder o) { + return key.compareTo(o.key); + } + } + + private static <E> E getNthElement(Set<E> set, int offset) { + int current = 0; + for (E elem : set) { + if (current == offset) { + return elem; + } + current++; + } + return null; + } + + private class VariableTableModel extends AbstractTableModel { + private final TreeSet<VariableHolder> set = Sets.newTreeSet(); + + public void refresh() { + Collection<String> variables; + try { + variables = monkeyManager.listVariable(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error getting list of variables", e); + return; + } + for (final String variable : variables) { + EXECUTOR.execute(new Runnable() { + public void run() { + String value; + try { + value = monkeyManager.getVariable(variable); + } catch (IOException e) { + LOG.log(Level.SEVERE, + "Error getting variable value for " + variable, e); + return; + } + if (value == null) { + value = ""; + } + synchronized (set) { + set.add(new VariableHolder(variable, value)); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + VariableTableModel.this.fireTableDataChanged(); + } + }); + + } + } + }); + } + } + + public int getColumnCount() { + return 2; + } + + public int getRowCount() { + synchronized (set) { + return set.size(); + } + } + + public Object getValueAt(int rowIndex, int columnIndex) { + VariableHolder nthElement; + synchronized (set) { + nthElement = getNthElement(set, rowIndex); + } + if (columnIndex == 0) { + return nthElement.getKey(); + } + return nthElement.getValue(); + } + } + + public VariableFrame() { + super("Variables"); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); + + final VariableTableModel tableModel = new VariableTableModel(); + + JButton refreshButton = new JButton("Refresh"); + add(refreshButton); + refreshButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + tableModel.refresh(); + } + }); + + + JTable table = new JTable(tableModel); + add(table); + + tableModel.addTableModelListener(new TableModelListener() { + public void tableChanged(TableModelEvent e) { + pack(); + } + }); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + tableModel.refresh(); + } + }); + + pack(); + } + + public void init(MonkeyManager monkeyManager) { + this.monkeyManager = monkeyManager; + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java b/monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java new file mode 100644 index 0000000..dd3eb0b --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.doc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated method is a public API to expose to the + * scripting interface. Can be used to generate documentation of what + * methods are exposed and also can be used to enforce visibility of + * these methods in the scripting environment. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE }) +public @interface MonkeyRunnerExported { + /** + * A documentation string for this method. + */ + String doc(); + + /** + * The list of names for the keywords in this method in their proper positional order. + * + * For example: + * + * @MonkeyRunnerExported(args={"one", "two"}) + * public void foo(); + * + * would allow calls like this: + * foo(one=1, two=2) + * foo(1, 2) + */ + String[] args() default {}; + + /** + * The list of documentation for the arguments. + */ + String[] argDocs() default {}; + + /** + * The documentation for the return type of this method. + */ + String returns() default "returns nothing."; +}
\ No newline at end of file diff --git a/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java b/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java new file mode 100644 index 0000000..8fe4143 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.exceptions; + +/** + * Base exception class for all MonkeyRunner Exceptions. + */ +public class MonkeyRunnerException extends Exception { + public MonkeyRunnerException(String message) { + super(message); + } + + public MonkeyRunnerException(Throwable e) { + super(e); + } + + public MonkeyRunnerException(String message, Throwable e) { + super(message, e); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java b/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java new file mode 100644 index 0000000..c2fa5f7 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.stub; + +import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.MonkeyManager; +import com.android.monkeyrunner.MonkeyRunnerBackend; + +public class StubBackend implements MonkeyRunnerBackend { + + public MonkeyManager createManager(String address, int port) { + // TODO Auto-generated method stub + return null; + } + + public MonkeyDevice waitForConnection(long timeout, String deviceId) { + // TODO Auto-generated method stub + return null; + } + + public void shutdown() { + // We're stub - we've got nothing to do. + } +} diff --git a/monkeyrunner/src/resources/com/android/monkeyrunner/html.cs b/monkeyrunner/src/resources/com/android/monkeyrunner/html.cs new file mode 100644 index 0000000..7d2c93f --- /dev/null +++ b/monkeyrunner/src/resources/com/android/monkeyrunner/html.cs @@ -0,0 +1,25 @@ +<html> +<body> +<h1>MonkeyRunner Help<h1> +<h2>Table of Contents</h2> +<ul> +<?cs each:item = help ?> +<li><a href="#<?cs name:item ?>"><?cs var:item.name ?></a></li> +<?cs /each ?> +</ul> +<?cs each:item = help ?> +<h2><a name="<?cs name:item ?>"><?cs var:item.name ?></a></h2> + <p><?cs var:item.doc ?></p> + <?cs if:subcount(item.argument) ?> +<h3>Args</h3> +<ul> + <?cs each:arg = item.argument ?> + <li><?cs var:arg.name ?> - <?cs var:arg.doc ?></li> + <?cs /each ?> +</ul> +<h3>Returns</h3> +<p><?cs var:item.returns ?></p> +<?cs /if ?> +<?cs /each ?> +</body> +</html> diff --git a/monkeyrunner/src/resources/com/android/monkeyrunner/text.cs b/monkeyrunner/src/resources/com/android/monkeyrunner/text.cs new file mode 100644 index 0000000..4a4af5f --- /dev/null +++ b/monkeyrunner/src/resources/com/android/monkeyrunner/text.cs @@ -0,0 +1,9 @@ +MonkeyRunner help +<?cs each:item = help ?> +<?cs var:item.name ?> + <?cs var:item.doc ?> + +<?cs if:subcount(item.argument) ?> Args:<?cs each:arg = item.argument ?> + <?cs var:arg.name ?> - <?cs var:arg.doc ?><?cs /each ?> +<?cs /if ?> Returns: <?cs var:item.returns ?> +<?cs /each ?> diff --git a/monkeyrunner/test/Android.mk b/monkeyrunner/test/Android.mk new file mode 100644 index 0000000..6e2233b --- /dev/null +++ b/monkeyrunner/test/Android.mk @@ -0,0 +1,23 @@ +# +# Copyright (C) 2010 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_MODULE := MonkeyRunnerTest +LOCAL_JAVA_LIBRARIES := junit monkeyrunner ddmlib guavalib jython + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/monkeyrunner/test/com/android/monkeyrunner/AllTests.java b/monkeyrunner/test/com/android/monkeyrunner/AllTests.java new file mode 100644 index 0000000..9616759 --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/AllTests.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.android.monkeyrunner.adb.AdbMonkeyDeviceTest; +import com.android.monkeyrunner.adb.LinearInterpolatorTest; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestResult; +import junit.framework.TestSuite; +import junit.textui.TestRunner; + +/** + * Test suite to run all the tests for MonkeyRunner. + */ +public class AllTests { + public static Test suite(Class<? extends TestCase>... classes) { + TestSuite suite = new TestSuite(); + for (Class<? extends TestCase> clz : classes) { + suite.addTestSuite(clz); + } + return suite; + } + + public static void main(String args[]) { + TestRunner tr = new TestRunner(); + TestResult result = tr.doRun(AllTests.suite(ImageUtilsTest.class, JythonUtilsTest.class, + MonkeyRunnerOptionsTest.class, LinearInterpolatorTest.class, + AdbMonkeyDeviceTest.class)); + if (result.wasSuccessful()) { + System.exit(0); + } else { + System.exit(1); + } + } +} diff --git a/monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java b/monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java new file mode 100644 index 0000000..f07c2f3 --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.android.ddmlib.RawImage; +import com.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage; +import com.android.monkeyrunner.adb.image.ImageUtils; +import com.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage.MonkeyRunnerRawImage; + +import junit.framework.TestCase; + +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import javax.imageio.ImageIO; + +public class ImageUtilsTest extends TestCase { + private static BufferedImage createBufferedImage(String name) throws IOException { + InputStream is = ImageUtilsTest.class.getResourceAsStream(name); + BufferedImage img = ImageIO.read(is); + is.close(); + return img; + } + + private static RawImage createRawImage(String name) throws IOException, ClassNotFoundException { + ObjectInputStream is = + new ObjectInputStream(ImageUtilsTest.class.getResourceAsStream(name)); + CaptureRawAndConvertedImage.MonkeyRunnerRawImage wrapper = (MonkeyRunnerRawImage) is.readObject(); + is.close(); + return wrapper.toRawImage(); + } + + /** + * Check that the two images will draw the same (ie. have the same pixels). This is different + * that BufferedImage.equals(), which also wants to check that they have the same ColorModel + * and other parameters. + * + * @param i1 the first image + * @param i2 the second image + * @return true if both images will draw the same (ie. have same pixels). + */ + private static boolean checkImagesHaveSamePixels(BufferedImage i1, BufferedImage i2) { + if (i1.getWidth() != i2.getWidth()) { + return false; + } + if (i1.getHeight() != i2.getHeight()) { + return false; + } + + for (int y = 0; y < i1.getHeight(); y++) { + for (int x = 0; x < i1.getWidth(); x++) { + int p1 = i1.getRGB(x, y); + int p2 = i2.getRGB(x, y); + if (p1 != p2) { + WritableRaster r1 = i1.getRaster(); + WritableRaster r2 = i2.getRaster(); + return false; + } + } + } + + return true; + } + + public void testImageConversionOld() throws IOException, ClassNotFoundException { + RawImage rawImage = createRawImage("image1.raw"); + BufferedImage convertedImage = ImageUtils.convertImage(rawImage); + BufferedImage correctConvertedImage = createBufferedImage("image1.png"); + + assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage)); + } + + public void testImageConversionNew() throws IOException, ClassNotFoundException { + RawImage rawImage = createRawImage("image2.raw"); + BufferedImage convertedImage = ImageUtils.convertImage(rawImage); + BufferedImage correctConvertedImage = createBufferedImage("image2.png"); + + assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage)); + } +} diff --git a/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java b/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java new file mode 100644 index 0000000..5b8c8f9 --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; + +import com.android.monkeyrunner.doc.MonkeyRunnerExported; + +import junit.framework.TestCase; + +import org.python.core.ArgParser; +import org.python.core.PyDictionary; +import org.python.core.PyException; +import org.python.core.PyObject; +import org.python.core.PyString; + +import java.util.List; +import java.util.Map; + +/** + * Unit tests for the JythonUtils class. + */ +public class JythonUtilsTest extends TestCase { + private static final String PACKAGE_NAME = JythonUtilsTest.class.getPackage().getName(); + private static final String CLASS_NAME = JythonUtilsTest.class.getSimpleName(); + + private static boolean called = false; + private static double floatValue = 0.0; + private static List<Object> listValue = null; + private static Map<String, Object> mapValue; + + @MonkeyRunnerExported(doc = "", args = {"value"}) + public static void floatTest(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + called = true; + + floatValue = JythonUtils.getFloat(ap, 0); + } + + @MonkeyRunnerExported(doc = "", args = {"value"}) + public static void listTest(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + called = true; + + listValue = JythonUtils.getList(ap, 0); + } + + @MonkeyRunnerExported(doc = "", args = {"value"}) + public static void mapTest(PyObject[] args, String[] kws) { + ArgParser ap = JythonUtils.createArgParser(args, kws); + Preconditions.checkNotNull(ap); + called = true; + + mapValue = JythonUtils.getMap(ap, 0); + } + + @MonkeyRunnerExported(doc = "") + public static PyDictionary convertMapTest(PyObject[] args, String[] kws) { + Map<String, Object> map = Maps.newHashMap(); + map.put("string", "value"); + map.put("integer", 1); + map.put("double", 3.14); + return JythonUtils.convertMapToDict(map); + } + + @Override + protected void setUp() throws Exception { + called = false; + floatValue = 0.0; + } + + private static PyObject call(String method) { + return call(method, new String[]{ }); + } + private static PyObject call(String method, String... args) { + StringBuilder sb = new StringBuilder(); + sb.append("from ").append(PACKAGE_NAME); + sb.append(" import ").append(CLASS_NAME).append("\n"); + + // Exec line + sb.append("result = "); + sb.append(CLASS_NAME).append(".").append(method); + sb.append("("); + for (String arg : args) { + sb.append(arg).append(","); + } + sb.append(")"); + + return ScriptRunner.runStringAndGet(sb.toString(), "result").get("result"); + } + + public void testSimpleCall() { + call("floatTest", "0.0"); + assertTrue(called); + } + + public void testMissingFloatArg() { + try { + call("floatTest"); + } catch(PyException e) { + return; + } + fail("Should have thrown exception"); + } + + public void testBadFloatArgType() { + try { + call("floatTest", "\'foo\'"); + } catch(PyException e) { + return; + } + fail("Should have thrown exception"); + } + + public void testFloatParse() { + call("floatTest", "103.2"); + assertTrue(called); + assertEquals(floatValue, 103.2); + } + + public void testFloatParseInteger() { + call("floatTest", "103"); + assertTrue(called); + assertEquals(floatValue, 103.0); + } + + public void testParseStringList() { + call("listTest", "['a', 'b', 'c']"); + assertTrue(called); + assertEquals(3, listValue.size()); + assertEquals("a", listValue.get(0)); + assertEquals("b", listValue.get(1)); + assertEquals("c", listValue.get(2)); + } + + public void testParseIntList() { + call("listTest", "[1, 2, 3]"); + assertTrue(called); + assertEquals(3, listValue.size()); + assertEquals(new Integer(1), listValue.get(0)); + assertEquals(new Integer(2), listValue.get(1)); + assertEquals(new Integer(3), listValue.get(2)); + } + + public void testParseMixedList() { + call("listTest", "['a', 1, 3.14]"); + assertTrue(called); + assertEquals(3, listValue.size()); + assertEquals("a", listValue.get(0)); + assertEquals(new Integer(1), listValue.get(1)); + assertEquals(new Double(3.14), listValue.get(2)); + } + + public void testParseOptionalList() { + call("listTest"); + assertTrue(called); + assertEquals(0, listValue.size()); + } + + public void testParsingNotAList() { + try { + call("listTest", "1.0"); + } catch (PyException e) { + return; + } + fail("Should have thrown an exception"); + } + + public void testParseMap() { + call("mapTest", "{'a': 0, 'b': 'bee', 3: 'cee'}"); + assertTrue(called); + assertEquals(3, mapValue.size()); + assertEquals(new Integer(0), mapValue.get("a")); + assertEquals("bee", mapValue.get("b")); + // note: coerced key type + assertEquals("cee", mapValue.get("3")); + } + + public void testParsingNotAMap() { + try { + call("mapTest", "1.0"); + } catch (PyException e) { + return; + } + fail("Should have thrown an exception"); + } + + public void testParseOptionalMap() { + call("mapTest"); + assertTrue(called); + assertEquals(0, mapValue.size()); + } + + public void testConvertMap() { + PyDictionary result = (PyDictionary) call("convertMapTest"); + PyObject stringPyObject = result.__getitem__(new PyString("string")); + String string = (String) stringPyObject.__tojava__(String.class); + assertEquals("value", string); + + PyObject intPyObject = result.__getitem__(new PyString("integer")); + int i = (Integer) intPyObject.__tojava__(Integer.class); + assertEquals(i, 1); + + PyObject doublePyObject = result.__getitem__(new PyString("double")); + double d = (Double) doublePyObject.__tojava__(Double.class); + assertEquals(3.14, d); + } +} diff --git a/monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java b/monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java new file mode 100644 index 0000000..fd23721 --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner; + +import junit.framework.TestCase; + +import java.io.File; +import java.util.Iterator; + +/** + * Unit Tests to test command line argument parsing. + */ +public class MonkeyRunnerOptionsTest extends TestCase { + // We need to use a file that actually exists + private static final String FILENAME = "/etc/passwd"; + + public void testSimpleArgs() { + MonkeyRunnerOptions options = + MonkeyRunnerOptions.processOptions(new String[] { FILENAME }); + assertEquals(options.getScriptFile(), new File(FILENAME)); + } + + public void testParsingArgsBeforeScriptName() { + MonkeyRunnerOptions options = + MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME}); + assertEquals("stub", options.getBackendName()); + assertEquals(options.getScriptFile(), new File(FILENAME)); + } + + public void testParsingScriptArgument() { + MonkeyRunnerOptions options = + MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "arg1", "arg2" }); + assertEquals(options.getScriptFile(), new File(FILENAME)); + Iterator<String> i = options.getArguments().iterator(); + assertEquals("arg1", i.next()); + assertEquals("arg2", i.next()); + } + + public void testParsingScriptArgumentWithDashes() { + MonkeyRunnerOptions options = + MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "--arg1" }); + assertEquals(options.getScriptFile(), new File(FILENAME)); + assertEquals("--arg1", options.getArguments().iterator().next()); + } + + public void testMixedArgs() { + MonkeyRunnerOptions options = + MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME, + "arg1", "--debug=True"}); + assertEquals("stub", options.getBackendName()); + assertEquals(options.getScriptFile(), new File(FILENAME)); + Iterator<String> i = options.getArguments().iterator(); + assertEquals("arg1", i.next()); + assertEquals("--debug=True", i.next()); + } +} diff --git a/monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java b/monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java new file mode 100644 index 0000000..258e184 --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.google.common.base.Joiner; +import com.google.common.io.Resources; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +/** + * Unit Tests for AdbMonkeyDevice. + */ +public class AdbMonkeyDeviceTest extends TestCase { + private static String MULTILINE_RESULT = "\r\n" + + "Test results for InstrumentationTestRunner=.\r\n" + + "Time: 2.242\r\n" + + "\r\n" + + "OK (1 test)"; + + private static String getResource(String resName) throws IOException { + URL resource = Resources.getResource(AdbMonkeyDeviceTest.class, resName); + List<String> lines = Resources.readLines(resource, Charset.defaultCharset()); + return Joiner.on("\r\n").join(lines); + } + + public void testSimpleResultParse() throws IOException { + String result = getResource("instrument_result.txt"); + Map<String, Object> convertedResult = AdbMonkeyDevice.convertInstrumentResult(result); + + assertEquals("one", convertedResult.get("result1")); + assertEquals("two", convertedResult.get("result2")); + } + + public void testMultilineResultParse() throws IOException { + String result = getResource("multiline_instrument_result.txt"); + Map<String, Object> convertedResult = AdbMonkeyDevice.convertInstrumentResult(result); + + assertEquals(MULTILINE_RESULT, convertedResult.get("stream")); + } +} diff --git a/monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java b/monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java new file mode 100644 index 0000000..00670ce --- /dev/null +++ b/monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2010 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. + */ +package com.android.monkeyrunner.adb; + +import com.google.common.collect.Lists; + +import com.android.monkeyrunner.adb.LinearInterpolator.Point; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * Unit tests for the LinerInterpolator class.S + */ +public class LinearInterpolatorTest extends TestCase { + private static class Collector implements LinearInterpolator.Callback { + private final List<LinearInterpolator.Point> points = Lists.newArrayList(); + + public List<LinearInterpolator.Point> getPoints() { + return points; + } + + public void end(Point input) { + points.add(input); + } + + public void start(Point input) { + points.add(input); + } + + public void step(Point input) { + points.add(input); + } + } + + List<Integer> STEP_POINTS = Lists.newArrayList(0, 100, 200, 300, 400, 500, 600, 700, 800, 900, + 1000); + List<Integer> REVERSE_STEP_POINTS = Lists.newArrayList(1000, 900, 800, 700, 600, 500, 400, 300, + 200, 100, 0); + + public void testLerpRight() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(0, 100), + new LinearInterpolator.Point(1000, 100), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(STEP_POINTS.get(x), 100), points.get(x)); + } + } + + public void testLerpLeft() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(1000, 100), + new LinearInterpolator.Point(0, 100), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(REVERSE_STEP_POINTS.get(x), 100), points.get(x)); + } + } + + public void testLerpUp() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(100, 1000), + new LinearInterpolator.Point(100, 0), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(100, REVERSE_STEP_POINTS.get(x)), points.get(x)); + } + } + + public void testLerpDown() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(100, 0), + new LinearInterpolator.Point(100, 1000), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(100, STEP_POINTS.get(x)), points.get(x)); + } + } + + public void testLerpNW() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(0, 0), + new LinearInterpolator.Point(1000, 1000), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(STEP_POINTS.get(x), STEP_POINTS.get(x)), points.get(x)); + } + } + + public void testLerpNE() { + LinearInterpolator lerp = new LinearInterpolator(10); + Collector collector = new Collector(); + lerp.interpolate(new LinearInterpolator.Point(1000, 1000), + new LinearInterpolator.Point(0, 0), + collector); + + List<LinearInterpolator.Point> points = collector.getPoints(); + assertEquals(11, points.size()); + for (int x = 0; x < points.size(); x++) { + assertEquals(new Point(REVERSE_STEP_POINTS.get(x), REVERSE_STEP_POINTS.get(x)), points.get(x)); + } + } +} diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt new file mode 100644 index 0000000..c127c0f --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt @@ -0,0 +1,10 @@ +INSTRUMENTATION_STATUS: id=InstrumentationTestRunner +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: result1=one +INSTRUMENTATION_RESULT: result2=two +INSTRUMENTATION_CODE: -1 diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt new file mode 100644 index 0000000..32fd901 --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt @@ -0,0 +1,15 @@ +INSTRUMENTATION_STATUS: id=InstrumentationTestRunner +INSTRUMENTATION_STATUS: current=1 +INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest +INSTRUMENTATION_STATUS: stream=. +INSTRUMENTATION_STATUS: numtests=1 +INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly +INSTRUMENTATION_STATUS_CODE: 0 +INSTRUMENTATION_RESULT: stream= +Test results for InstrumentationTestRunner=. +Time: 2.242 + +OK (1 test) + + +INSTRUMENTATION_CODE: -1 diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png Binary files differnew file mode 100644 index 0000000..9ef1800 --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw Binary files differnew file mode 100644 index 0000000..99ec013 --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png Binary files differnew file mode 100644 index 0000000..03ff0c1 --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw Binary files differnew file mode 100644 index 0000000..06e5b47 --- /dev/null +++ b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw |