Add hosttestlib library.

hosttestlib is a simple JUnit extension framework for tests that need to interact externally (ie
from a host machine) with an Android device.
diff --git a/tools/hosttestlib/Android.mk b/tools/hosttestlib/Android.mk
new file mode 100644
index 0000000..2184d3b
--- /dev/null
+++ b/tools/hosttestlib/Android.mk
@@ -0,0 +1,28 @@
+# 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)
+
+# Only compile source java files in this lib.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := hosttestlib
+
+LOCAL_JAVA_LIBRARIES := ddmlib junit
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+
diff --git a/tools/hosttestlib/src/com/android/hosttest/DeviceTest.java b/tools/hosttestlib/src/com/android/hosttest/DeviceTest.java
new file mode 100644
index 0000000..a782380
--- /dev/null
+++ b/tools/hosttestlib/src/com/android/hosttest/DeviceTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package com.android.hosttest;
+
+import com.android.ddmlib.IDevice;
+
+import junit.framework.Test;
+
+/**
+ * Helper JUnit test that stores reference to a Android device and test data.
+ */
+public interface DeviceTest extends Test {
+
+    /**
+     * Sets the device under test
+     * @param device the Android device to test
+     */
+    public void setDevice(IDevice device);
+
+    /**
+     * Retrieves the Android device under test
+     * @return the {@link IDevice} device.
+     */
+    public IDevice getDevice();
+
+    /**
+     * Retrieves host file system path that contains test app files
+     * @return {@link String} containing path, or <code>null</code>
+     */
+    public String getTestAppPath();
+
+    /**
+     * Sets host file system path that contains test app files
+     * @param path absolute file system path to test data files
+     */
+    public void setTestAppPath(String path);
+}
diff --git a/tools/hosttestlib/src/com/android/hosttest/DeviceTestCase.java b/tools/hosttestlib/src/com/android/hosttest/DeviceTestCase.java
new file mode 100644
index 0000000..42535d8
--- /dev/null
+++ b/tools/hosttestlib/src/com/android/hosttest/DeviceTestCase.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package com.android.hosttest;
+
+import com.android.ddmlib.IDevice;
+
+import junit.framework.TestCase;
+
+/**
+ * Helper JUnit test case that stores reference to an Android device and test data.
+ *
+ * Can be extended to verify an Android device's response to various adb commands.
+ */
+public abstract class DeviceTestCase extends TestCase implements DeviceTest {
+
+    /** Android device under test */
+    private IDevice mDevice = null;
+    /** optionally, used to store path to test data files */
+    private String mTestDataPath = null;
+
+    protected DeviceTestCase() {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setDevice(IDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public IDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getTestAppPath() {
+        return mTestDataPath;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setTestAppPath(String path) {
+        mTestDataPath = path;
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        // ensure device has been set before test is run
+        assertNotNull(getDevice());
+    }
+}
diff --git a/tools/hosttestlib/src/com/android/hosttest/DeviceTestRunner.java b/tools/hosttestlib/src/com/android/hosttest/DeviceTestRunner.java
new file mode 100644
index 0000000..ab4ce5f
--- /dev/null
+++ b/tools/hosttestlib/src/com/android/hosttest/DeviceTestRunner.java
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+
+package com.android.hosttest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.Test;
+import junit.framework.TestResult;
+import junit.textui.TestRunner;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+
+/**
+ * Command line interface for running DeviceTest tests.
+ *
+ * Extends junit.textui.TestRunner to handle optional -s (device serial) and -p (test data)
+ * arguments, and then pass their values to the instantiated DeviceTests.
+ *
+ * Provided test class must be a DeviceTest.
+ *
+ * @see junit.textui.TestRunner for more information on command line syntax.
+ */
+public class DeviceTestRunner extends TestRunner {
+
+    private static final String LOG_TAG = "DeviceTestRunner";
+    private String mDeviceSerial = null;
+    private IDevice mDevice = null;
+    private String mTestDataPath = null;
+
+    private DeviceTestRunner() {
+    }
+
+    /**
+     * Starts the test run.
+     * Extracts out DeviceTestCase specific command line arguments, then passes control to parent
+     * TestRunner.
+     * @param args command line arguments
+     * @return {@link TestResult}
+     */
+    @Override
+    public TestResult start(String[] args) throws Exception {
+        // holds unprocessed arguments to pass to parent
+        List<String> parentArgs = new ArrayList<String>();
+        for (int i=0; i < args.length; i++) {
+            if (args[i].equals("-s")) {
+                i++;
+                mDeviceSerial = extractArg(args, i);
+            } else if (args[i].equals("-p")) {
+                i++;
+                mTestDataPath = extractArg(args, i);
+            } else {
+                // unrecognized arg, must be for parent
+                parentArgs.add(args[i]);
+            }
+        }
+        mDevice = connectToDevice(mDeviceSerial);
+        return super.start(parentArgs.toArray(new String[parentArgs.size()]));
+    }
+
+    private String extractArg(String[] args, int index) {
+        if (args.length <= index) {
+            printUsage();
+            throw new IllegalArgumentException("Error: not enough arguments");
+        }
+        return args[index];
+    }
+
+    /**
+     * Initializes DDMS library, and connects to specified Android device
+     * @param deviceSerial
+     * @return {@link IDevice}
+     * @throws IllegalArgumentException if specified device cannot be found
+     */
+    private IDevice connectToDevice(String deviceSerial) {
+        // initialize DDMS with no clientSupport aka debugger support
+        AndroidDebugBridge.init(false /* clientSupport */);
+        AndroidDebugBridge adbBridge = AndroidDebugBridge.createBridge();
+        for (IDevice device : adbBridge.getDevices()) {
+            if (deviceSerial == null) {
+                return device;
+            } else if (deviceSerial.equals(device.getSerialNumber())) {
+                return device;
+            }
+        }
+        System.out.println("Waiting for device...");
+        NewDeviceListener listener = new NewDeviceListener(deviceSerial);
+        AndroidDebugBridge.addDeviceChangeListener(listener);
+        IDevice device = listener.waitForDevice(5000);
+        AndroidDebugBridge.removeDeviceChangeListener(listener);
+        if (device == null) {
+            throw new IllegalArgumentException("Could not connect to device");
+        }
+        return device;
+    }
+
+    /**
+     * Listener for new Android devices
+     */
+    private static class NewDeviceListener implements IDeviceChangeListener {
+        private IDevice mDevice;
+        private String mSerial;
+
+        public NewDeviceListener(String serial) {
+            mSerial = serial;
+        }
+
+        public void deviceChanged(IDevice device, int changeMask) {
+        }
+
+        public void deviceConnected(IDevice device) {
+            if (mSerial == null) {
+                setDevice(device);
+            } else if (mSerial.equals(device.getSerialNumber())) {
+                setDevice(device);
+            }
+        }
+
+        private synchronized void setDevice(IDevice device) {
+            mDevice = device;
+            notify();
+        }
+
+        public void deviceDisconnected(IDevice device) {
+        }
+
+        public IDevice waitForDevice(long waitTime) {
+            synchronized(this) {
+                if (mDevice == null) {
+                    try {
+                        wait(waitTime);
+                    } catch (InterruptedException e) {
+                        System.out.println("Waiting for device interrupted");
+                    }
+                }
+            }
+            return mDevice;
+        }
+    }
+
+    /**
+     * Main entry point.
+     *
+     * Establishes connection to provided adb device and runs tests
+     *
+     * @param args expects:
+     *     test class to run
+     *     optionally, device serial number. If unspecified, will connect to first device found
+     *     optionally, file system path to test data files
+     */
+    public static void main(String[] args) {
+        DeviceTestRunner aTestRunner = new DeviceTestRunner();
+        try {
+            TestResult r = aTestRunner.start(args);
+            if (!r.wasSuccessful())
+                System.exit(FAILURE_EXIT);
+            System.exit(SUCCESS_EXIT);
+        } catch(Exception e) {
+            System.err.println(e.getMessage());
+            System.exit(EXCEPTION_EXIT);
+        }
+    }
+
+    private static void printUsage() {
+        System.out.println("Usage: DeviceTestRunner <test_class> [-s device_serial] " +
+                "[-p test_data_path]");
+    }
+
+    /**
+     * Override parent to set DeviceTest data
+     */
+    @Override
+    public TestResult doRun(Test test, boolean wait) {
+        if (test instanceof DeviceTest) {
+            DeviceTest deviceTest = (DeviceTest)test;
+            deviceTest.setDevice(mDevice);
+            deviceTest.setTestAppPath(mTestDataPath);
+        } else {
+            Log.w(LOG_TAG, String.format("%s test class is not a DeviceTest.",
+                    test.getClass().getName()));
+        }
+        return super.doRun(test, wait);
+    }
+
+    /**
+     * Override parent to create DeviceTestSuite wrapper, instead of TestSuite
+     */
+    @Override
+    protected TestResult runSingleMethod(String testCase, String method, boolean wait)
+    throws Exception {
+        Class testClass = loadSuiteClass(testCase);
+        Test test = DeviceTestSuite.createTest(testClass, method);
+        return doRun(test, wait);
+    }
+}
diff --git a/tools/hosttestlib/src/com/android/hosttest/DeviceTestSuite.java b/tools/hosttestlib/src/com/android/hosttest/DeviceTestSuite.java
new file mode 100644
index 0000000..65bdaaa
--- /dev/null
+++ b/tools/hosttestlib/src/com/android/hosttest/DeviceTestSuite.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package com.android.hosttest;
+
+import com.android.ddmlib.IDevice;
+
+import junit.framework.Test;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+/**
+ * Helper JUnit test suite that stores reference to an Android device and test data.
+ */
+public class DeviceTestSuite extends TestSuite implements DeviceTest {
+
+    private IDevice mDevice = null;
+    private String mTestDataPath = null;
+
+    public DeviceTestSuite(Class testClass) {
+        super(testClass);
+    }
+
+    public DeviceTestSuite() {
+        super();
+    }
+
+    /**
+     * Adds the tests from the given class to the suite
+     */
+    @Override
+    public void addTestSuite(Class testClass) {
+        addTest(new DeviceTestSuite(testClass));
+    }
+
+    /**
+     * Overrides parent method to pass in device and test app path to included test
+     */
+    @Override
+    public void runTest(Test test, TestResult result) {
+        if (test instanceof DeviceTest) {
+            DeviceTest deviceTest = (DeviceTest)test;
+            deviceTest.setDevice(mDevice);
+            deviceTest.setTestAppPath(mTestDataPath);
+        }
+        test.run(result);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public IDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getTestAppPath() {
+        return mTestDataPath;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setDevice(IDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setTestAppPath(String path) {
+        mTestDataPath = path;
+    }
+}