Merge "Keep the order from getaddrinfo() unchanged if preferIPv6Addresses() is true."
diff --git a/JavaLibrary.mk b/JavaLibrary.mk
index b12b47d..64fdb7e 100644
--- a/JavaLibrary.mk
+++ b/JavaLibrary.mk
@@ -75,7 +75,7 @@
 LOCAL_JAVA_RESOURCE_DIRS := $(test_resource_dirs)
 
 LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_JAVA_LIBRARIES := core caliper
+LOCAL_JAVA_LIBRARIES := core
 LOCAL_DX_FLAGS := --core-library
 
 LOCAL_MODULE_TAGS := tests
@@ -181,4 +181,4 @@
 
     include $(BUILD_HOST_JAVA_LIBRARY)
 
-endif
\ No newline at end of file
+endif
diff --git a/dalvik/src/main/java/dalvik/system/VMDebug.java b/dalvik/src/main/java/dalvik/system/VMDebug.java
index ce3e95c..6f64c5f 100644
--- a/dalvik/src/main/java/dalvik/system/VMDebug.java
+++ b/dalvik/src/main/java/dalvik/system/VMDebug.java
@@ -34,6 +34,8 @@
 public final class VMDebug {
     /**
      * Specifies the default method trace data file name.
+     *
+     * @deprecated only used in one place, which is unused and deprecated
      */
     static public final String DEFAULT_METHOD_TRACE_FILE_NAME = "/sdcard/dmtrace.trace";
 
@@ -44,11 +46,13 @@
     public static final int TRACE_COUNT_ALLOCS = 1;
 
     /* constants for getAllocCount */
-    private static final int KIND_ALLOCATED_OBJECTS = 1<<0;
-    private static final int KIND_ALLOCATED_BYTES   = 1<<1;
-    private static final int KIND_FREED_OBJECTS     = 1<<2;
-    private static final int KIND_FREED_BYTES       = 1<<3;
-    private static final int KIND_GC_INVOCATIONS    = 1<<4;
+    private static final int KIND_ALLOCATED_OBJECTS     = 1<<0;
+    private static final int KIND_ALLOCATED_BYTES       = 1<<1;
+    private static final int KIND_FREED_OBJECTS         = 1<<2;
+    private static final int KIND_FREED_BYTES           = 1<<3;
+    private static final int KIND_GC_INVOCATIONS        = 1<<4;
+    private static final int KIND_CLASS_INIT_COUNT      = 1<<5;
+    private static final int KIND_CLASS_INIT_TIME       = 1<<6;
     private static final int KIND_EXT_ALLOCATED_OBJECTS = 1<<12;
     private static final int KIND_EXT_ALLOCATED_BYTES   = 1<<13;
     private static final int KIND_EXT_FREED_OBJECTS     = 1<<14;
@@ -64,6 +68,10 @@
         KIND_FREED_BYTES;
     public static final int KIND_GLOBAL_GC_INVOCATIONS =
         KIND_GC_INVOCATIONS;
+    public static final int KIND_GLOBAL_CLASS_INIT_COUNT =
+        KIND_CLASS_INIT_COUNT;
+    public static final int KIND_GLOBAL_CLASS_INIT_TIME =
+        KIND_CLASS_INIT_TIME;
     public static final int KIND_GLOBAL_EXT_ALLOCATED_OBJECTS =
         KIND_EXT_ALLOCATED_OBJECTS;
     public static final int KIND_GLOBAL_EXT_ALLOCATED_BYTES =
@@ -83,6 +91,10 @@
         KIND_FREED_BYTES << 16;
     public static final int KIND_THREAD_GC_INVOCATIONS =
         KIND_GC_INVOCATIONS << 16;
+    public static final int KIND_THREAD_CLASS_INIT_COUNT =
+        KIND_CLASS_INIT_COUNT << 16;
+    public static final int KIND_THREAD_CLASS_INIT_TIME =
+        KIND_CLASS_INIT_TIME << 16;
     public static final int KIND_THREAD_EXT_ALLOCATED_OBJECTS =
         KIND_EXT_ALLOCATED_OBJECTS << 16;
     public static final int KIND_THREAD_EXT_ALLOCATED_BYTES =
@@ -131,6 +143,8 @@
     /**
      * Start method tracing with default name, size, and with <code>0</code>
      * flags.
+     *
+     * @deprecated not used, not needed
      */
     public static void startMethodTracing() {
         startMethodTracing(DEFAULT_METHOD_TRACE_FILE_NAME, 0, 0);
diff --git a/include/UniquePtr.h b/include/UniquePtr.h
new file mode 100644
index 0000000..f5c7c2c
--- /dev/null
+++ b/include/UniquePtr.h
@@ -0,0 +1,231 @@
+/*
+ * 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.
+ */
+
+#ifndef UNIQUE_PTR_H_included
+#define UNIQUE_PTR_H_included
+
+#include <cstdlib> // For NULL.
+
+// Default deleter for pointer types.
+template <typename T>
+struct DefaultDelete {
+    enum { type_must_be_complete = sizeof(T) };
+    DefaultDelete() {}
+    void operator()(T* p) const {
+        delete p;
+    }
+};
+
+// Default deleter for array types.
+template <typename T>
+struct DefaultDelete<T[]> {
+    enum { type_must_be_complete = sizeof(T) };
+    void operator()(T* p) const {
+        delete[] p;
+    }
+};
+
+// A smart pointer that deletes the given pointer on destruction.
+// Equivalent to C++0x's std::unique_ptr (a combination of boost::scoped_ptr
+// and boost::scoped_array).
+// Named to be in keeping with Android style but also to avoid
+// collision with any other implementation, until we can switch over
+// to unique_ptr.
+// Use thus:
+//   UniquePtr<C> c(new C);
+template <typename T, typename D = DefaultDelete<T> >
+class UniquePtr {
+public:
+    // Construct a new UniquePtr, taking ownership of the given raw pointer.
+    explicit UniquePtr(T* ptr = NULL) : mPtr(ptr) {
+    }
+
+    ~UniquePtr() {
+        reset();
+    }
+
+    // Accessors.
+    T& operator*() const { return *mPtr; }
+    T* operator->() const { return mPtr; }
+    T* get() const { return mPtr; }
+
+    // Returns the raw pointer and hands over ownership to the caller.
+    // The pointer will not be deleted by UniquePtr.
+    T* release() {
+        T* result = mPtr;
+        mPtr = NULL;
+        return result;
+    }
+
+    // Takes ownership of the given raw pointer.
+    // If this smart pointer previously owned a different raw pointer, that
+    // raw pointer will be freed.
+    void reset(T* ptr = NULL) {
+        if (ptr != mPtr) {
+            D()(mPtr);
+            mPtr = ptr;
+        }
+    }
+
+private:
+    // The raw pointer.
+    T* mPtr;
+
+    // Comparing unique pointers is probably a mistake, since they're unique.
+    template <typename T2> bool operator==(const UniquePtr<T2>& p) const;
+    template <typename T2> bool operator!=(const UniquePtr<T2>& p) const;
+
+    // Disallow copy and assignment.
+    UniquePtr(const UniquePtr&);
+    void operator=(const UniquePtr&);
+};
+
+// Partial specialization for array types. Like std::unique_ptr, this removes
+// operator* and operator-> but adds operator[].
+template <typename T, typename D>
+class UniquePtr<T[], D> {
+public:
+    explicit UniquePtr(T* ptr = NULL) : mPtr(ptr) {
+    }
+
+    ~UniquePtr() {
+        reset();
+    }
+
+    T& operator[](size_t i) const {
+        return mPtr[i];
+    }
+    T* get() const { return mPtr; }
+
+    T* release() {
+        T* result = mPtr;
+        mPtr = NULL;
+        return result;
+    }
+
+    void reset(T* ptr = NULL) {
+        if (ptr != mPtr) {
+            D()(mPtr);
+            mPtr = ptr;
+        }
+    }
+
+private:
+    T* mPtr;
+
+    // Disallow copy and assignment.
+    UniquePtr(const UniquePtr&);
+    void operator=(const UniquePtr&);
+};
+
+#if UNIQUE_PTR_TESTS
+
+// Run these tests with:
+// g++ -g -DUNIQUE_PTR_TESTS -x c++ UniquePtr.h && ./a.out
+
+#include <stdio.h>
+
+static void assert(bool b) {
+    if (!b) {
+        fprintf(stderr, "FAIL\n");
+        abort();
+    }
+    fprintf(stderr, "OK\n");
+}
+static int cCount = 0;
+struct C {
+    C() { ++cCount; }
+    ~C() { --cCount; }
+};
+static bool freed = false;
+struct Freer {
+    void operator()(int* p) {
+        assert(*p == 123);
+        free(p);
+        freed = true;
+    }
+};
+
+int main(int argc, char* argv[]) {
+    //
+    // UniquePtr<T> tests...
+    //
+
+    // Can we free a single object?
+    {
+        UniquePtr<C> c(new C);
+        assert(cCount == 1);
+    }
+    assert(cCount == 0);
+    // Does release work?
+    C* rawC;
+    {
+        UniquePtr<C> c(new C);
+        assert(cCount == 1);
+        rawC = c.release();
+    }
+    assert(cCount == 1);
+    delete rawC;
+    // Does reset work?
+    {
+        UniquePtr<C> c(new C);
+        assert(cCount == 1);
+        c.reset(new C);
+        assert(cCount == 1);
+    }
+    assert(cCount == 0);
+
+    //
+    // UniquePtr<T[]> tests...
+    //
+
+    // Can we free an array?
+    {
+        UniquePtr<C[]> cs(new C[4]);
+        assert(cCount == 4);
+    }
+    assert(cCount == 0);
+    // Does release work?
+    {
+        UniquePtr<C[]> c(new C[4]);
+        assert(cCount == 4);
+        rawC = c.release();
+    }
+    assert(cCount == 4);
+    delete[] rawC;
+    // Does reset work?
+    {
+        UniquePtr<C[]> c(new C[4]);
+        assert(cCount == 4);
+        c.reset(new C[2]);
+        assert(cCount == 2);
+    }
+    assert(cCount == 0);
+
+    //
+    // Custom deleter tests...
+    //
+    assert(!freed);
+    {
+        UniquePtr<int, Freer> i(reinterpret_cast<int*>(malloc(sizeof(int))));
+        *i = 123;
+    }
+    assert(freed);
+    return 0;
+}
+#endif
+
+#endif  // UNIQUE_PTR_H_included
diff --git a/json/src/test/java/org/json/AllTests.java b/json/src/test/java/org/json/AllTests.java
new file mode 100644
index 0000000..8261a4d
--- /dev/null
+++ b/json/src/test/java/org/json/AllTests.java
@@ -0,0 +1,30 @@
+/*
+ * 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 org.json;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class AllTests {
+    public static Test suite() {
+        TestSuite suite = tests.TestSuiteFactory.createTestSuite();
+        suite.addTestSuite(JSONArrayTest.class);
+        suite.addTestSuite(JSONStringerTest.class);
+        suite.addTestSuite(JSONStringerTest.class);
+        return suite;
+    }
+}
diff --git a/json/src/test/java/org/json/JSONArrayTest.java b/json/src/test/java/org/json/JSONArrayTest.java
new file mode 100644
index 0000000..34e5ff6
--- /dev/null
+++ b/json/src/test/java/org/json/JSONArrayTest.java
@@ -0,0 +1,339 @@
+/**
+ * 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 org.json;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * This black box test was written without inspecting the non-free org.json sourcecode.
+ */
+public class JSONArrayTest extends TestCase {
+
+    public void testEmptyArray() throws JSONException {
+        JSONArray array = new JSONArray();
+        assertEquals(0, array.length());
+        assertEquals("", array.join(" AND "));
+        try {
+            array.get(0);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            array.getBoolean(0);
+            fail();
+        } catch (JSONException e) {
+        }
+
+        assertEquals("[]", array.toString());
+        assertEquals("[]", array.toString(4));
+
+        // out of bounds is co-opted with defaulting
+        assertTrue(array.isNull(0));
+        assertNull(array.opt(0));
+        assertFalse(array.optBoolean(0));
+        assertTrue(array.optBoolean(0, true));
+
+        // bogus (but documented) behaviour: returns null rather than an empty object
+        assertNull(array.toJSONObject(new JSONArray()));
+    }
+
+    public void testEqualsAndHashCode() throws JSONException {
+        JSONArray a = new JSONArray();
+        JSONArray b = new JSONArray();
+        assertTrue(a.equals(b));
+        // bogus behavior: JSONArray overrides equals() but not hashCode().
+        assertEquals(a.hashCode(), b.hashCode());
+
+        a.put(true);
+        a.put(false);
+        b.put(true);
+        b.put(false);
+        assertTrue(a.equals(b));
+        assertEquals(a.hashCode(), b.hashCode());
+
+        b.put(true);
+        assertFalse(a.equals(b));
+        assertTrue(a.hashCode() != b.hashCode());
+    }
+
+    public void testBooleans() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put(true);
+        array.put(false);
+        array.put(2, false);
+        array.put(3, false);
+        array.put(2, true);
+        assertEquals("[true,false,true,false]", array.toString());
+        assertEquals(4, array.length());
+        assertEquals(Boolean.TRUE, array.get(0));
+        assertEquals(Boolean.FALSE, array.get(1));
+        assertEquals(Boolean.TRUE, array.get(2));
+        assertEquals(Boolean.FALSE, array.get(3));
+        assertFalse(array.isNull(0));
+        assertFalse(array.isNull(1));
+        assertFalse(array.isNull(2));
+        assertFalse(array.isNull(3));
+        assertEquals(true, array.optBoolean(0));
+        assertEquals(false, array.optBoolean(1, true));
+        assertEquals(true, array.optBoolean(2, false));
+        assertEquals(false, array.optBoolean(3));
+        assertEquals("true", array.getString(0));
+        assertEquals("false", array.getString(1));
+        assertEquals("true", array.optString(2));
+        assertEquals("false", array.optString(3, "x"));
+        assertEquals("[\n     true,\n     false,\n     true,\n     false\n]", array.toString(5));
+
+        JSONArray other = new JSONArray();
+        other.put(true);
+        other.put(false);
+        other.put(true);
+        other.put(false);
+        assertTrue(array.equals(other));
+        other.put(true);
+        assertFalse(array.equals(other));
+
+        other = new JSONArray();
+        other.put("true");
+        other.put("false");
+        other.put("truE");
+        other.put("FALSE");
+        assertFalse(array.equals(other));
+        assertFalse(other.equals(array));
+        assertEquals(true, other.getBoolean(0));
+        assertEquals(false, other.optBoolean(1, true));
+        assertEquals(true, other.optBoolean(2));
+        assertEquals(false, other.getBoolean(3));
+    }
+
+    public void testNulls() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put(3, null);
+        array.put(0, JSONObject.NULL);
+        assertEquals(4, array.length());
+        assertEquals("[null,null,null,null]", array.toString());
+
+        // bogus behaviour: there's 2 ways to represent null; each behaves differently!
+        assertEquals(JSONObject.NULL, array.get(0));
+        try {
+            assertEquals(null, array.get(1));
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            assertEquals(null, array.get(2));
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            assertEquals(null, array.get(3));
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals(JSONObject.NULL, array.opt(0));
+        assertEquals(null, array.opt(1));
+        assertEquals(null, array.opt(2));
+        assertEquals(null, array.opt(3));
+        assertTrue(array.isNull(0));
+        assertTrue(array.isNull(1));
+        assertTrue(array.isNull(2));
+        assertTrue(array.isNull(3));
+        assertEquals("null", array.optString(0));
+        assertEquals("", array.optString(1));
+        assertEquals("", array.optString(2));
+        assertEquals("", array.optString(3));
+    }
+
+    public void testNumbers() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put(Double.MIN_VALUE);
+        array.put(9223372036854775806L);
+        array.put(Double.MAX_VALUE);
+        array.put(-0d);
+        assertEquals(4, array.length());
+
+        // bogus behaviour: toString() and getString(int) return different values for -0d
+        assertEquals("[4.9E-324,9223372036854775806,1.7976931348623157E308,-0]", array.toString());
+
+        assertEquals(Double.MIN_VALUE, array.get(0));
+        assertEquals(9223372036854775806L, array.get(1));
+        assertEquals(Double.MAX_VALUE, array.get(2));
+        assertEquals(-0d, array.get(3));
+        assertEquals(Double.MIN_VALUE, array.getDouble(0));
+        assertEquals(9.223372036854776E18, array.getDouble(1));
+        assertEquals(Double.MAX_VALUE, array.getDouble(2));
+        assertEquals(-0d, array.getDouble(3));
+        assertEquals(0, array.getLong(0));
+        assertEquals(9223372036854775806L, array.getLong(1));
+        assertEquals(Long.MAX_VALUE, array.getLong(2));
+        assertEquals(0, array.getLong(3));
+        assertEquals(0, array.getInt(0));
+        assertEquals(-2, array.getInt(1));
+        assertEquals(Integer.MAX_VALUE, array.getInt(2));
+        assertEquals(0, array.getInt(3));
+        assertEquals(Double.MIN_VALUE, array.opt(0));
+        assertEquals(Double.MIN_VALUE, array.optDouble(0));
+        assertEquals(0, array.optLong(0, 1L));
+        assertEquals(0, array.optInt(0, 1));
+        assertEquals("4.9E-324", array.getString(0));
+        assertEquals("9223372036854775806", array.getString(1));
+        assertEquals("1.7976931348623157E308", array.getString(2));
+        assertEquals("-0.0", array.getString(3));
+
+        JSONArray other = new JSONArray();
+        other.put(Double.MIN_VALUE);
+        other.put(9223372036854775806L);
+        other.put(Double.MAX_VALUE);
+        other.put(-0d);
+        assertTrue(array.equals(other));
+        other.put(0, 0L);
+        assertFalse(array.equals(other));
+    }
+
+    public void testStrings() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put("true");
+        array.put("5.5");
+        array.put("9223372036854775806");
+        array.put("null");
+        array.put("5\"8' tall");
+        assertEquals(5, array.length());
+        assertEquals("[\"true\",\"5.5\",\"9223372036854775806\",\"null\",\"5\\\"8' tall\"]",
+                array.toString());
+
+        // although the documentation doesn't mention it, join() escapes text and wraps
+        // strings in quotes
+        assertEquals("\"true\" \"5.5\" \"9223372036854775806\" \"null\" \"5\\\"8' tall\"",
+                array.join(" "));
+
+        assertEquals("true", array.get(0));
+        assertEquals("null", array.getString(3));
+        assertEquals("5\"8' tall", array.getString(4));
+        assertEquals("true", array.opt(0));
+        assertEquals("5.5", array.optString(1));
+        assertEquals("9223372036854775806", array.optString(2, null));
+        assertEquals("null", array.optString(3, "-1"));
+        assertFalse(array.isNull(0));
+        assertFalse(array.isNull(3));
+
+        assertEquals(true, array.getBoolean(0));
+        assertEquals(true, array.optBoolean(0));
+        assertEquals(true, array.optBoolean(0, false));
+        assertEquals(0, array.optInt(0));
+        assertEquals(-2, array.optInt(0, -2));
+
+        assertEquals(5.5d, array.getDouble(1));
+        assertEquals(5, array.getLong(1));
+        assertEquals(5, array.getInt(1));
+        assertEquals(5, array.optInt(1, 3));
+
+        // The last digit of the string is a 6 but getLong returns a 7. It's probably parsing as a
+        // double and then converting that to a long. This is consistent with JavaScript.
+        assertEquals(9223372036854775807L, array.getLong(2));
+        assertEquals(9.223372036854776E18, array.getDouble(2));
+        assertEquals(Integer.MAX_VALUE, array.getInt(2));
+
+        assertFalse(array.isNull(3));
+        try {
+            array.getDouble(3);
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals(Double.NaN, array.optDouble(3));
+        assertEquals(-1.0d, array.optDouble(3, -1.0d));
+    }
+
+    public void testToJSONObject() throws JSONException {
+        JSONArray keys = new JSONArray();
+        keys.put("a");
+        keys.put("b");
+
+        JSONArray values = new JSONArray();
+        values.put(5.5d);
+        values.put(false);
+
+        JSONObject object = values.toJSONObject(keys);
+        assertEquals(5.5d, object.get("a"));
+        assertEquals(false, object.get("b"));
+
+        keys.put(0, "a");
+        values.put(0, 11.0d);
+        assertEquals(5.5d, object.get("a"));
+    }
+
+    public void testToJSONObjectWithNulls() throws JSONException {
+        JSONArray keys = new JSONArray();
+        keys.put("a");
+        keys.put("b");
+
+        JSONArray values = new JSONArray();
+        values.put(5.5d);
+        values.put(null);
+
+        // bogus behaviour: null values are stripped 
+        JSONObject object = values.toJSONObject(keys);
+        assertEquals(1, object.length());
+        assertFalse(object.has("b"));
+        assertEquals("{\"a\":5.5}", object.toString());
+    }
+
+    public void testPutUnsupportedNumbers() throws JSONException {
+        JSONArray array = new JSONArray();
+
+        try {
+            array.put(Double.NaN);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            array.put(0, Double.NEGATIVE_INFINITY);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            array.put(0, Double.POSITIVE_INFINITY);
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testCreateWithUnsupportedNumbers() throws JSONException {
+        JSONArray array = new JSONArray(Arrays.asList(5.5, Double.NaN));
+        assertEquals(2, array.length());
+        assertEquals(5.5, array.getDouble(0));
+        assertEquals(Double.NaN, array.getDouble(1));
+    }
+
+    public void testToStringWithUnsupportedNumbers() throws JSONException {
+        // bogus behaviour: when the array contains an unsupported number, toString returns null
+        JSONArray array = new JSONArray(Arrays.asList(5.5, Double.NaN));
+        assertNull(array.toString());
+    }
+
+    public void testCreate() throws JSONException {
+        JSONArray array = new JSONArray(Arrays.asList(5.5, true));
+        assertEquals(2, array.length());
+        assertEquals(5.5, array.getDouble(0));
+        assertEquals(true, array.get(1));
+        assertEquals("[5.5,true]", array.toString());
+    }
+
+    public void testParsingConstructor() {
+        fail("TODO");
+    }
+}
diff --git a/json/src/test/java/org/json/JSONStringerTest.java b/json/src/test/java/org/json/JSONStringerTest.java
new file mode 100644
index 0000000..a30df9e
--- /dev/null
+++ b/json/src/test/java/org/json/JSONStringerTest.java
@@ -0,0 +1,356 @@
+/**
+ * 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 org.json;
+
+import junit.framework.TestCase;
+
+/**
+ * This black box test was written without inspecting the non-free org.json sourcecode.
+ */
+public class JSONStringerTest extends TestCase {
+
+    public void testEmptyStringer() {
+        // bogus behaviour: why isn't this the empty string?
+        assertNull(new JSONStringer().toString());
+    }
+
+    public void testValueJSONNull() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.value(JSONObject.NULL);
+        stringer.endArray();
+        assertEquals("[null]", stringer.toString());
+    }
+
+    public void testEmptyObject() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.object();
+        stringer.endObject();
+        assertEquals("{}", stringer.toString());
+    }
+
+    public void testEmptyArray() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.endArray();
+        assertEquals("[]", stringer.toString());
+    }
+
+    public void testArray() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.value(false);
+        stringer.value(5.0);
+        stringer.value(5L);
+        stringer.value("five");
+        stringer.value(null);
+        stringer.endArray();
+        assertEquals("[false,5,5,\"five\",null]", stringer.toString());
+    }
+
+    public void testKeyValue() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.object();
+        stringer.key("a").value(false);
+        stringer.key("b").value(5.0);
+        stringer.key("c").value(5L);
+        stringer.key("d").value("five");
+        stringer.key("e").value(null);
+        stringer.endObject();
+        assertEquals("{\"a\":false," +
+                "\"b\":5," +
+                "\"c\":5," +
+                "\"d\":\"five\"," +
+                "\"e\":null}", stringer.toString());
+    }
+
+    /**
+     * Test what happens when extreme values are emitted. Such values are likely
+     * to be rounded during parsing.
+     */
+    public void testNumericRepresentations() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.value(Long.MAX_VALUE);
+        stringer.value(Double.MIN_VALUE);
+        stringer.endArray();
+        assertEquals("[9223372036854775807,4.9E-324]", stringer.toString());
+    }
+
+    public void testWeirdNumbers() throws JSONException {
+        try {
+            new JSONStringer().array().value(Double.NaN);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().array().value(Double.NEGATIVE_INFINITY);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().array().value(Double.POSITIVE_INFINITY);
+            fail();
+        } catch (JSONException e) {
+        }
+
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.value(-0.0d);
+        stringer.value(0.0d);
+        stringer.endArray();
+        assertEquals("[-0,0]", stringer.toString());
+    }
+
+    public void testMismatchedScopes() {
+        try {
+            new JSONStringer().key("a");
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().value("a");
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().endObject();
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().endArray();
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().array().endObject();
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().object().endArray();
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().object().key("a").key("a");
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONStringer().object().value(false);
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testNullKey() {
+        try {
+            new JSONStringer().object().key(null);
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testRepeatedKey() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.object();
+        stringer.key("a").value(true);
+        stringer.key("a").value(false);
+        stringer.endObject();
+        // JSONStringer doesn't attempt to detect duplicates
+        assertEquals("{\"a\":true,\"a\":false}", stringer.toString());
+    }
+
+    public void testEmptyKey() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.object();
+        stringer.key("").value(false);
+        stringer.endObject();
+        assertEquals("{\"\":false}", stringer.toString()); // legit behaviour! 
+    }
+
+    public void testEscaping() throws JSONException {
+        assertEscapedAllWays("a", "a");
+        assertEscapedAllWays("a\"", "a\\\"");
+        assertEscapedAllWays("\"", "\\\"");
+        assertEscapedAllWays(":", ":");
+        assertEscapedAllWays(",", ",");
+        assertEscapedAllWays("\n", "\\n");
+        assertEscapedAllWays("\t", "\\t");
+        assertEscapedAllWays(" ", " ");
+        assertEscapedAllWays("\\", "\\\\");
+        assertEscapedAllWays("{", "{");
+        assertEscapedAllWays("}", "}");
+        assertEscapedAllWays("[", "[");
+        assertEscapedAllWays("]", "]");
+
+        // how does it decide which characters to escape?
+        assertEscapedAllWays("\0", "\\u0000");
+        assertEscapedAllWays("\u0019", "\\u0019");
+        assertEscapedAllWays("\u0020", " ");
+    }
+
+    private void assertEscapedAllWays(String original, String escaped) throws JSONException {
+        assertEquals("{\"" + escaped + "\":false}",
+                new JSONStringer().object().key(original).value(false).endObject().toString());
+        assertEquals("{\"a\":\"" + escaped + "\"}",
+                new JSONStringer().object().key("a").value(original).endObject().toString());
+        assertEquals("[\"" + escaped + "\"]",
+                new JSONStringer().array().value(original).endArray().toString());
+    }
+
+    public void testJSONArrayAsValue() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put(false);
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.value(array);
+        stringer.endArray();
+        assertEquals("[[false]]", stringer.toString());
+    }
+
+    public void testJSONObjectAsValue() throws JSONException {
+        JSONObject object = new JSONObject();
+        object.put("a", false);
+        JSONStringer stringer = new JSONStringer();
+        stringer.object();
+        stringer.key("b").value(object);
+        stringer.endObject();
+        assertEquals("{\"b\":{\"a\":false}}", stringer.toString());
+    }
+
+    public void testArrayNestingMaxDepthIs20() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.array();
+        }
+        for (int i = 0; i < 20; i++) {
+            stringer.endArray();
+        }
+        assertEquals("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]", stringer.toString());
+
+        stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.array();
+        }
+        try {
+            stringer.array();
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testObjectNestingMaxDepthIs20() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.object();
+            stringer.key("a");
+        }
+        stringer.value(false);
+        for (int i = 0; i < 20; i++) {
+            stringer.endObject();
+        }
+        assertEquals("{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":" +
+                "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":false" +
+                "}}}}}}}}}}}}}}}}}}}}", stringer.toString());
+
+        stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.object();
+            stringer.key("a");
+        }
+        try {
+            stringer.object();
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testMixedMaxDepth() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        for (int i = 0; i < 20; i+=2) {
+            stringer.array();
+            stringer.object();
+            stringer.key("a");
+        }
+        stringer.value(false);
+        for (int i = 0; i < 20; i+=2) {
+            stringer.endObject();
+            stringer.endArray();
+        }
+        assertEquals("[{\"a\":[{\"a\":[{\"a\":[{\"a\":[{\"a\":" +
+                "[{\"a\":[{\"a\":[{\"a\":[{\"a\":[{\"a\":false" +
+                "}]}]}]}]}]}]}]}]}]}]", stringer.toString());
+
+        stringer = new JSONStringer();
+        for (int i = 0; i < 20; i+=2) {
+            stringer.array();
+            stringer.object();
+            stringer.key("a");
+        }
+        try {
+            stringer.array();
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testMaxDepthWithArrayValue() throws JSONException {
+        JSONArray array = new JSONArray();
+        array.put(false);
+
+        JSONStringer stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.array();
+        }
+        stringer.value(array);
+        for (int i = 0; i < 20; i++) {
+            stringer.endArray();
+        }
+        assertEquals("[[[[[[[[[[[[[[[[[[[[[false]]]]]]]]]]]]]]]]]]]]]", stringer.toString());
+    }
+
+    public void testMaxDepthWithObjectValue() throws JSONException {
+        JSONObject object = new JSONObject();
+        object.put("a", false);
+        JSONStringer stringer = new JSONStringer();
+        for (int i = 0; i < 20; i++) {
+            stringer.object();
+            stringer.key("b");
+        }
+        stringer.value(object);
+        for (int i = 0; i < 20; i++) {
+            stringer.endObject();
+        }
+        assertEquals("{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":" +
+                "{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":{\"b\":" +
+                "{\"a\":false}}}}}}}}}}}}}}}}}}}}}", stringer.toString());
+    }
+
+    public void testMultipleRoots() throws JSONException {
+        JSONStringer stringer = new JSONStringer();
+        stringer.array();
+        stringer.endArray();
+        try {
+            stringer.object();
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+}
diff --git a/json/src/test/java/org/json/JSONTokenerTest.java b/json/src/test/java/org/json/JSONTokenerTest.java
new file mode 100644
index 0000000..1409a3b
--- /dev/null
+++ b/json/src/test/java/org/json/JSONTokenerTest.java
@@ -0,0 +1,565 @@
+/**
+ * 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 org.json;
+
+import junit.framework.TestCase;
+
+/**
+ * This black box test was written without inspecting the non-free org.json sourcecode.
+ */
+public class JSONTokenerTest extends TestCase {
+
+    public void testNulls() throws JSONException {
+        // bogus behaviour: JSONTokener accepts null, only to fail later on almost all APIs.
+        new JSONTokener(null).back();
+
+        try {
+            new JSONTokener(null).more();
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).next();
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).next(3);
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).next('A');
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).nextClean();
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).nextString('"');
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).nextTo('A');
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).nextTo("ABC");
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).nextValue();
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).skipPast("ABC");
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        try {
+            new JSONTokener(null).skipTo('A');
+            fail();
+        } catch (NullPointerException e) {
+        }
+
+        assertEquals("foo! at character 0 of null",
+                new JSONTokener(null).syntaxError("foo!").getMessage());
+
+        assertEquals(" at character 0 of null", new JSONTokener(null).toString());
+    }
+
+    public void testEmptyString() throws JSONException {
+        JSONTokener backTokener = new JSONTokener("");
+        backTokener.back();
+        assertEquals(" at character 0 of ", backTokener.toString());
+        assertFalse(new JSONTokener("").more());
+        assertEquals('\0', new JSONTokener("").next());
+        try {
+            new JSONTokener("").next(3);
+            fail();
+        } catch (JSONException expected) {
+        }
+        try {
+            new JSONTokener("").next('A');
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals('\0', new JSONTokener("").nextClean());
+        try {
+            new JSONTokener("").nextString('"');
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals("", new JSONTokener("").nextTo('A'));
+        assertEquals("", new JSONTokener("").nextTo("ABC"));
+        try {
+            new JSONTokener("").nextValue();
+            fail();
+        } catch (JSONException e) {
+        }
+        new JSONTokener("").skipPast("ABC");
+        assertEquals('\0', new JSONTokener("").skipTo('A'));
+        assertEquals("foo! at character 0 of ",
+                new JSONTokener("").syntaxError("foo!").getMessage());
+        assertEquals(" at character 0 of ", new JSONTokener("").toString());
+    }
+
+    public void testCharacterNavigation() throws JSONException {
+        JSONTokener abcdeTokener = new JSONTokener("ABCDE");
+        assertEquals('A', abcdeTokener.next());
+        assertEquals('B', abcdeTokener.next('B'));
+        assertEquals("CD", abcdeTokener.next(2));
+        try {
+            abcdeTokener.next(2);
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals('E', abcdeTokener.nextClean());
+        assertEquals('\0', abcdeTokener.next());
+        try {
+            // bogus behaviour: returning an empty string should be valid
+            abcdeTokener.next(0);
+            fail();
+        } catch (JSONException e) {
+        }
+        assertFalse(abcdeTokener.more());
+        abcdeTokener.back();
+        assertTrue(abcdeTokener.more());
+        assertEquals('E', abcdeTokener.next());
+    }
+
+    public void testBackNextAndMore() throws JSONException {
+        JSONTokener abcTokener = new JSONTokener("ABC");
+        assertTrue(abcTokener.more());
+        abcTokener.next();
+        abcTokener.next();
+        assertTrue(abcTokener.more());
+        abcTokener.next();
+        assertFalse(abcTokener.more());
+        abcTokener.back();
+        assertTrue(abcTokener.more());
+        abcTokener.next();
+        assertFalse(abcTokener.more());
+        abcTokener.back();
+        abcTokener.back();
+        abcTokener.back();
+        abcTokener.back(); // bogus behaviour: you can back up before the beginning of a String
+        assertEquals('A', abcTokener.next());
+    }
+
+    public void testNextMatching() throws JSONException {
+        JSONTokener abcdTokener = new JSONTokener("ABCD");
+        assertEquals('A', abcdTokener.next('A'));
+        try {
+            abcdTokener.next('C'); // although it failed, this op consumes a character of input
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals('C', abcdTokener.next('C'));
+        assertEquals('D', abcdTokener.next('D'));
+        try {
+            abcdTokener.next('E');
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testNextN() throws JSONException {
+        JSONTokener abcdeTokener = new JSONTokener("ABCDEF");
+        assertEquals("", abcdeTokener.next(0));
+        try {
+            abcdeTokener.next(7);
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals("ABC", abcdeTokener.next(3));
+        try {
+            abcdeTokener.next(4);
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            // bogus behaviour: there should be 3 characters left, but there must be an off-by-one
+            // error in the implementation.
+            assertEquals("DEF", abcdeTokener.next(3));
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals("DE", abcdeTokener.next(2));
+        assertEquals('F', abcdeTokener.next());
+        try {
+            // bogus behaviour: returning an empty string should be valid
+            abcdeTokener.next(0);
+            fail();
+        } catch (JSONException e) {
+        }
+        abcdeTokener.back();
+        abcdeTokener.back();
+        abcdeTokener.back();
+        assertEquals("DE", abcdeTokener.next(2));
+        assertEquals('F', abcdeTokener.next());
+    }
+
+    public void testNextCleanComments() throws JSONException {
+        JSONTokener tokener = new JSONTokener(
+                "  A  /*XX*/B/*XX//XX\n//XX\nXX*/C//X//X//X\nD/*X*///X\n");
+        assertEquals('A', tokener.nextClean());
+        assertEquals('B', tokener.nextClean());
+        assertEquals('C', tokener.nextClean());
+        assertEquals('D', tokener.nextClean());
+        assertEquals('\0', tokener.nextClean());
+    }
+
+    public void testNextCleanTrailingOpenComment() throws JSONException {
+        try {
+            new JSONTokener("  /* ").nextClean();
+            fail();
+        } catch (JSONException e) {
+        }
+        assertEquals('\0', new JSONTokener("  // ").nextClean());
+    }
+
+    public void testNextCleanNewlineDelimiters() throws JSONException {
+        assertEquals('B', new JSONTokener("  // \r\n  B ").nextClean());
+        assertEquals('B', new JSONTokener("  // \n  B ").nextClean());
+        assertEquals('B', new JSONTokener("  // \r  B ").nextClean());
+    }
+
+    /**
+     * Tests which characters tokener treats as ignorable whitespace. See Kevin Bourrillion's
+     * <a href="https://spreadsheets.google.com/pub?key=pd8dAQyHbdewRsnE5x5GzKQ">list
+     * of whitespace characters</a>.
+     */
+    public void testNextCleanWhitespace() throws JSONException {
+        // This behaviour contradicts the JSON spec. It claims the only space
+        // characters are space, tab, newline and carriage return. But it treats
+        // many characters like whitespace! These are the same whitespace
+        // characters used by String.trim(), with the exception of '\0'.
+        assertEquals("character tabulation",      'A', new JSONTokener("\u0009A").nextClean());
+        assertEquals("line feed",                 'A', new JSONTokener("\nA").nextClean());
+        assertEquals("line tabulation",           'A', new JSONTokener("\u000bA").nextClean());
+        assertEquals("form feed",                 'A', new JSONTokener("\u000cA").nextClean());
+        assertEquals("carriage return",           'A', new JSONTokener("\rA").nextClean());
+        assertEquals("information separator 4",   'A', new JSONTokener("\u001cA").nextClean());
+        assertEquals("information separator 3",   'A', new JSONTokener("\u001dA").nextClean());
+        assertEquals("information separator 2",   'A', new JSONTokener("\u001eA").nextClean());
+        assertEquals("information separator 1",   'A', new JSONTokener("\u001fA").nextClean());
+        assertEquals("space",                     'A', new JSONTokener("\u0020A").nextClean());
+        for (char c = '\u0002'; c < ' '; c++) {
+            assertEquals('A', new JSONTokener(new String(new char[] { ' ', c, 'A' })).nextClean());
+        }
+
+        // These characters are neither whitespace in the JSON spec nor the implementation
+        assertEquals("null",                      '\u0000', new JSONTokener("\u0000A").nextClean());
+        assertEquals("next line",                 '\u0085', new JSONTokener("\u0085A").nextClean());
+        assertEquals("non-breaking space",        '\u00a0', new JSONTokener("\u00a0A").nextClean());
+        assertEquals("ogham space mark",          '\u1680', new JSONTokener("\u1680A").nextClean());
+        assertEquals("mongolian vowel separator", '\u180e', new JSONTokener("\u180eA").nextClean());
+        assertEquals("en quad",                   '\u2000', new JSONTokener("\u2000A").nextClean());
+        assertEquals("em quad",                   '\u2001', new JSONTokener("\u2001A").nextClean());
+        assertEquals("en space",                  '\u2002', new JSONTokener("\u2002A").nextClean());
+        assertEquals("em space",                  '\u2003', new JSONTokener("\u2003A").nextClean());
+        assertEquals("three-per-em space",        '\u2004', new JSONTokener("\u2004A").nextClean());
+        assertEquals("four-per-em space",         '\u2005', new JSONTokener("\u2005A").nextClean());
+        assertEquals("six-per-em space",          '\u2006', new JSONTokener("\u2006A").nextClean());
+        assertEquals("figure space",              '\u2007', new JSONTokener("\u2007A").nextClean());
+        assertEquals("punctuation space",         '\u2008', new JSONTokener("\u2008A").nextClean());
+        assertEquals("thin space",                '\u2009', new JSONTokener("\u2009A").nextClean());
+        assertEquals("hair space",                '\u200a', new JSONTokener("\u200aA").nextClean());
+        assertEquals("zero-width space",          '\u200b', new JSONTokener("\u200bA").nextClean());
+        assertEquals("left-to-right mark",        '\u200e', new JSONTokener("\u200eA").nextClean());
+        assertEquals("right-to-left mark",        '\u200f', new JSONTokener("\u200fA").nextClean());
+        assertEquals("line separator",            '\u2028', new JSONTokener("\u2028A").nextClean());
+        assertEquals("paragraph separator",       '\u2029', new JSONTokener("\u2029A").nextClean());
+        assertEquals("narrow non-breaking space", '\u202f', new JSONTokener("\u202fA").nextClean());
+        assertEquals("medium mathematical space", '\u205f', new JSONTokener("\u205fA").nextClean());
+        assertEquals("ideographic space",         '\u3000', new JSONTokener("\u3000A").nextClean());
+    }
+
+    public void testNextString() throws JSONException {
+        assertEquals("", new JSONTokener("'").nextString('\''));
+        assertEquals("", new JSONTokener("\"").nextString('\"'));
+        assertEquals("ABC", new JSONTokener("ABC'DEF").nextString('\''));
+        assertEquals("ABC", new JSONTokener("ABC'''DEF").nextString('\''));
+
+        // nextString permits slash-escaping of arbitrary characters! 
+        assertEquals("ABC", new JSONTokener("A\\B\\C'DEF").nextString('\''));
+
+        JSONTokener tokener = new JSONTokener(" 'abc' 'def' \"ghi\"");
+        tokener.next();
+        assertEquals('\'', tokener.next());
+        assertEquals("abc", tokener.nextString('\''));
+        tokener.next();
+        assertEquals('\'', tokener.next());
+        assertEquals("def", tokener.nextString('\''));
+        tokener.next();
+        assertEquals('"', tokener.next());
+        assertEquals("ghi", tokener.nextString('\"'));
+        assertFalse(tokener.more());
+    }
+
+    public void testNextStringNoDelimiter() throws JSONException {
+        try {
+            new JSONTokener("").nextString('\'');
+            fail();
+        } catch (JSONException e) {
+        }
+
+        JSONTokener tokener = new JSONTokener(" 'abc");
+        tokener.next();
+        tokener.next();
+        try {
+            tokener.next('\'');
+            fail();
+        } catch (JSONException e) {
+        }
+    }
+
+    public void testNextStringEscapedQuote() throws JSONException {
+        try {
+            new JSONTokener("abc\\").nextString('"');
+            fail();
+        } catch (JSONException e) {
+        }
+
+        // we're mixing Java escaping like \" and JavaScript escaping like \\\"
+        // which makes these tests extra tricky to read!
+        assertEquals("abc\"def", new JSONTokener("abc\\\"def\"ghi").nextString('"'));
+        assertEquals("abc\\def", new JSONTokener("abc\\\\def\"ghi").nextString('"'));
+        assertEquals("abc/def", new JSONTokener("abc\\/def\"ghi").nextString('"'));
+        assertEquals("abc\bdef", new JSONTokener("abc\\bdef\"ghi").nextString('"'));
+        assertEquals("abc\fdef", new JSONTokener("abc\\fdef\"ghi").nextString('"'));
+        assertEquals("abc\ndef", new JSONTokener("abc\\ndef\"ghi").nextString('"'));
+        assertEquals("abc\rdef", new JSONTokener("abc\\rdef\"ghi").nextString('"'));
+        assertEquals("abc\tdef", new JSONTokener("abc\\tdef\"ghi").nextString('"'));
+    }
+
+    public void testNextStringUnicodeEscaped() throws JSONException {
+        // we're mixing Java escaping like \\ and JavaScript escaping like \\u
+        assertEquals("abc def", new JSONTokener("abc\\u0020def\"ghi").nextString('"'));
+        assertEquals("abcU0020def", new JSONTokener("abc\\U0020def\"ghi").nextString('"'));
+
+        // JSON requires 4 hex characters after a unicode escape
+        try {
+            new JSONTokener("abc\\u002\"").nextString('"');
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONTokener("abc\\u").nextString('"');
+            fail();
+        } catch (JSONException e) {
+        }
+        try {
+            new JSONTokener("abc\\u    \"").nextString('"');
+            fail();
+        } catch (NumberFormatException e) {
+        }
+        assertEquals("abc\"def", new JSONTokener("abc\\u0022def\"ghi").nextString('"'));
+        try {
+            new JSONTokener("abc\\u000G\"").nextString('"');
+            fail();
+        } catch (NumberFormatException e) {
+        }
+    }
+
+    public void testNextStringNonQuote() throws JSONException {
+        assertEquals("AB", new JSONTokener("ABC").nextString('C'));
+        assertEquals("ABCD", new JSONTokener("AB\\CDC").nextString('C'));
+        assertEquals("AB\nC", new JSONTokener("AB\\nCn").nextString('n'));
+    }
+
+    public void testNextTo() throws JSONException {
+        assertEquals("ABC", new JSONTokener("ABCDEFG").nextTo("DHI"));
+        assertEquals("ABCDEF", new JSONTokener("ABCDEF").nextTo(""));
+
+        JSONTokener tokener = new JSONTokener("ABC\rDEF\nGHI\r\nJKL");
+        assertEquals("ABC", tokener.nextTo("M"));
+        assertEquals('\r', tokener.next());
+        assertEquals("DEF", tokener.nextTo("M"));
+        assertEquals('\n', tokener.next());
+        assertEquals("GHI", tokener.nextTo("M"));
+        assertEquals('\r', tokener.next());
+        assertEquals('\n', tokener.next());
+        assertEquals("JKL", tokener.nextTo("M"));
+
+        tokener = new JSONTokener("ABCDEFGHI");
+        assertEquals("ABC", tokener.nextTo("DEF"));
+        assertEquals("", tokener.nextTo("DEF"));
+        assertEquals('D', tokener.next());
+        assertEquals("", tokener.nextTo("DEF"));
+        assertEquals('E', tokener.next());
+        assertEquals("", tokener.nextTo("DEF"));
+        assertEquals('F', tokener.next());
+        assertEquals("GHI", tokener.nextTo("DEF"));
+        assertEquals("", tokener.nextTo("DEF"));
+
+        tokener = new JSONTokener(" \t \fABC \t DEF");
+        assertEquals("ABC", tokener.nextTo("DEF"));
+        assertEquals('D', tokener.next());
+
+        tokener = new JSONTokener(" \t \fABC \n DEF");
+        assertEquals("ABC", tokener.nextTo("\n"));
+        assertEquals("", tokener.nextTo("\n"));
+
+        // Bogus behaviour: the tokener stops after \0 always
+        tokener = new JSONTokener(" \0\t \fABC \n DEF");
+        assertEquals("", tokener.nextTo("D"));
+        assertEquals('\t', tokener.next());
+        assertEquals("ABC", tokener.nextTo("D"));
+        tokener = new JSONTokener("ABC\0DEF");
+        assertEquals("ABC", tokener.nextTo("\0"));
+        assertEquals("DEF", tokener.nextTo("\0"));
+
+        tokener = new JSONTokener("");
+        try {
+            tokener.nextTo(null);
+            fail();
+        } catch (NullPointerException e) {
+        }
+    }
+
+    public void testSkipPast() {
+        JSONTokener tokener = new JSONTokener("ABCDEF");
+        tokener.skipPast("ABC");
+        assertEquals('D', tokener.next());
+        tokener.skipPast("EF");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABCDEF");
+        tokener.skipPast("ABCDEF");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABCDEF");
+        tokener.skipPast("G");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABC\0ABC");
+        tokener.skipPast("ABC");
+        assertEquals('\0', tokener.next());
+        assertEquals('A', tokener.next());
+
+        tokener = new JSONTokener("\0ABC");
+        tokener.skipPast("ABC");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABC\nDEF");
+        tokener.skipPast("DEF");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABC");
+        tokener.skipPast("ABCDEF");
+        assertEquals('\0', tokener.next());
+
+        tokener = new JSONTokener("ABCDABCDABCD");
+        tokener.skipPast("ABC");
+        assertEquals('D', tokener.next());
+        tokener.skipPast("ABC");
+        assertEquals('D', tokener.next());
+        tokener.skipPast("ABC");
+        assertEquals('D', tokener.next());
+
+        tokener = new JSONTokener("");
+        try {
+            tokener.skipPast(null);
+            fail();
+        } catch (NullPointerException e) {
+        }
+    }
+
+    public void testSkipTo() {
+        JSONTokener tokener = new JSONTokener("ABCDEF");
+        tokener.skipTo('A');
+        assertEquals('A', tokener.next());
+        tokener.skipTo('D');
+        assertEquals('D', tokener.next());
+        tokener.skipTo('G');
+        assertEquals('E', tokener.next());
+        tokener.skipTo('A');
+        assertEquals('F', tokener.next());
+
+        tokener = new JSONTokener("ABC\0DEF");
+        tokener.skipTo('F');
+        // bogus behaviour: skipTo gives up when it sees '\0'
+        assertEquals('A', tokener.next());
+
+        tokener = new JSONTokener("ABC\nDEF");
+        tokener.skipTo('F');
+        assertEquals('F', tokener.next());
+
+        tokener = new JSONTokener("ABCfDEF");
+        tokener.skipTo('F');
+        assertEquals('F', tokener.next());
+
+        tokener = new JSONTokener("ABC/* DEF */");
+        tokener.skipTo('D');
+        assertEquals('D', tokener.next());
+    }
+
+    public void testDehexchar() {
+        assertEquals( 0, JSONTokener.dehexchar('0'));
+        assertEquals( 1, JSONTokener.dehexchar('1'));
+        assertEquals( 2, JSONTokener.dehexchar('2'));
+        assertEquals( 3, JSONTokener.dehexchar('3'));
+        assertEquals( 4, JSONTokener.dehexchar('4'));
+        assertEquals( 5, JSONTokener.dehexchar('5'));
+        assertEquals( 6, JSONTokener.dehexchar('6'));
+        assertEquals( 7, JSONTokener.dehexchar('7'));
+        assertEquals( 8, JSONTokener.dehexchar('8'));
+        assertEquals( 9, JSONTokener.dehexchar('9'));
+        assertEquals(10, JSONTokener.dehexchar('A'));
+        assertEquals(11, JSONTokener.dehexchar('B'));
+        assertEquals(12, JSONTokener.dehexchar('C'));
+        assertEquals(13, JSONTokener.dehexchar('D'));
+        assertEquals(14, JSONTokener.dehexchar('E'));
+        assertEquals(15, JSONTokener.dehexchar('F'));
+        assertEquals(10, JSONTokener.dehexchar('a'));
+        assertEquals(11, JSONTokener.dehexchar('b'));
+        assertEquals(12, JSONTokener.dehexchar('c'));
+        assertEquals(13, JSONTokener.dehexchar('d'));
+        assertEquals(14, JSONTokener.dehexchar('e'));
+        assertEquals(15, JSONTokener.dehexchar('f'));
+
+        for (int c = 0; c <= 0xFFFF; c++) {
+            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) {
+                continue;
+            }
+            assertEquals("dehexchar " + c, -1, JSONTokener.dehexchar((char) c));
+        }
+    }
+
+    public void testNextValue() {
+        fail("TODO");
+    }
+}
diff --git a/luni/src/test/java/org/apache/harmony/luni/tests/java/util/NullBenchmarkSuite.java b/luni/src/test/java/org/apache/harmony/luni/tests/java/util/NullBenchmarkSuite.java
deleted file mode 100644
index 52f383c..0000000
--- a/luni/src/test/java/org/apache/harmony/luni/tests/java/util/NullBenchmarkSuite.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 org.apache.harmony.luni.tests.java.util;
-
-import com.google.caliper.Benchmark;
-import com.google.caliper.SimpleBenchmark;
-
-/**
- * This class exists only to force a dependency from our libraries on Caliper,
- * our micro benchmarking framework. 
- */
-public class NullBenchmarkSuite extends SimpleBenchmark {
-
-    public void timeNullBenchmark(int trials) throws Exception {
-            for (int i = 0; i < trials; i++) {
-                // code under test goes here!
-            }
-    }
-}
diff --git a/luni/src/test/java/tests/AllTests.java b/luni/src/test/java/tests/AllTests.java
index 950156e..864a3ed 100644
--- a/luni/src/test/java/tests/AllTests.java
+++ b/luni/src/test/java/tests/AllTests.java
@@ -66,6 +66,7 @@
         suite.addTest(java.util.AllTests.suite());
         suite.addTest(javax.xml.parsers.AllTests.suite());
         suite.addTest(org.apache.harmony.luni.platform.AllTests.suite());
+        suite.addTest(org.json.AllTests.suite());
         suite.addTest(tests.api.org.apache.harmony.kernel.dalvik.AllTests.suite());
         
         return suite;
diff --git a/math/src/main/java/java/math/BigInt.java b/math/src/main/java/java/math/BigInt.java
index 3ba1da2..1eae2e0 100644
--- a/math/src/main/java/java/math/BigInt.java
+++ b/math/src/main/java/java/math/BigInt.java
@@ -82,7 +82,6 @@
     public static int consumeErrors(StringBuilder sb) {
         int cnt = 0;
         int e, reason;
-        boolean first = true;
         while ((e = NativeBN.ERR_get_error()) != 0) {
             reason = e & 255;
             if (reason == 103) {
@@ -96,7 +95,6 @@
             if (reason == 65) {
                 throw new OutOfMemoryError();
             }
-            if (!first) { sb.append(" *** "); first = false; }
             sb.append(e).append(": ");
             String s = NativeBN.ERR_error_string(e);
             sb.append(s);
diff --git a/support/src/test/java/tests/support/resource/Support_Resources.java b/support/src/test/java/tests/support/resource/Support_Resources.java
index 53a8925..67b6001 100644
--- a/support/src/test/java/tests/support/resource/Support_Resources.java
+++ b/support/src/test/java/tests/support/resource/Support_Resources.java
@@ -205,4 +205,20 @@
             throw new RuntimeException("Failed to load resource: " + name);
         }
     }
+
+    public static File resourceToTempFile(String path) throws IOException {
+        File f = File.createTempFile("out", ".xml");
+        f.deleteOnExit();
+        FileOutputStream out = new FileOutputStream(f);
+
+        InputStream xml = Support_Resources.class.getResourceAsStream(path);
+        int b;
+        while ((b = xml.read()) != -1) {
+            out.write(b);
+        }
+        out.flush();
+        out.close();
+        xml.close();
+        return f;
+    }
 }
diff --git a/tools/runner/Android.mk b/tools/runner/Android.mk
index bc7c5b7..b208219 100644
--- a/tools/runner/Android.mk
+++ b/tools/runner/Android.mk
@@ -1,69 +1,22 @@
 LOCAL_PATH:= $(call my-dir)
 
+# build DalvikRunner from the source under java/.
 include $(CLEAR_VARS)
-
-ext_dirs := \
-        ../../../../external/jsr305/ri/src/main/java \
-        ../../../../external/guava/src \
-        ../../../../external/caliper/src
-
-ext_src_files := $(call all-java-files-under,$(ext_dirs))
-
-LOCAL_SRC_FILES := \
-        $(ext_src_files) \
-        java/dalvik/runner/Aapt.java \
-        java/dalvik/runner/Adb.java \
-        java/dalvik/runner/ActivityMode.java \
-        java/dalvik/runner/CaliperFinder.java \
-        java/dalvik/runner/CaliperRunner.java \
-        java/dalvik/runner/Classpath.java \
-        java/dalvik/runner/CodeFinder.java \
-        java/dalvik/runner/Command.java \
-        java/dalvik/runner/CommandFailedException.java \
-        java/dalvik/runner/DalvikRunner.java \
-        java/dalvik/runner/DeviceDalvikVm.java \
-        java/dalvik/runner/Driver.java \
-        java/dalvik/runner/Dx.java \
-        java/dalvik/runner/Environment.java \
-        java/dalvik/runner/EnvironmentDevice.java \
-        java/dalvik/runner/EnvironmentHost.java \
-        java/dalvik/runner/ExpectedResult.java \
-        java/dalvik/runner/JUnitFinder.java \
-        java/dalvik/runner/JUnitRunner.java \
-        java/dalvik/runner/JavaVm.java \
-        java/dalvik/runner/Javac.java \
-        java/dalvik/runner/JtregFinder.java \
-        java/dalvik/runner/JtregRunner.java \
-        java/dalvik/runner/MainFinder.java \
-        java/dalvik/runner/MainRunner.java \
-        java/dalvik/runner/Mkdir.java \
-        java/dalvik/runner/Mode.java \
-        java/dalvik/runner/NamingPatternCodeFinder.java \
-        java/dalvik/runner/Option.java \
-        java/dalvik/runner/OptionParser.java \
-        java/dalvik/runner/Result.java \
-        java/dalvik/runner/Rm.java \
-        java/dalvik/runner/Runner.java \
-        java/dalvik/runner/Strings.java \
-        java/dalvik/runner/TestProperties.java \
-        java/dalvik/runner/TestRun.java \
-        java/dalvik/runner/TestRunner.java \
-        java/dalvik/runner/Threads.java \
-        java/dalvik/runner/Vm.java \
-        java/dalvik/runner/XmlReportPrinter.java \
-
+LOCAL_SRC_FILES :=  $(call all-java-files-under,java)
 LOCAL_MODULE:= dalvik_runner
-LOCAL_STATIC_JAVA_LIBRARIES := javatest jh jtreg kxml2-2.3.0
-
+LOCAL_STATIC_JAVA_LIBRARIES := caliper javatest jh jtreg kxml2-2.3.0
 # TODO this only works when junit is already built...
 LOCAL_JAVA_LIBRARIES := junit
-
 LOCAL_JAVACFLAGS := -Werror -Xlint:unchecked
-
 include $(BUILD_HOST_JAVA_LIBRARY)
 
 include $(call all-subdir-makefiles)
 
+# prebuilt caliper.jar
+include $(CLEAR_VARS)
+LOCAL_PREBUILT_JAVA_LIBRARIES := caliper:lib/caliper.jar
+include $(BUILD_HOST_PREBUILT)
+
 # prebuilt javatest.jar
 include $(CLEAR_VARS)
 LOCAL_PREBUILT_JAVA_LIBRARIES := javatest:lib/javatest.jar
diff --git a/tools/runner/java/dalvik/runner/ActivityMode.java b/tools/runner/java/dalvik/runner/ActivityMode.java
index dd7c420..163c72a 100644
--- a/tools/runner/java/dalvik/runner/ActivityMode.java
+++ b/tools/runner/java/dalvik/runner/ActivityMode.java
@@ -19,10 +19,12 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.logging.Logger;
@@ -36,11 +38,11 @@
 
     private static final String TEST_ACTIVITY_CLASS   = "dalvik.runner.TestActivity";
 
-    ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp,
+    ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee, File localTemp,
             boolean cleanBefore, boolean cleanAfter, File deviceRunnerDir) {
         super(new EnvironmentDevice(cleanBefore, cleanAfter,
                 debugPort, localTemp, deviceRunnerDir),
-                timeoutSeconds, sdkJar);
+                timeoutSeconds, sdkJar, tee);
     }
 
     private EnvironmentDevice getEnvironmentDevice() {
@@ -48,7 +50,7 @@
     }
 
     @Override protected void prepare(Set<File> testRunnerJava, Classpath testRunnerClasspath) {
-        testRunnerJava.add(new File(DalvikRunner.HOME_JAVA, "dalvik/runner/TestActivity.java"));
+        testRunnerJava.add(new File("dalvik/libcore/tools/runner/lib/TestActivity.java"));
         super.prepare(testRunnerJava, testRunnerClasspath);
     }
 
@@ -148,11 +150,21 @@
         return dex;
     }
 
+    /**
+     * According to android.content.pm.PackageParser, package name
+     * "must have at least one '.' separator" Since the qualified name
+     * may not contain a dot, we prefix containing one to ensure we
+     * are compliant.
+     */
+    private static String packageName(TestRun testRun) {
+        return "DalvikRunner." + testRun.getQualifiedName();
+    }
+
     private File createApk (TestRun testRun, File dex) {
         String androidManifest =
             "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
             "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
-            "      package=\"" + testRun.getQualifiedName() + "\">\n" +
+            "      package=\"" + packageName(testRun) + "\">\n" +
             "    <uses-permission android:name=\"android.permission.INTERNET\" />\n" +
             "    <application>\n" +
             "        <activity android:name=\"" + TEST_ACTIVITY_CLASS + "\">\n" +
@@ -202,7 +214,7 @@
 
     private void installApk(TestRun testRun, File apkSigned) {
         // install the local apk ona the device
-        getEnvironmentDevice().adb.uninstall(testRun.getQualifiedName());
+        getEnvironmentDevice().adb.uninstall(packageName(testRun));
         getEnvironmentDevice().adb.install(apkSigned);
     }
 
@@ -211,40 +223,20 @@
         properties.setProperty(TestProperties.DEVICE_RUNNER_DIR, getEnvironmentDevice().runnerDir.getPath());
     }
 
-    @Override protected List<Command> buildCommands(TestRun testRun) {
-        List<Command> commands = new ArrayList<Command>();
-        commands.add(new Command.Builder()
-            .args("adb")
-            .args("shell")
-            .args("am")
-            .args("start")
-            .args("-a")
-            .args("android.intent.action.MAIN")
-            .args("-n")
-            .args(testRun.getQualifiedName() + "/" + TEST_ACTIVITY_CLASS).build());
+    @Override protected List<String> runTestCommand(TestRun testRun)
+            throws TimeoutException {
+        new Command(
+            "adb", "shell", "am", "start",
+            "-a","android.intent.action.MAIN",
+            "-n", (packageName(testRun) + "/" + TEST_ACTIVITY_CLASS)).executeWithTimeout(timeoutSeconds);
 
         File resultDir = new File(getEnvironmentDevice().runnerDir, testRun.getQualifiedName());
         File resultFile = new File(resultDir, TestProperties.RESULT_FILE);
-        /*
-         * The follow bash script waits for the result file to
-         * exist. It polls once a second to see if it is there with
-         * "adb shell ls". The "tr" is to remove the carriage return
-         * and newline from the adb output. When it does exist, we
-         * "adb shell cat" it so we can see the SUCCESS/FAILURE
-         * results that are expected by Mode.runTest.
-         */
-        // TODO: move loop to Java
-        commands.add(new Command.Builder()
-            .args("bash")
-            .args("-c")
-            .args(
-                    "while [ ! \"`adb shell ls " + resultFile + " | tr -d '\\r\\n'`\" = " +
-                    "        \"" + resultFile + "\" ] ; do " +
-                    "    sleep 1; " +
-                    "done; " +
-                    "adb shell cat " + resultFile).build());
-
-        return commands;
+        getEnvironmentDevice().adb.waitForFile(resultFile, timeoutSeconds);
+        return new Command.Builder()
+            .args("adb", "shell", "cat", resultFile.getPath())
+            .tee(tee)
+            .build().executeWithTimeout(timeoutSeconds);
     }
 
     @Override void cleanup(TestRun testRun) {
diff --git a/tools/runner/java/dalvik/runner/Adb.java b/tools/runner/java/dalvik/runner/Adb.java
index 075ca5f..c982058 100644
--- a/tools/runner/java/dalvik/runner/Adb.java
+++ b/tools/runner/java/dalvik/runner/Adb.java
@@ -58,10 +58,22 @@
     }
 
     /**
+     * Loop until we see a file on the device. For example, wait
+     * result.txt appears.
+     */
+    public void waitForFile(File file, long timeoutSeconds) {
+        waitFor(true, file, timeoutSeconds);
+    }
+
+    /**
      * Loop until we see a non-empty directory on the device. For
      * example, wait until /sdcard is mounted.
      */
-    public void waitForNonEmptyDirectory(File path, int timeoutSeconds) {
+    public void waitForNonEmptyDirectory(File path, long timeoutSeconds) {
+        waitFor(false, path, timeoutSeconds);
+    }
+
+    private void waitFor(boolean file, File path, long timeoutSeconds) {
         final int millisPerSecond = 1000;
         final long start = System.currentTimeMillis();
         final long deadline = start + (millisPerSecond * timeoutSeconds);
@@ -69,7 +81,11 @@
         while (true) {
             final long remainingSeconds = ((deadline - System.currentTimeMillis())
                                            / millisPerSecond);
-            Command command = new Command("adb", "shell", "ls", path.getPath());
+            String pathArgument = path.getPath();
+            if (!file) {
+                pathArgument += "/";
+            }
+            Command command = new Command("adb", "shell", "ls", pathArgument);
             List<String> output;
             try {
                 output = command.executeWithTimeout(remainingSeconds);
@@ -82,8 +98,16 @@
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
-            if (!output.isEmpty()) {
-                return;
+            if (file) {
+                // for files, we expect one line of output that matches the filename
+                if (output.size() == 1 && output.get(0).equals(path.getPath())) {
+                    return;
+                }
+            } else {
+                // for a non empty directory, we just want any output
+                if (!output.isEmpty()) {
+                    return;
+                }
             }
         }
     }
diff --git a/tools/runner/java/dalvik/runner/CaliperFinder.java b/tools/runner/java/dalvik/runner/CaliperFinder.java
index 094609a..3609471 100644
--- a/tools/runner/java/dalvik/runner/CaliperFinder.java
+++ b/tools/runner/java/dalvik/runner/CaliperFinder.java
@@ -41,15 +41,6 @@
     }
 
     public Classpath getRunnerClasspath() {
-        return Classpath.of(
-            // TODO: we should be able to work with a shipping SDK, not depend on out/...
-            // TODO: have a pre-packaged caliper-all.jar in our lib directory, with the jtreg stuff.
-            // external/caliper
-            new File("out/target/common/obj/JAVA_LIBRARIES/caliper_intermediates/classes.jar").getAbsoluteFile(),
-            // external/guava for external/caliper
-            new File("out/target/common/obj/JAVA_LIBRARIES/guava_intermediates/classes.jar").getAbsoluteFile(),
-            // external/jsr305 for external/guava
-            new File("out/target/common/obj/JAVA_LIBRARIES/jsr305_intermediates/classes.jar").getAbsoluteFile());
-
+        return new Classpath();
     }
 }
diff --git a/tools/runner/java/dalvik/runner/Command.java b/tools/runner/java/dalvik/runner/Command.java
index 4319cf9..88ba38e 100644
--- a/tools/runner/java/dalvik/runner/Command.java
+++ b/tools/runner/java/dalvik/runner/Command.java
@@ -20,6 +20,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -44,6 +45,7 @@
     private final List<String> args;
     private final File workingDirectory;
     private final boolean permitNonZeroExitStatus;
+    private final PrintStream tee;
     private Process process;
 
     Command(String... args) {
@@ -54,12 +56,14 @@
         this.args = new ArrayList<String>(args);
         this.workingDirectory = null;
         this.permitNonZeroExitStatus = false;
+        this.tee = null;
     }
 
     private Command(Builder builder) {
         this.args = new ArrayList<String>(builder.args);
         this.workingDirectory = builder.workingDirectory;
         this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
+        this.tee = builder.tee;
     }
 
     public List<String> getArgs() {
@@ -106,6 +110,9 @@
         List<String> outputLines = new ArrayList<String>();
         String outputLine;
         while ((outputLine = in.readLine()) != null) {
+            if (tee != null) {
+                tee.println(outputLine);
+            }
             outputLines.add(outputLine);
         }
 
@@ -167,6 +174,7 @@
         private final List<String> args = new ArrayList<String>();
         private File workingDirectory;
         private boolean permitNonZeroExitStatus = false;
+        private PrintStream tee = null;
 
         public Builder args(Object... objects) {
             for (Object object : objects) {
@@ -200,6 +208,11 @@
             return this;
         }
 
+        public Builder tee(PrintStream printStream) {
+            tee = printStream;
+            return this;
+        }
+
         public Command build() {
             return new Command(this);
         }
diff --git a/tools/runner/java/dalvik/runner/DalvikRunner.java b/tools/runner/java/dalvik/runner/DalvikRunner.java
index a943be9..c78866e 100644
--- a/tools/runner/java/dalvik/runner/DalvikRunner.java
+++ b/tools/runner/java/dalvik/runner/DalvikRunner.java
@@ -16,8 +16,12 @@
 
 package dalvik.runner;
 
+import java.io.BufferedOutputStream;
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
@@ -72,6 +76,10 @@
         @Option(names = { "--verbose" })
         private boolean verbose;
 
+        @Option(names = { "--tee" })
+        private String teeName;
+        private PrintStream tee;
+
         @Option(names = { "--debug" })
         private Integer debugPort;
 
@@ -116,6 +124,9 @@
             System.out.println("  --clean: synonym for --clean-before and --clean-after (default).");
             System.out.println("      Disable with --no-clean if you want no files removed.");
             System.out.println();
+            System.out.println("  --tee <file>: emit test output to file during execution.");
+            System.out.println("      Specify '-' for stdout.");
+            System.out.println();
             System.out.println("  --timeout-seconds <seconds>: maximum execution time of each");
             System.out.println("      test before the runner aborts it.");
             System.out.println("      Default is: " + timeoutSeconds);
@@ -197,10 +208,6 @@
                     System.out.println("Invalid java home: " + javaHome);
                     return false;
                 }
-                if (debugPort != null) {
-                    System.out.println("debug port " + debugPort + " should not be specified for mode " + mode);
-                    return false;
-                }
             }
 
             // check vm option consistency
@@ -239,6 +246,19 @@
                 testFiles.add(new File(testFilename));
             }
 
+            if (teeName != null) {
+                if (teeName.equals("-")) {
+                    tee = System.out;
+                } else {
+                    try {
+                        tee = new PrintStream(new BufferedOutputStream(new FileOutputStream(teeName)));
+                    } catch (FileNotFoundException e) {
+                        System.out.println("Could not open file teeName: " + e);
+                        return false;
+                    }
+                }
+            }
+
             if (verbose) {
                 Logger.getLogger("dalvik.runner").setLevel(Level.FINE);
             }
@@ -273,6 +293,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.vmArgs,
                     options.cleanBefore,
@@ -283,6 +304,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.javaHome,
                     options.vmArgs,
@@ -293,6 +315,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.cleanBefore,
                     options.cleanAfter,
diff --git a/tools/runner/java/dalvik/runner/DeviceDalvikVm.java b/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
index fec3463..061e374 100644
--- a/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
+++ b/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
@@ -17,6 +17,7 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.io.PrintStream;
 import java.util.List;
 import java.util.logging.Logger;
 
@@ -24,22 +25,13 @@
  * Execute tests on a Dalvik VM using an Android device or emulator.
  */
 final class DeviceDalvikVm extends Vm {
-
-    // TODO: Don't assume we can put files in /system/framework,
-    // so we can run on production devices.
-    private static final Classpath RUNTIME_SUPPORT_CLASSPATH = Classpath.of(
-            new File("/system/framework/core-tests.jar"),
-            new File("/system/framework/caliper.jar"),
-            new File("/system/framework/guava.jar"),
-            new File("/system/framework/jsr305.jar"));
-
     private static final Logger logger = Logger.getLogger(DeviceDalvikVm.class.getName());
 
-    DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar,
+    DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee,
             File localTemp, List<String> additionalVmArgs,
             boolean cleanBefore, boolean cleanAfter, File runnerDir) {
         super(new EnvironmentDevice(cleanBefore, cleanAfter, debugPort, localTemp, runnerDir),
-                timeoutSeconds, sdkJar, additionalVmArgs);
+                timeoutSeconds, sdkJar, tee, additionalVmArgs);
     }
 
     private EnvironmentDevice getEnvironmentDevice() {
@@ -47,7 +39,24 @@
     }
 
     @Override protected void postCompileTestRunner() {
+        // TODO: does this really need to be a special case?
         postCompile("testrunner", environment.testRunnerClassesDir());
+
+        // dex everything on the classpath and push it to the device.
+        for (File classpathElement : testClasspath.getElements()) {
+            String name = basenameOfJar(classpathElement);
+            logger.fine("dex and push " + name);
+            // make the local dex (inside a jar)
+            // TODO: this is *really* expensive. we need a cache!
+            File outputFile = getEnvironmentDevice().testDir(name + ".jar");
+            new Dx().dex(outputFile, Classpath.of(classpathElement));
+            // push the local dex to the device
+            getEnvironmentDevice().adb.push(outputFile, deviceDexFile(name));
+        }
+    }
+
+    private String basenameOfJar(File jarFile) {
+        return jarFile.getName().replaceAll("\\.jar$", "");
     }
 
     @Override protected void postCompileTest(TestRun testRun) {
@@ -74,8 +83,12 @@
             File workingDirectory) {
         // ignore the working directory; it's device-local and we can't easily
         // set the working directory for commands run via adb shell.
+        // TODO: we only *need* to set ANDROID_DATA on production devices.
+        // We set "user.home" to /sdcard because code might reasonably assume it can write to
+        // that directory.
         return new VmCommandBuilder()
-                .vmCommand("adb", "shell", "dalvikvm")
+                .vmCommand("adb", "shell", "ANDROID_DATA=/sdcard", "dalvikvm")
+                .vmArgs("-Duser.home=/sdcard")
                 .vmArgs("-Duser.name=root")
                 .vmArgs("-Duser.language=en")
                 .vmArgs("-Duser.region=US")
@@ -87,7 +100,9 @@
         Classpath classpath = new Classpath();
         classpath.addAll(deviceDexFile(testRun.getQualifiedName()));
         classpath.addAll(deviceDexFile("testrunner"));
-        classpath.addAll(RUNTIME_SUPPORT_CLASSPATH);
+        for (File testClasspathElement : testClasspath.getElements()) {
+            classpath.addAll(deviceDexFile(basenameOfJar(testClasspathElement)));
+        }
         return classpath;
     }
 }
diff --git a/tools/runner/java/dalvik/runner/Dx.java b/tools/runner/java/dalvik/runner/Dx.java
index 09772bd..393b70d 100644
--- a/tools/runner/java/dalvik/runner/Dx.java
+++ b/tools/runner/java/dalvik/runner/Dx.java
@@ -17,13 +17,25 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.util.logging.Logger;
 
 /**
  * A dx command.
  */
 final class Dx {
+    private static final Logger logger = Logger.getLogger(Dx.class.getName());
+    private static final Md5Cache DEX_CACHE = new Md5Cache("dex");
 
+    /**
+     * Converts all the .class files on 'classpath' into a dex file written to 'output'.
+     */
     public void dex(File output, Classpath classpath) {
+        File key = DEX_CACHE.makeKey(classpath);
+        if (key != null && key.exists()) {
+            logger.fine("dex cache hit for " + classpath);
+            new Command.Builder().args("cp", key, output).execute();
+            return;
+        }
         /*
          * We pass --core-library so that we can write tests in the
          * same package they're testing, even when that's a core
@@ -33,7 +45,7 @@
          * yourself.
          *
          * Memory options pulled from build/core/definitions.mk to
-         * handle larged dx input when building dex for APK.
+         * handle large dx input when building dex for APK.
          */
         new Command.Builder()
                 .args("dx")
@@ -44,5 +56,6 @@
                 .args("--core-library")
                 .args(Strings.objectsToStrings(classpath.getElements()))
                 .execute();
+        DEX_CACHE.insert(key, output);
     }
 }
diff --git a/tools/runner/java/dalvik/runner/EnvironmentDevice.java b/tools/runner/java/dalvik/runner/EnvironmentDevice.java
index c44152d..9ac1c64 100644
--- a/tools/runner/java/dalvik/runner/EnvironmentDevice.java
+++ b/tools/runner/java/dalvik/runner/EnvironmentDevice.java
@@ -41,6 +41,7 @@
         }
         adb.mkdir(runnerDir);
         adb.mkdir(testTemp);
+        adb.mkdir(new File("/sdcard/dalvik-cache")); // TODO: only necessary on production devices.
         if (debugPort != null) {
             adb.forwardTcp(debugPort, debugPort);
         }
diff --git a/tools/runner/java/dalvik/runner/JavaVm.java b/tools/runner/java/dalvik/runner/JavaVm.java
index 561b0ce..38e0386 100644
--- a/tools/runner/java/dalvik/runner/JavaVm.java
+++ b/tools/runner/java/dalvik/runner/JavaVm.java
@@ -17,7 +17,9 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.io.PrintStream;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A local Java virtual machine like Harmony or the RI.
@@ -26,15 +28,16 @@
 
     private final File javaHome;
 
-    JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp,
-            File javaHome, List<String> additionalVmArgs,
+    JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee,
+            File localTemp, File javaHome, List<String> additionalVmArgs,
             boolean cleanBefore, boolean cleanAfter) {
         super(new EnvironmentHost(cleanBefore, cleanAfter, debugPort, localTemp),
-                timeoutSeconds, sdkJar, additionalVmArgs);
+                timeoutSeconds, sdkJar, tee, additionalVmArgs);
         this.javaHome = javaHome;
     }
 
-    @Override protected void postCompileTestRunner() {}
+    @Override protected void postCompileTestRunner() {
+    }
 
     @Override protected void postCompileTest(TestRun testRun) {
     }
diff --git a/tools/runner/java/dalvik/runner/Md5Cache.java b/tools/runner/java/dalvik/runner/Md5Cache.java
new file mode 100644
index 0000000..f6ba85d
--- /dev/null
+++ b/tools/runner/java/dalvik/runner/Md5Cache.java
@@ -0,0 +1,115 @@
+/*
+ * 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 dalvik.runner;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.security.MessageDigest;
+import java.util.logging.Logger;
+
+/**
+ * Caches content by MD5.
+ */
+public final class Md5Cache {
+    private static final Logger logger = Logger.getLogger(Md5Cache.class.getName());
+    private static final File CACHE_ROOT = new File("/tmp/vogar-md5-cache/");
+
+    private final String keyPrefix;
+
+    /**
+     * Creates a new cache accessor. There's only one directory on disk, so 'keyPrefix' is really
+     * just a convenience for humans inspecting the cache.
+     */
+    public Md5Cache(String keyPrefix) {
+        this.keyPrefix = keyPrefix;
+    }
+
+    /**
+     * Returns an ASCII hex representation of the MD5 of the content of 'file'.
+     */
+    private static String md5(File file) {
+        byte[] digest = null;
+        try {
+            MessageDigest digester = MessageDigest.getInstance("MD5");
+            byte[] bytes = new byte[8192];
+            FileInputStream in = new FileInputStream(file);
+            try {
+                int byteCount;
+                while ((byteCount = in.read(bytes)) > 0) {
+                    digester.update(bytes, 0, byteCount);
+                }
+                digest = digester.digest();
+            } finally {
+                in.close();
+            }
+        } catch (Exception cause) {
+            throw new RuntimeException("Unable to compute MD5 of \"" + file + "\"", cause);
+        }
+        return (digest == null) ? null : byteArrayToHexString(digest);
+    }
+
+    private static String byteArrayToHexString(byte[] bytes) {
+        StringBuilder result = new StringBuilder();
+        for (byte b : bytes) {
+            result.append(Integer.toHexString((b >> 4) & 0xf));
+            result.append(Integer.toHexString(b & 0xf));
+        }
+        return result.toString();
+    }
+
+    /**
+     * Returns the appropriate key for a dex file corresponding to the contents of 'classpath'.
+     * Returns null if we don't think it's possible to cache the given classpath.
+     */
+    public File makeKey(Classpath classpath) {
+        // Do we have it in cache?
+        String key = keyPrefix;
+        for (File element : classpath.getElements()) {
+            // We only cache dexed .jar files, not directories.
+            if (!element.toString().endsWith(".jar")) {
+                return null;
+            }
+            key += "-" + md5(element);
+        }
+        return new File(CACHE_ROOT, key);
+    }
+
+    /**
+     * Copy the file 'content' into the cache with the given 'key'.
+     * This method assumes you're using the appropriate key for the content (and has no way to
+     * check because the key is a function of the inputs that made the content, not the content
+     * itself).
+     * We accept a null so the caller doesn't have to pay attention to whether we think we can
+     * cache the content or not.
+     */
+    public void insert(File key, File content) {
+        if (key == null) {
+            return;
+        }
+        logger.fine("inserting " + key);
+        if (!key.toString().startsWith(CACHE_ROOT.toString())) {
+            throw new IllegalArgumentException("key '" + key + "' not a valid cache key");
+        }
+        // Make sure the cache exists first.
+        new Mkdir().mkdirs(CACHE_ROOT);
+        // Copy it onto the same file system first, then atomically move it into place.
+        // That way, if we fail, we don't leave anything dangerous lying around.
+        File temporary = new File(key + ".tmp");
+        new Command.Builder().args("cp", content, temporary).execute();
+        new Command.Builder().args("mv", temporary, key).execute();
+    }
+}
diff --git a/tools/runner/java/dalvik/runner/Mode.java b/tools/runner/java/dalvik/runner/Mode.java
index c30e023..0ad7172 100644
--- a/tools/runner/java/dalvik/runner/Mode.java
+++ b/tools/runner/java/dalvik/runner/Mode.java
@@ -18,7 +18,9 @@
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -43,6 +45,7 @@
     protected final Environment environment;
     protected final long timeoutSeconds;
     protected final File sdkJar;
+    protected final PrintStream tee;
 
     /**
      * Set of Java files needed to built to tun the currently selected
@@ -61,18 +64,21 @@
      */
     protected final Classpath testRunnerClasspath = new Classpath();
 
+    // TODO: this should be an immutable collection.
     protected final Classpath testClasspath = Classpath.of(
+            new File("dalvik/libcore/tools/runner/lib/jsr305.jar"),
+            new File("dalvik/libcore/tools/runner/lib/guava.jar"),
+            new File("dalvik/libcore/tools/runner/lib/caliper.jar"),
             // TODO: we should be able to work with a shipping SDK, not depend on out/...
-            // dalvik/libcore for tests
-            new File("out/target/common/obj/JAVA_LIBRARIES/core-tests_intermediates/classes.jar").getAbsoluteFile(),
-            // framework/base for tests
-            new File("out/target/common/obj/JAVA_LIBRARIES/core_intermediates/classes.jar").getAbsoluteFile());
+            // dalvik/libcore/**/test/ for junit
+            // TODO: jar up just the junit classes and drop the jar in our lib/ directory.
+            new File("out/target/common/obj/JAVA_LIBRARIES/core-tests_intermediates/classes.jar").getAbsoluteFile());
 
-
-    Mode(Environment environment, long timeoutSeconds, File sdkJar) {
+    Mode(Environment environment, long timeoutSeconds, File sdkJar, PrintStream tee) {
         this.environment = environment;
         this.timeoutSeconds = timeoutSeconds;
         this.sdkJar = sdkJar;
+        this.tee = tee;
     }
 
     /**
@@ -81,20 +87,37 @@
      */
     protected void prepare(Set<File> testRunnerJava, Classpath testRunnerClasspath) {
         this.testRunnerJava.add(new File(DalvikRunner.HOME_JAVA, "dalvik/runner/TestRunner.java"));
+        this.testRunnerJava.addAll(dalvikAnnotationSourceFiles());
         this.testRunnerJava.addAll(testRunnerJava);
         this.testRunnerClasspath.addAll(testRunnerClasspath);
         environment.prepare();
         compileTestRunner();
     }
 
+    private List<File> dalvikAnnotationSourceFiles() {
+        // Hopefully one day we'll strip the dalvik annotations out, but until then we need to make
+        // them available to javac(1).
+        File sourceDir = new File("dalvik/libcore/dalvik/src/main/java/dalvik/annotation");
+        File[] javaSourceFiles = sourceDir.listFiles(new FilenameFilter() {
+            public boolean accept(File dir, String filename) {
+                return filename.endsWith(".java");
+            }
+        });
+        return Arrays.asList(javaSourceFiles);
+    }
+
     private void compileTestRunner() {
         logger.fine("build testrunner");
 
+        Classpath classpath = new Classpath();
+        classpath.addAll(testClasspath);
+        classpath.addAll(testRunnerClasspath);
+
         File base = environment.testRunnerClassesDir();
         new Mkdir().mkdirs(base);
         new Javac()
                 .bootClasspath(sdkJar)
-                .classpath(testRunnerClasspath)
+                .classpath(classpath)
                 .sourcepath(DalvikRunner.HOME_JAVA)
                 .destination(base)
                 .compile(testRunnerJava);
@@ -158,13 +181,17 @@
         classpath.addAll(testClasspath);
         classpath.addAll(testRun.getRunnerClasspath());
 
+        Set<File> sourceFiles = new HashSet<File>();
+        sourceFiles.add(testRun.getTestJava());
+        sourceFiles.addAll(dalvikAnnotationSourceFiles());
+
         // compile the test case
         new Javac()
                 .bootClasspath(sdkJar)
                 .classpath(classpath)
                 .sourcepath(testRun.getTestDirectory())
                 .destination(testClassesDir)
-                .compile(testRun.getTestJava());
+                .compile(sourceFiles);
         postCompileTest(testRun);
         return true;
     }
@@ -194,20 +221,16 @@
             throw new IllegalArgumentException();
         }
 
-        final List<Command> commands = buildCommands(testRun);
-
-        List<String> output = null;
-        for (final Command command : commands) {
-            try {
-                output = command.executeWithTimeout(timeoutSeconds);
-            } catch (TimeoutException e) {
-                testRun.setResult(Result.EXEC_TIMEOUT,
-                        Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
-                return;
-            } catch (Exception e) {
-                testRun.setResult(Result.ERROR, e);
-                return;
-            }
+        List<String> output;
+        try {
+            output = runTestCommand(testRun);
+        } catch (TimeoutException e) {
+            testRun.setResult(Result.EXEC_TIMEOUT,
+                Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
+            return;
+        } catch (Exception e) {
+            testRun.setResult(Result.ERROR, e);
+            return;
         }
         // we only look at the output of the last command
         if (output.isEmpty()) {
@@ -223,9 +246,10 @@
     }
 
     /**
-     * Returns commands for test execution.
+     * Run the actual test to gather output
      */
-    protected abstract List<Command> buildCommands(TestRun testRun);
+    protected abstract List<String> runTestCommand(TestRun testRun)
+        throws TimeoutException;
 
     /**
      * Deletes files and releases any resources required for the execution of
diff --git a/tools/runner/java/dalvik/runner/Vm.java b/tools/runner/java/dalvik/runner/Vm.java
index 9f96ec5..8ff5858 100644
--- a/tools/runner/java/dalvik/runner/Vm.java
+++ b/tools/runner/java/dalvik/runner/Vm.java
@@ -19,6 +19,7 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -27,6 +28,7 @@
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 import java.util.logging.Logger;
 
 /**
@@ -39,22 +41,25 @@
     protected final List<String> additionalVmArgs;
 
     Vm(Environment environment, long timeoutSeconds, File sdkJar,
-            List<String> additionalVmArgs) {
-        super(environment, timeoutSeconds, sdkJar);
+           PrintStream tee, List<String> additionalVmArgs) {
+        super(environment, timeoutSeconds, sdkJar, tee);
         this.additionalVmArgs = additionalVmArgs;
     }
 
     /**
      * Returns a VM for test execution.
      */
-    @Override protected List<Command> buildCommands(TestRun testRun) {
-        return Collections.singletonList(newVmCommandBuilder(testRun.getUserDir())
+    @Override protected List<String> runTestCommand(TestRun testRun)
+            throws TimeoutException {
+        Command command = newVmCommandBuilder(testRun.getUserDir())
                 .classpath(getRuntimeSupportClasspath(testRun))
                 .userDir(testRun.getUserDir())
                 .debugPort(environment.debugPort)
                 .vmArgs(additionalVmArgs)
                 .mainClass(TestRunner.class.getName())
-                .build());
+                .output(tee)
+                .build();
+        return command.executeWithTimeout(timeoutSeconds);
     }
 
     /**
@@ -78,6 +83,7 @@
         private File userDir;
         private Integer debugPort;
         private String mainClass;
+        private PrintStream output;
         private List<String> vmCommand = Collections.singletonList("java");
         private List<String> vmArgs = new ArrayList<String>();
 
@@ -116,6 +122,11 @@
             return this;
         }
 
+        public VmCommandBuilder output(PrintStream output) {
+            this.output = output;
+            return this;
+        }
+
         public VmCommandBuilder vmArgs(String... vmArgs) {
             return vmArgs(Arrays.asList(vmArgs));
         }
@@ -146,6 +157,8 @@
             builder.args(vmArgs);
             builder.args(mainClass);
 
+            builder.tee(output);
+
             return builder.build();
         }
     }
diff --git a/tools/runner/java/dalvik/runner/TestActivity.java b/tools/runner/lib/TestActivity.java
similarity index 100%
rename from tools/runner/java/dalvik/runner/TestActivity.java
rename to tools/runner/lib/TestActivity.java
diff --git a/tools/runner/lib/caliper.jar b/tools/runner/lib/caliper.jar
new file mode 100644
index 0000000..63a156a
--- /dev/null
+++ b/tools/runner/lib/caliper.jar
Binary files differ
diff --git a/tools/runner/lib/caliper.jar.txt b/tools/runner/lib/caliper.jar.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/tools/runner/lib/caliper.jar.txt
@@ -0,0 +1,202 @@
+
+                                 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
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/tools/runner/lib/guava.jar b/tools/runner/lib/guava.jar
new file mode 100644
index 0000000..39adc7f
--- /dev/null
+++ b/tools/runner/lib/guava.jar
Binary files differ
diff --git a/tools/runner/lib/guava.jar.txt b/tools/runner/lib/guava.jar.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/tools/runner/lib/guava.jar.txt
@@ -0,0 +1,202 @@
+
+                                 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
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/tools/runner/lib/jsr305.jar b/tools/runner/lib/jsr305.jar
new file mode 100644
index 0000000..57a62c1
--- /dev/null
+++ b/tools/runner/lib/jsr305.jar
Binary files differ
diff --git a/tools/runner/lib/jsr305.jar.txt b/tools/runner/lib/jsr305.jar.txt
new file mode 100644
index 0000000..6736681
--- /dev/null
+++ b/tools/runner/lib/jsr305.jar.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2007-2009, JSR305 expert group
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its 
+      contributors may be used to endorse or promote products derived from 
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/tools/runner/vogar b/tools/runner/vogar
new file mode 100755
index 0000000..e5a6ad0
--- /dev/null
+++ b/tools/runner/vogar
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# 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.
+
+# m core-tests junit caliper snod && adb reboot bootloader && fastboot flashall && adb wait-for-device
+# mmm dalvik/libcore/tools/runner
+
+classpath=`dirname $0`/../../../../out/host/linux-x86/framework/dalvik_runner.jar
+exec java -cp $classpath dalvik.runner.DalvikRunner "$@"
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
index 59a8b78..4e689fb 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/AttrImpl.java
@@ -73,7 +73,7 @@
             throw new DOMException(DOMException.NAMESPACE_ERR, localName);
         }
             
-        if (!document.isXMLIdentifier(localName)) {
+        if (!DocumentImpl.isXMLIdentifier(localName)) {
             throw new DOMException(DOMException.INVALID_CHARACTER_ERR, localName);
         }
             
@@ -90,11 +90,11 @@
             String prefix = name.substring(0, prefixSeparator);
             String localName = name.substring(prefixSeparator + 1);
             
-            if (!document.isXMLIdentifier(prefix) || !document.isXMLIdentifier(localName)) {
+            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         } else {
-            if (!document.isXMLIdentifier(name)) {
+            if (!DocumentImpl.isXMLIdentifier(name)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         }
@@ -108,7 +108,9 @@
     }
 
     public String getName() {
-        return (prefix != null ? prefix + ":" : "") + localName;
+        return prefix != null
+                ? prefix + ":" + localName
+                : localName;
     }
 
     @Override
@@ -148,28 +150,8 @@
     }
 
     @Override
-    public void setNodeValue(String value) throws DOMException {
-        setValue(value);
-    }
-    
-    @Override
     public void setPrefix(String prefix) {
-        if (!namespaceAware) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-        }
-        
-        if (prefix != null) {
-            if (namespaceURI == null
-                    || !DocumentImpl.isXMLIdentifier(prefix)
-                    || ("xmlns".equals(prefix)
-                            && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI))
-                    || ("xml".equals(prefix)
-                            && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI))) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-        }
-
-        this.prefix = prefix;
+        this.prefix = validatePrefix(prefix, namespaceAware, namespaceURI);
     }
     
     public void setValue(String value) throws DOMException {
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/CDATASectionImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/CDATASectionImpl.java
index 33e216a..b28c9da 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/CDATASectionImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/CDATASectionImpl.java
@@ -31,7 +31,7 @@
  */
 public class CDATASectionImpl extends TextImpl implements CDATASection {
 
-    CDATASectionImpl(DocumentImpl document, String data) {
+    public CDATASectionImpl(DocumentImpl document, String data) {
         super(document, data);
     }
 
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/CharacterDataImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/CharacterDataImpl.java
index c39423c..6354747 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/CharacterDataImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/CharacterDataImpl.java
@@ -51,6 +51,13 @@
         return buffer.toString();
     }
 
+    /**
+     * Appends this node's text content to the given builder.
+     */
+    public void appendDataTo(StringBuilder stringBuilder) {
+        stringBuilder.append(buffer);
+    }
+
     public int getLength() {
         return buffer.length();
     }
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
index 861f0a3..1283eeb 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/DOMImplementationImpl.java
@@ -31,7 +31,7 @@
  * the DOM implementation can easily access them while maintaining the DOM tree
  * structure.
  */
-public class DOMImplementationImpl implements DOMImplementation {
+public final class DOMImplementationImpl implements DOMImplementation {
 
     // Singleton instance.
     private static DOMImplementationImpl instance;
@@ -50,17 +50,24 @@
     }
 
     public boolean hasFeature(String feature, String version) {
-        // We claim to support DOM Core Level 1 & 2, nothing else.
-
-        // TODO
-
-        if ("Core".equalsIgnoreCase(feature) || "XML".equalsIgnoreCase(feature)) {
-            if (version == null || "".equals(version) || "1.0".equals(version) || "2.0".equals(version)) {
-                return true;
-            }
+        boolean anyVersion = version == null || version.length() == 0;
+        if (feature.startsWith("+")) {
+            feature = feature.substring(1);
         }
 
-        return false;
+        // TODO: fully implement these APIs:
+        // "LS" (org.w3c.dom.ls) versions "3.0"
+        // "ElementTraversal" (org.w3c.dom.traversal) versions "1.0"
+
+        if (feature.equalsIgnoreCase("Core")) {
+            return anyVersion || version.equals("1.0") || version.equals("2.0") || version.equals("3.0");
+        } else if (feature.equalsIgnoreCase("XML")) {
+            return anyVersion || version.equals("1.0") || version.equals("2.0") || version.equals("3.0");
+        } else if (feature.equalsIgnoreCase("XMLVersion")) {
+            return anyVersion || version.equals("1.0") || version.equals("1.1");
+        } else {
+            return false;
+        }
     }
 
     /**
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
index d6d412b..b2f16d1 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java
@@ -48,7 +48,7 @@
 
     private DOMImplementation domImplementation;
 
-    DocumentImpl(DOMImplementationImpl impl, String namespaceURI,
+    public DocumentImpl(DOMImplementationImpl impl, String namespaceURI,
             String qualifiedName, DocumentType doctype) {
         super(null);
 
@@ -100,6 +100,8 @@
      * @return The new node.
      */
     Node cloneNode(Node node, boolean deep) throws DOMException {
+        // TODO: callback the UserDataHandler with a NODE_CLONED event
+
         Node target;
         
         switch (node.getNodeType()) {
@@ -279,6 +281,7 @@
     }
 
     public Node importNode(Node importedNode, boolean deep) throws DOMException {
+        // TODO: callback the UserDataHandler with a NODE_IMPORTED event
         return cloneNode(importedNode, deep);
     }
 
@@ -341,6 +344,7 @@
     }
 
     public Node adoptNode(Node source) throws DOMException {
+        // TODO: callback the UserDataHandler with a NODE_ADOPTED event
         throw new UnsupportedOperationException(); // TODO
     }
 
@@ -354,6 +358,7 @@
 
     public Node renameNode(Node n, String namespaceURI, String qualifiedName)
             throws DOMException {
+        // TODO: callback the UserDataHandler with a NODE_RENAMED event
         throw new UnsupportedOperationException(); // TODO
     }
 }
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
index 230e444..df1383d 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/ElementImpl.java
@@ -65,7 +65,7 @@
             qualifiedName = qualifiedName.substring(p + 1);
         }
         
-        if (!document.isXMLIdentifier(qualifiedName)) {
+        if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
             throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
         }
             
@@ -82,11 +82,11 @@
             String prefix = name.substring(0, p);
             String localName = name.substring(p + 1);
             
-            if (!document.isXMLIdentifier(prefix) || !document.isXMLIdentifier(localName)) {
+            if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         } else {
-            if (!document.isXMLIdentifier(name)) {
+            if (!DocumentImpl.isXMLIdentifier(name)) {
                 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name);
             }
         }
@@ -241,7 +241,9 @@
     }
 
     public String getTagName() {
-        return (prefix != null ? prefix + ":" : "") + localName;
+        return prefix != null
+                ? prefix + ":" + localName
+                : localName;
     }
 
     public boolean hasAttribute(String name) {
@@ -281,7 +283,7 @@
             throw new DOMException(DOMException.NOT_FOUND_ERR, null);
         }
 
-        attributes.remove(oldAttr);
+        attributes.remove(oldAttrImpl);
         oldAttrImpl.ownerElement = null;
 
         return oldAttrImpl;
@@ -362,21 +364,7 @@
 
     @Override
     public void setPrefix(String prefix) {
-        if (!namespaceAware) {
-            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-        }
-        
-        if (prefix != null) {
-            if (namespaceURI == null || !document.isXMLIdentifier(prefix)) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-            
-            if ("xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)) {
-                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
-            }
-        }
-        
-        this.prefix = prefix;
+        this.prefix = validatePrefix(prefix, namespaceAware, namespaceURI);
     }
     
     public class ElementAttrNamedNodeMapImpl implements NamedNodeMap {
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
index b752506..24ed102 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
@@ -16,24 +16,30 @@
 
 package org.apache.harmony.xml.dom;
 
+import org.w3c.dom.Attr;
+import org.w3c.dom.CharacterData;
 import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
+import org.w3c.dom.ProcessingInstruction;
 import org.w3c.dom.UserDataHandler;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
- * Provides a straightforward implementation of the corresponding W3C DOM
- * interface. The class is used internally only, thus only notable members that
- * are not in the original interface are documented (the W3C docs are quite
- * extensive). Hope that's ok.
- * <p>
- * Some of the fields may have package visibility, so other classes belonging to
- * the DOM implementation can easily access them while maintaining the DOM tree
- * structure.
- * <p>
- * This class represents a Node that has neither a parent nor children.
+ * A straightforward implementation of the corresponding W3C DOM node.
+ *
+ * <p>Some fields have package visibility so other classes can access them while
+ * maintaining the DOM structure.
+ *
+ * <p>This class represents a Node that has neither a parent nor children.
+ * Subclasses may have either.
+ *
+ * <p>Some code was adapted from Apache Xerces.
  */
 public abstract class NodeImpl implements Node {
 
@@ -135,13 +141,64 @@
         throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
     }
 
-    public void setNodeValue(String nodeValue) throws DOMException {
+    public final void setNodeValue(String nodeValue) throws DOMException {
+        switch (getNodeType()) {
+            case CDATA_SECTION_NODE:
+            case COMMENT_NODE:
+            case TEXT_NODE:
+                ((CharacterData) this).setData(nodeValue);
+                return;
+
+            case PROCESSING_INSTRUCTION_NODE:
+                ((ProcessingInstruction) this).setData(nodeValue);
+                return;
+
+            case ATTRIBUTE_NODE:
+                ((Attr) this).setValue(nodeValue);
+                return;
+
+            case ELEMENT_NODE:
+            case ENTITY_REFERENCE_NODE:
+            case ENTITY_NODE:
+            case DOCUMENT_NODE:
+            case DOCUMENT_TYPE_NODE:
+            case DOCUMENT_FRAGMENT_NODE:
+            case NOTATION_NODE:
+                return; // do nothing!
+
+            default:
+                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
+                        "Unsupported node type " + getNodeType());
+        }
     }
 
     public void setPrefix(String prefix) throws DOMException {
     }
 
     /**
+     * Validates the element or attribute namespace prefix on this node.
+     *
+     * @param namespaceAware whether this node is namespace aware
+     * @param namespaceURI this node's namespace URI
+     */
+    protected String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
+        if (!namespaceAware) {
+            throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
+        }
+
+        if (prefix != null) {
+            if (namespaceURI == null
+                    || !DocumentImpl.isXMLIdentifier(prefix)
+                    || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)
+                    || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
+                throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
+            }
+        }
+
+        return prefix;
+    }
+
+    /**
      * Checks whether a required string matches an actual string. This utility
      * method is used for comparing namespaces and such. It takes into account
      * null arguments and the "*" special case.
@@ -167,7 +224,6 @@
      * account null arguments and the "*" special case.
      * 
      * @param name The required name.
-     * @param wildcard TODO
      * @return True if and only if the actual name matches the required one.
      */
     public boolean matchesName(String name, boolean wildcard) {
@@ -181,7 +237,6 @@
      *
      * @param namespaceURI The required namespace.
      * @param localName The required local name.
-     * @param wildcard TODO
      * @return True if and only if the actual namespace and local name match
      *         the required pair of namespace and local name.
      */
@@ -190,7 +245,34 @@
     }
 
     public String getBaseURI() {
-        return null; // TODO
+        /*
+         * TODO: implement. For reference, here's Xerces' behaviour:
+         *
+         * In all cases, the returned URI should be sanitized before it is
+         * returned. If the URI is malformed, null should be returned instead.
+         *
+         * For document nodes, this should return a member field that's
+         * initialized by the parser.
+         *
+         * For element nodes, this should first look for the xml:base attribute.
+         *   if that exists and is absolute, it should be returned.
+         *   if that exists and is relative, it should be resolved to the parent's base URI
+         *   if it doesn't exist, the parent's baseURI should be returned
+         *
+         * For entity nodes, if a base URI exists that should be returned.
+         * Otherwise the document's base URI should be returned
+         *
+         * For entity references, if a base URI exists that should be returned
+         * otherwise it dereferences the entity (via the document) and uses the
+         * entity's base URI.
+         *
+         * For notations, it returns the base URI field.
+         *
+         * For processing instructions, it returns the parent's base URI.
+         *
+         * For all other node types, it returns null.
+         */
+        return null;
     }
 
     public short compareDocumentPosition(Node other)
@@ -209,32 +291,298 @@
         }
     }
 
-    public void setTextContent(String textContent) throws DOMException {
-        throw new UnsupportedOperationException(); // TODO
+    public final void setTextContent(String textContent) throws DOMException {
+        switch (getNodeType()) {
+            case DOCUMENT_TYPE_NODE:
+            case DOCUMENT_NODE:
+                return; // do nothing!
+
+            case ELEMENT_NODE:
+            case ENTITY_NODE:
+            case ENTITY_REFERENCE_NODE:
+            case DOCUMENT_FRAGMENT_NODE:
+                // remove all existing children
+                Node child;
+                while ((child = getFirstChild()) != null) {
+                    removeChild(child);
+                }
+                // create a text node to hold the given content
+                if (textContent != null && textContent.length() != 0) {
+                    appendChild(getOwnerDocument().createTextNode(textContent));
+                }
+                return;
+
+            case ATTRIBUTE_NODE:
+            case TEXT_NODE:
+            case CDATA_SECTION_NODE:
+            case PROCESSING_INSTRUCTION_NODE:
+            case COMMENT_NODE:
+            case NOTATION_NODE:
+                setNodeValue(textContent);
+                return;
+
+            default:
+                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
+                        "Unsupported node type " + getNodeType());
+        }
     }
 
     public boolean isSameNode(Node other) {
-        throw new UnsupportedOperationException(); // TODO
+        return this == other;
     }
 
-    public String lookupPrefix(String namespaceURI) {
-        throw new UnsupportedOperationException(); // TODO
+    /**
+     * Returns the element whose namespace definitions apply to this node. Use
+     * this element when mapping prefixes to URIs and vice versa.
+     */
+    private NodeImpl getNamespacingElement() {
+        switch (this.getNodeType()) {
+            case ELEMENT_NODE:
+                return this;
+
+            case DOCUMENT_NODE:
+                return (NodeImpl) ((Document) this).getDocumentElement();
+
+            case ENTITY_NODE:
+            case NOTATION_NODE:
+            case DOCUMENT_FRAGMENT_NODE:
+            case DOCUMENT_TYPE_NODE:
+                return null;
+
+            case ATTRIBUTE_NODE:
+                return (NodeImpl) ((Attr) this).getOwnerElement();
+
+            case TEXT_NODE:
+            case CDATA_SECTION_NODE:
+            case ENTITY_REFERENCE_NODE:
+            case PROCESSING_INSTRUCTION_NODE:
+            case COMMENT_NODE:
+                return getContainingElement();
+
+            default:
+                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
+                        "Unsupported node type " + getNodeType());
+        }
     }
 
-    public boolean isDefaultNamespace(String namespaceURI) {
-        throw new UnsupportedOperationException(); // TODO
+    /**
+     * Returns the nearest ancestor element that contains this node.
+     */
+    private NodeImpl getContainingElement() {
+        for (Node p = getParentNode(); p != null; p = p.getParentNode()) {
+            if (p.getNodeType() == ELEMENT_NODE) {
+                return (NodeImpl) p;
+            }
+        }
+        return null;
     }
 
-    public String lookupNamespaceURI(String prefix) {
-        throw new UnsupportedOperationException(); // TODO
+    public final String lookupPrefix(String namespaceURI) {
+        if (namespaceURI == null) {
+            return null;
+        }
+
+        // the XML specs define some prefixes (like "xml" and "xmlns") but this
+        // API is explicitly defined to ignore those.
+
+        NodeImpl target = getNamespacingElement();
+        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
+            // check this element's namespace first
+            if (namespaceURI.equals(node.getNamespaceURI())
+                    && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) {
+                return node.getPrefix();
+            }
+
+            // search this element for an attribute of this form:
+            //   xmlns:foo="http://namespaceURI"
+            if (!node.hasAttributes()) {
+                continue;
+            }
+            NamedNodeMap attributes = node.getAttributes();
+            for (int i = 0, length = attributes.getLength(); i < length; i++) {
+                Node attr = attributes.item(i);
+                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())
+                        || !"xmlns".equals(attr.getPrefix())
+                        || !namespaceURI.equals(attr.getNodeValue())) {
+                    continue;
+                }
+                if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) {
+                    return attr.getLocalName();
+                }
+            }
+        }
+
+        return null;
     }
 
-    public boolean isEqualNode(Node arg) {
-        throw new UnsupportedOperationException(); // TODO
+    /**
+     * Returns true if the given prefix is mapped to the given URI on this
+     * element. Since child elements can redefine prefixes, this check is
+     * necessary: {@code
+     * <foo xmlns:a="http://good">
+     *   <bar xmlns:a="http://evil">
+     *     <a:baz />
+     *   </bar>
+     * </foo>}
+     *
+     * @param prefix the prefix to find. Nullable.
+     * @param uri the URI to match. Non-null.
+     */
+    boolean isPrefixMappedToUri(String prefix, String uri) {
+        if (prefix == null) {
+            return false;
+        }
+
+        String actual = lookupNamespaceURI(prefix);
+        return uri.equals(actual);
     }
 
-    public Object getFeature(String feature, String version) {
-        throw new UnsupportedOperationException(); // TODO
+    public final boolean isDefaultNamespace(String namespaceURI) {
+        String actual = lookupNamespaceURI(null); // null yields the default namespace
+        return namespaceURI == null
+                ? actual == null
+                : namespaceURI.equals(actual);
+    }
+
+    public final String lookupNamespaceURI(String prefix) {
+        NodeImpl target = getNamespacingElement();
+        for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
+            // check this element's namespace first
+            String nodePrefix = node.getPrefix();
+            if (node.getNamespaceURI() != null) {
+                if (prefix == null // null => default prefix
+                        ? nodePrefix == null
+                        : prefix.equals(nodePrefix)) {
+                    return node.getNamespaceURI();
+                }
+            }
+
+            // search this element for an attribute of the appropriate form.
+            //    default namespace: xmlns="http://resultUri"
+            //          non default: xmlns:specifiedPrefix="http://resultUri"
+            if (!node.hasAttributes()) {
+                continue;
+            }
+            NamedNodeMap attributes = node.getAttributes();
+            for (int i = 0, length = attributes.getLength(); i < length; i++) {
+                Node attr = attributes.item(i);
+                if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) {
+                    continue;
+                }
+                if (prefix == null // null => default prefix
+                        ? "xmlns".equals(attr.getNodeName())
+                        : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) {
+                    String value = attr.getNodeValue();
+                    return value.length() > 0 ? value : null;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns a list of objects such that two nodes are equal if their lists
+     * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes,
+     * neither of which override Object.equals(). Such values must be compared
+     * manually.
+     */
+    private static List<Object> createEqualityKey(Node node) {
+        List<Object> values = new ArrayList<Object>();
+        values.add(node.getNodeType());
+        values.add(node.getNodeName());
+        values.add(node.getLocalName());
+        values.add(node.getNamespaceURI());
+        values.add(node.getPrefix());
+        values.add(node.getNodeValue());
+        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
+            values.add(child);
+        }
+
+        switch (node.getNodeType()) {
+            case DOCUMENT_TYPE_NODE:
+                DocumentTypeImpl doctype = (DocumentTypeImpl) node;
+                values.add(doctype.getPublicId());
+                values.add(doctype.getSystemId());
+                values.add(doctype.getInternalSubset());
+                values.add(doctype.getEntities());
+                values.add(doctype.getNotations());
+                break;
+
+            case ELEMENT_NODE:
+                Element element = (Element) node;
+                values.add(element.getAttributes());
+                break;
+        }
+
+        return values;
+    }
+
+    public final boolean isEqualNode(Node arg) {
+        if (arg == this) {
+            return true;
+        }
+
+        List<Object> listA = createEqualityKey(this);
+        List<Object> listB = createEqualityKey(arg);
+
+        if (listA.size() != listB.size()) {
+            return false;
+        }
+
+        for (int i = 0; i < listA.size(); i++) {
+            Object a = listA.get(i);
+            Object b = listB.get(i);
+
+            if (a == b) {
+                continue;
+
+            } else if (a == null || b == null) {
+                return false;
+
+            } else if (a instanceof String || a instanceof Short) {
+                if (!a.equals(b)) {
+                    return false;
+                }
+
+            } else if (a instanceof NamedNodeMap) {
+                if (!(b instanceof NamedNodeMap)
+                        || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) {
+                    return false;
+                }
+
+            } else if (a instanceof Node) {
+                if (!(b instanceof Node)
+                        || !((Node) a).isEqualNode((Node) b)) {
+                    return false;
+                }
+
+            } else {
+                throw new AssertionError(); // unexpected type
+            }
+        }
+        
+        return true;
+    }
+
+    private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) {
+        if (a.getLength() != b.getLength()) {
+            return false;
+        }
+        for (int i = 0; i < a.getLength(); i++) {
+            Node aNode = a.item(i);
+            Node bNode = aNode.getLocalName() == null
+                    ? b.getNamedItem(aNode.getNodeName())
+                    : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName());
+            if (bNode == null || !aNode.isEqualNode(bNode)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public final Object getFeature(String feature, String version) {
+        return isSupported(feature, version) ? this : null;
     }
 
     public Object setUserData(String key, Object data,
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/TextImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/TextImpl.java
index 3905865..3840ef4 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/TextImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/TextImpl.java
@@ -32,7 +32,7 @@
  */
 public class TextImpl extends CharacterDataImpl implements Text {
 
-    TextImpl(DocumentImpl document, String data) {
+    public TextImpl(DocumentImpl document, String data) {
         super(document, data);
     }
 
@@ -46,12 +46,7 @@
         return Node.TEXT_NODE;
     }
 
-    @Override
-    public String getNodeValue() {
-        return getData();
-    }
-
-    public Text splitText(int offset) throws DOMException {
+    public final Text splitText(int offset) throws DOMException {
         Text newText = getOwnerDocument().createTextNode(
                 substringData(offset, getLength() - offset));
         deleteData(0, offset);
@@ -66,15 +61,83 @@
         return this;
     }
 
-    public boolean isElementContentWhitespace() {
-        throw new UnsupportedOperationException(); // TODO
+    public final boolean isElementContentWhitespace() {
+        // Undefined because we don't validate. Whether whitespace characters
+        // constitute "element content whitespace" is defined by the containing
+        // element's declaration (DTD) and we don't parse that.
+        // TODO: wire this up when we support document validation
+        return false;
     }
 
-    public String getWholeText() {
-        throw new UnsupportedOperationException(); // TODO
+    public final String getWholeText() {
+        // TODO: support entity references. This code should expand through
+        // the child elements of entity references.
+        //     http://code.google.com/p/android/issues/detail?id=6807
+
+        StringBuilder result = new StringBuilder();
+        for (TextImpl n = firstTextNodeInCurrentRun(); n != null; n = n.nextTextNode()) {
+            n.appendDataTo(result);
+        }
+        return result.toString();
     }
 
-    public Text replaceWholeText(String content) throws DOMException {
-        throw new UnsupportedOperationException(); // TODO
+    public final Text replaceWholeText(String content) throws DOMException {
+        // TODO: support entity references. This code should expand and replace
+        // the child elements of entity references.
+        //     http://code.google.com/p/android/issues/detail?id=6807
+
+        Node parent = getParentNode();
+        Text result = null;
+
+        // delete all nodes in the current run of text...
+        for (TextImpl n = firstTextNodeInCurrentRun(); n != null; ) {
+
+            // ...except the current node if we have content for it
+            if (n == this && content != null && content.length() > 0) {
+                setData(content);
+                result = this;
+                n = n.nextTextNode();
+
+            } else {
+                Node toRemove = n; // because removeChild() detaches siblings
+                n = n.nextTextNode();
+                parent.removeChild(toRemove);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the first text or CDATA node in the current sequence of text and
+     * CDATA nodes.
+     */
+    private TextImpl firstTextNodeInCurrentRun() {
+        TextImpl firstTextInCurrentRun = this;
+        for (Node p = getPreviousSibling(); p != null; p = p.getPreviousSibling()) {
+            short nodeType = p.getNodeType();
+            if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
+                firstTextInCurrentRun = (TextImpl) p;
+            } else {
+                break;
+            }
+        }
+        return firstTextInCurrentRun;
+    }
+
+    /**
+     * Returns the next sibling node if it exists and it is text or CDATA.
+     * Otherwise returns null.
+     */
+    private TextImpl nextTextNode() {
+        Node nextSibling = getNextSibling();
+        if (nextSibling == null) {
+            return null;
+        }
+
+        short nodeType = nextSibling.getNodeType();
+        return nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE
+                ? (TextImpl) nextSibling
+                : null;
     }
 }
diff --git a/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java b/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
index 5a3c48c..ca2ff98 100644
--- a/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java
@@ -23,10 +23,14 @@
 
 import javax.xml.parsers.DocumentBuilder;
 
+import org.apache.harmony.xml.dom.CDATASectionImpl;
+import org.apache.harmony.xml.dom.DocumentImpl;
+import org.apache.harmony.xml.dom.TextImpl;
 import org.kxml2.io.KXmlParser;
 import org.w3c.dom.Attr;
 import org.w3c.dom.DOMImplementation;
 import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.Text;
@@ -49,7 +53,7 @@
  */
 class DocumentBuilderImpl extends DocumentBuilder {
 
-    private static DOMImplementation dom = DOMImplementationImpl.getInstance();
+    private static DOMImplementationImpl dom = DOMImplementationImpl.getInstance();
 
     private boolean coalescing;
 
@@ -72,25 +76,6 @@
         return dom;
     }
 
-    /**
-     * Reflects whether this DocumentBuilder is configured to ignore comments.
-     * 
-     * @return True if and only if comments are ignored.
-     */
-    public boolean isIgnoringComments() {
-        return ignoreComments;
-    }
-
-    /**
-     * Reflects whether this DocumentBuilder is configured to ignore element
-     * content whitespace.
-     * 
-     * @return True if and only if whitespace element content is ignored.
-     */
-    public boolean isIgnoringElementContentWhitespace() {
-        return ignoreElementContentWhitespace;
-    }
-
     @Override
     public boolean isNamespaceAware() {
         return namespaceAware;
@@ -112,11 +97,14 @@
             throw new IllegalArgumentException();
         }
         
-        Document document = newDocument();
+        String namespaceURI = null;
+        String qualifiedName = null;
+        DocumentType doctype = null;
+        DocumentImpl document = new DocumentImpl(dom, namespaceURI, qualifiedName, doctype);
 
         try {
-            XmlPullParser parser = new KXmlParser();
-
+            KXmlParser parser = new KXmlParser();
+            parser.keepNamespaceAttributes();
             parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
                     namespaceAware);
             
@@ -189,7 +177,7 @@
      * @throws XmlPullParserException If a parsing error occurs.
      * @throws IOException If a general IO error occurs.
      */
-    private void parse(XmlPullParser parser, Document document, Node node,
+    private void parse(XmlPullParser parser, DocumentImpl document, Node node,
             int endToken) throws XmlPullParserException, IOException {
 
         int token = parser.getEventType();
@@ -271,7 +259,7 @@
                  * whitespace at all.
                  */
                 if (!ignoreElementContentWhitespace) {
-                    appendText(document, node, true, parser.getText());
+                    appendText(document, node, token, parser.getText());
                 }
             } else if (token == XmlPullParser.TEXT || token == XmlPullParser.CDSECT) {
                 /*
@@ -279,7 +267,7 @@
                  * That's the easiest case. We simply take it and create a new text node,
                  * or merge with an adjacent text node.
                  */
-                appendText(document, node, token == XmlPullParser.TEXT, parser.getText());
+                appendText(document, node, token, parser.getText());
             } else if (token == XmlPullParser.ENTITY_REF) {
                 /*
                  * Found an entity reference. If an entity resolver is
@@ -294,7 +282,7 @@
 
                 String replacement = resolveStandardEntity(entity);
                 if (replacement != null) {
-                    appendText(document, node, true, replacement);
+                    appendText(document, node, token, replacement);
                 } else {
                     node.appendChild(document.createEntityReference(entity));
                 }
@@ -380,17 +368,17 @@
     }
 
     /**
-     * @param isText true for a normal TextNode, false for a CDATA section.
-     * (If we're not coalescing, it matters which kind of node we put into the DOM.)
+     * @param token the XML pull parser token type, such as XmlPullParser.CDSECT
+     *      or XmlPullParser.ENTITY_REF.
      */ 
-    private void appendText(Document document, Node node, boolean isText, String text) {
+    private void appendText(DocumentImpl document, Node parent, int token, String text) {
         // Ignore empty runs.
         if (text.length() == 0) {
             return;
         }
         // Merge with any previous text node if possible.
         if (coalescing) {
-            Node lastChild = node.getLastChild();
+            Node lastChild = parent.getLastChild();
             if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) {
                 Text textNode = (Text) lastChild;
                 textNode.setData(textNode.getNodeValue() + text);
@@ -398,11 +386,9 @@
             }
         }
         // Okay, we really do need a new text node
-        if (isText) {
-            node.appendChild(document.createTextNode(text));
-        } else {
-            node.appendChild(document.createCDATASection(text));
-        }
+        parent.appendChild(token == XmlPullParser.CDSECT
+                ? new CDATASectionImpl(document, text)
+                : new TextImpl(document, text));
     }
 
     @Override
diff --git a/xml/src/main/java/org/kxml2/io/KXmlParser.java b/xml/src/main/java/org/kxml2/io/KXmlParser.java
index c4d8f3d..99eb03b 100644
--- a/xml/src/main/java/org/kxml2/io/KXmlParser.java
+++ b/xml/src/main/java/org/kxml2/io/KXmlParser.java
@@ -45,6 +45,7 @@
 
     private boolean processNsp;
     private boolean relaxed;
+    private boolean keepNamespaceAttributes; // android-added
     private Hashtable entityMap;
     private int depth;
     private String[] elementStack = new String[16];
@@ -80,6 +81,14 @@
 
     private boolean degenerated;
     private int attributeCount;
+
+    /**
+     * The current element's attributes arranged in groups of 4:
+     * i + 0 = attribute namespace URI
+     * i + 1 = attribute namespace prefix
+     * i + 2 = attribute qualified name (may contain ":", as in "html:h1")
+     * i + 3 = attribute value
+     */
     private String[] attributes = new String[16];
 //    private int stackMismatch = 0;
     private String error;
@@ -100,6 +109,19 @@
             new char[Runtime.getRuntime().freeMemory() >= 1048576 ? 8192 : 128];
     }
 
+    // BEGIN android-added
+    /**
+     * Retains namespace attributes like {@code xmlns="http://foo"} or {@code
+     * xmlns:foo="http:foo"} in pulled elements. Most applications will only be
+     * interested in the effective namespaces of their elements, so these
+     * attributes aren't useful. But for structure preserving wrappers like DOM,
+     * it is necessary to keep the namespace data around.
+     */
+    public void keepNamespaceAttributes() {
+        this.keepNamespaceAttributes = true;
+    }
+    // END android-added
+
     private final boolean isProp(String n1, boolean prop, String n2) {
         if (!n1.startsWith("http://xmlpull.org/v1/doc/"))
             return false;
@@ -148,14 +170,23 @@
 
                 //System.out.println (prefixMap);
 
-                System.arraycopy(
-                    attributes,
-                    i + 4,
-                    attributes,
-                    i,
-                    ((--attributeCount) << 2) - i);
+                // BEGIN android-changed
+                if (keepNamespaceAttributes) {
+                    // explicitly set the namespace for unprefixed attributes 
+                    // such as xmlns="http://foo"
+                    attributes[i] = "http://www.w3.org/2000/xmlns/";
+                    any = true;
+                } else {
+                    System.arraycopy(
+                            attributes,
+                            i + 4,
+                            attributes,
+                            i,
+                            ((--attributeCount) << 2) - i);
 
-                i -= 4;
+                    i -= 4;
+                }
+                // END android-changed
             }
         }
 
diff --git a/xml/src/main/java/org/w3c/dom/Attr.java b/xml/src/main/java/org/w3c/dom/Attr.java
index d9ed6ff..bd7267b 100644
--- a/xml/src/main/java/org/w3c/dom/Attr.java
+++ b/xml/src/main/java/org/w3c/dom/Attr.java
@@ -176,7 +176,7 @@
     /**
      * On retrieval, the value of the attribute is returned as a string. 
      * Character and general entity references are replaced with their 
-     * values. See also the method <code>getAttribute</code> on the 
+     * values. See also the method <code>getAttribute</code> on the
      * <code>Element</code> interface.
      * <br>On setting, this creates a <code>Text</code> node with the unparsed 
      * contents of the string, i.e. any characters that an XML processor 
diff --git a/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java b/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
index 02b6d80..ea7abed 100644
--- a/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
+++ b/xml/src/test/java/tests/api/javax/xml/parsers/DocumentBuilderTest.java
@@ -34,6 +34,7 @@
 import tests.api.org.xml.sax.support.MethodLogger;
 import tests.api.org.xml.sax.support.MockHandler;
 import tests.api.org.xml.sax.support.MockResolver;
+import tests.support.resource.Support_Resources;
 import tests.util.TestEnvironment;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -41,8 +42,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -272,7 +271,7 @@
         args = {java.io.File.class}
     )
     public void testGetBaseURI() throws IOException, SAXException {
-        File f = resourceToTmpFile("/simple.xml");
+        File f = Support_Resources.resourceToTempFile("/simple.xml");
         Document d = db.parse(f);
         assertTrue(d.getDocumentElement().getBaseURI().startsWith("file://"));
     }
@@ -291,7 +290,7 @@
         args = {java.io.File.class}
     )
     public void test_parseLjava_io_File() throws IOException {
-        File f = resourceToTmpFile("/simple.xml");
+        File f = Support_Resources.resourceToTempFile("/simple.xml");
 
         // case 1: Trivial use.
         try {
@@ -333,7 +332,7 @@
         }
 
         // case 4: Try to parse incorrect xml file
-        f = resourceToTmpFile("/wrong.xml");
+        f = Support_Resources.resourceToTempFile("/wrong.xml");
         try {
             db.parse(f);
             fail("Expected SAXException was not thrown");
@@ -344,22 +343,6 @@
         }
     }
 
-    private File resourceToTmpFile(String path) throws IOException,
-            FileNotFoundException {
-        File f = File.createTempFile("out", ".xml");
-        f.deleteOnExit();
-        FileOutputStream out = new FileOutputStream(f);
-
-        InputStream xml = getClass().getResourceAsStream(path);
-        while (xml.available() > 0) {
-            out.write(xml.read());
-        }
-        out.flush();
-        out.close();
-        xml.close();
-        return f;
-    }
-
     /**
      * @tests javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream)
      * Case 1: Try to parse correct xml document.
@@ -579,24 +562,18 @@
         method = "parse",
         args = {java.lang.String.class}
     )
-    public void test_parseLjava_lang_String() {
+    public void test_parseLjava_lang_String() throws Exception {
         // case 1: Trivial use.
         File f = new File(getClass().getResource("/simple.xml").getFile());
-        try {
-            Document d = db.parse(f.getAbsolutePath());
-            assertNotNull(d);
+        Document d = db.parse(f.getAbsolutePath());
+        assertNotNull(d);
 //          TBD  getXmlEncoding() is not supported
 //          assertEquals("ISO-8859-1", d.getXmlEncoding());
-            assertEquals(2, d.getChildNodes().getLength());
-            assertEquals("#comment",
-                    d.getChildNodes().item(0).getNodeName());
-            assertEquals("breakfast_menu",
-                    d.getChildNodes().item(1).getNodeName());
-        } catch (IOException ioe) {
-            fail("Unexpected IOException " + ioe.toString());
-        } catch (SAXException sax) {
-            fail("Unexpected SAXException " + sax.toString());
-        }
+        assertEquals(2, d.getChildNodes().getLength());
+        assertEquals("#comment",
+                d.getChildNodes().item(0).getNodeName());
+        assertEquals("breakfast_menu",
+                d.getChildNodes().item(1).getNodeName());
 
         // case 2: Try to call parse with null argument
         try {
@@ -604,10 +581,6 @@
             fail("Expected IllegalArgumentException was not thrown");
         } catch (IllegalArgumentException iae) {
             // expected
-        } catch (IOException ioe) {
-            fail("Unexpected IOException " + ioe.toString());
-        } catch (SAXException sax) {
-            fail("Unexpected SAXException " + sax.toString());
         }
 
         // case 3: Try to parse a non-existent uri
@@ -616,8 +589,6 @@
             fail("Expected IOException was not thrown");
         } catch (IOException ioe) {
             // expected
-        } catch (SAXException sax) {
-            fail("Unexpected SAXException " + sax.toString());
         }
 
         // case 4: Try to parse incorrect xml file
@@ -625,8 +596,6 @@
             f = new File(getClass().getResource("/wrong.xml").getFile());
             db.parse(f.getAbsolutePath());
             fail("Expected SAXException was not thrown");
-        } catch (IOException ioe) {
-            fail("Unexpected IOException " + ioe.toString());
         } catch (SAXException sax) {
             // expected
         }
diff --git a/xml/src/test/java/tests/api/javax/xml/parsers/SAXParserTest.java b/xml/src/test/java/tests/api/javax/xml/parsers/SAXParserTest.java
index ca7cf71..e6d6481 100644
--- a/xml/src/test/java/tests/api/javax/xml/parsers/SAXParserTest.java
+++ b/xml/src/test/java/tests/api/javax/xml/parsers/SAXParserTest.java
@@ -677,8 +677,7 @@
                 MyDefaultHandler dh = new MyDefaultHandler();
                 InputStream is = new FileInputStream(list_wf[i]);
                 parser.parse(is, dh, SAXParserTestSupport.XML_SYSTEM_ID);
-                assertTrue(SAXParserTestSupport.equalsMaps(hm, 
-                        dh.createData()));
+                assertEquals(hm, dh.createData());
             } catch (IOException ioe) {
                 fail("Unexpected IOException " + ioe.toString());
             } catch (SAXException sax) {
diff --git a/xml/src/test/java/tests/xml/AllTests.java b/xml/src/test/java/tests/xml/AllTests.java
index 45ca18e..597e35e 100644
--- a/xml/src/test/java/tests/xml/AllTests.java
+++ b/xml/src/test/java/tests/xml/AllTests.java
@@ -24,9 +24,10 @@
     public static Test suite() {
         TestSuite suite = tests.TestSuiteFactory.createTestSuite();
 
+        suite.addTestSuite(DomTest.class);
         suite.addTestSuite(SimpleParserTest.class);
         suite.addTestSuite(SimpleBuilderTest.class);
-        suite.addTestSuite(NodeTests.class);
+        suite.addTestSuite(NodeTest.class);
         
         //suite.addTest(tests.org.w3c.dom.AllTests.suite());
         suite.addTest(tests.api.javax.xml.parsers.AllTests.suite());
diff --git a/xml/src/test/java/tests/xml/DomTest.java b/xml/src/test/java/tests/xml/DomTest.java
new file mode 100644
index 0000000..69e8b37
--- /dev/null
+++ b/xml/src/test/java/tests/xml/DomTest.java
@@ -0,0 +1,748 @@
+/*
+ * 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 tests.xml;
+
+import junit.framework.TestCase;
+import org.w3c.dom.Attr;
+import org.w3c.dom.CDATASection;
+import org.w3c.dom.Comment;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Entity;
+import org.w3c.dom.EntityReference;
+import org.w3c.dom.Node;
+import org.w3c.dom.Notation;
+import org.w3c.dom.ProcessingInstruction;
+import org.w3c.dom.Text;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Construct a DOM and then interrogate it.
+ */
+public class DomTest extends TestCase {
+
+    private Transformer transformer;
+    private DocumentBuilder builder;
+    private DOMImplementation domImplementation;
+
+    private final String xml
+            = "<!DOCTYPE menu ["
+            + "  <!ENTITY sp \"Maple Syrup\">"
+            + "  <!NOTATION png SYSTEM \"image/png\">"
+            + "]>"
+            + "<menu>\n"
+            + "  <item xmlns=\"http://food\" xmlns:a=\"http://addons\">\n"
+            + "    <name a:standard=\"strawberry\" deluxe=\"&sp;\">Waffles</name>\n"
+            + "    <description xmlns=\"http://marketing\">Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)</description>\n"
+            + "    <a:option>Whipped Cream</a:option>\n"
+            + "    <a:option>&sp;</a:option>\n"
+            + "    <?wafflemaker square shape?>\n"
+            + "    <nutrition>\n"
+            + "      <a:vitamins xmlns:a=\"http://usda\">\n"
+            + "        <!-- add other vitamins? --> \n"
+            + "        <a:vitaminc>60%</a:vitaminc>\n"
+            + "      </a:vitamins>\n"
+            + "    </nutrition>\n"
+            + "  </item>\n"
+            + "</menu>";
+
+    private Document document;
+    private DocumentType doctype;
+    private Entity sp;
+    private Notation png;
+    private Element menu;
+    private Element item;
+    private Attr itemXmlns;
+    private Attr itemXmlnsA;
+    private Element name;
+    private Attr standard;
+    private Attr deluxe;
+    private Element description;
+    private Text descriptionText1;
+    private CDATASection descriptionText2;
+    private Text descriptionText3;
+    private Element option1;
+    private Element option2;
+    private Node option2Reference; // resolved to Text on RI, an EntityReference on Dalvik
+    private ProcessingInstruction wafflemaker;
+    private Element nutrition;
+    private Element vitamins;
+    private Attr vitaminsXmlnsA;
+    private Comment comment;
+    private Element vitaminc;
+    private Text vitamincText;
+    private List<Node> allNodes;
+
+    @Override protected void setUp() throws Exception {
+        transformer = TransformerFactory.newInstance().newTransformer();
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        builder = factory.newDocumentBuilder();
+        domImplementation = builder.getDOMImplementation();
+        document = builder.parse(new InputSource(new StringReader(xml)));
+
+        // doctype nodes
+        doctype = document.getDoctype();
+        if (doctype.getEntities() != null) {
+            sp = (Entity) doctype.getEntities().item(0);
+        }
+        if (doctype.getNotations() != null) {
+            png = (Notation) doctype.getNotations().item(0);
+        }
+
+        // document nodes
+        menu = document.getDocumentElement();
+        item = (Element) menu.getChildNodes().item(1);
+        itemXmlns = item.getAttributeNode("xmlns");
+        itemXmlnsA = item.getAttributeNode("xmlns:a");
+        name = (Element) item.getChildNodes().item(1);
+        standard = name.getAttributeNode("a:standard");
+        deluxe = name.getAttributeNode("deluxe");
+        description = (Element) item.getChildNodes().item(3);
+        descriptionText1 = (Text) description.getChildNodes().item(0);
+        descriptionText2 = (CDATASection) description.getChildNodes().item(1);
+        descriptionText3 = (Text) description.getChildNodes().item(2);
+        option1 = (Element) item.getChildNodes().item(5);
+        option2 = (Element) item.getChildNodes().item(7);
+        option2Reference = option2.getChildNodes().item(0);
+        wafflemaker = (ProcessingInstruction) item.getChildNodes().item(9);
+        nutrition = (Element) item.getChildNodes().item(11);
+        vitamins = (Element) nutrition.getChildNodes().item(1);
+        vitaminsXmlnsA = vitamins.getAttributeNode("xmlns:a");
+        comment = (Comment) vitamins.getChildNodes().item(1);
+        vitaminc = (Element) vitamins.getChildNodes().item(3);
+        vitamincText = (Text) vitaminc.getChildNodes().item(0);
+
+        allNodes = new ArrayList<Node>();
+
+        if (sp != null) {
+            allNodes.add(sp);
+        }
+        if (png != null) {
+            allNodes.add(png);
+        }
+
+        allNodes.addAll(Arrays.asList(document, doctype, menu, item, itemXmlns,
+                itemXmlnsA, name, standard, deluxe, description,
+                descriptionText1, descriptionText2, descriptionText3, option1,
+                option2, option2Reference, wafflemaker, nutrition, vitamins,
+                vitaminsXmlnsA, comment, vitaminc, vitamincText));
+    }
+
+    /**
+     * Android's parsed DOM doesn't include entity declarations. These nodes will
+     * only be tested for implementations that support them.
+     */
+    public void testEntityDeclarations() {
+        assertNotNull("This implementation does not parse entity declarations", sp);
+    }
+
+    /**
+     * Android's parsed DOM doesn't include notations. These nodes will only be
+     * tested for implementations that support them.
+     */
+    public void testNotations() {
+        assertNotNull("This implementation does not parse notations", png);
+    }
+
+    public void testLookupNamespaceURIByPrefix() {
+        assertEquals(null, doctype.lookupNamespaceURI("a"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupNamespaceURI("a"));
+        }
+        if (png != null) {
+            assertEquals(null, png.lookupNamespaceURI("a"));
+        }
+        assertEquals(null, document.lookupNamespaceURI("a"));
+        assertEquals(null, menu.lookupNamespaceURI("a"));
+        assertEquals("http://addons", item.lookupNamespaceURI("a"));
+        assertEquals("http://addons", itemXmlns.lookupNamespaceURI("a"));
+        assertEquals("http://addons", itemXmlnsA.lookupNamespaceURI("a"));
+        assertEquals("http://addons", name.lookupNamespaceURI("a"));
+        assertEquals("http://addons", standard.lookupNamespaceURI("a"));
+        assertEquals("http://addons", deluxe.lookupNamespaceURI("a"));
+        assertEquals("http://addons", description.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText1.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText2.lookupNamespaceURI("a"));
+        assertEquals("http://addons", descriptionText3.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option1.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option2.lookupNamespaceURI("a"));
+        assertEquals("http://addons", option2Reference.lookupNamespaceURI("a"));
+        assertEquals("http://addons", wafflemaker.lookupNamespaceURI("a"));
+        assertEquals("http://addons", nutrition.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitamins.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitaminsXmlnsA.lookupNamespaceURI("a"));
+        assertEquals("http://usda", comment.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitaminc.lookupNamespaceURI("a"));
+        assertEquals("http://usda", vitamincText.lookupNamespaceURI("a"));
+    }
+
+    public void testLookupNamespaceURIWithNullPrefix() {
+        assertEquals(null, document.lookupNamespaceURI(null));
+        assertEquals(null, doctype.lookupNamespaceURI(null));
+        if (sp != null) {
+            assertEquals(null, sp.lookupNamespaceURI(null));
+        }
+        if (png != null) {
+            assertEquals(null, png.lookupNamespaceURI(null));
+        }
+        assertEquals(null, menu.lookupNamespaceURI(null));
+        assertEquals("http://food", item.lookupNamespaceURI(null));
+        assertEquals("http://food", itemXmlns.lookupNamespaceURI(null));
+        assertEquals("http://food", itemXmlnsA.lookupNamespaceURI(null));
+        assertEquals("http://food", name.lookupNamespaceURI(null));
+        assertEquals("http://food", standard.lookupNamespaceURI(null));
+        assertEquals("http://food", deluxe.lookupNamespaceURI(null));
+        assertEquals("http://marketing", description.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText1.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText2.lookupNamespaceURI(null));
+        assertEquals("http://marketing", descriptionText3.lookupNamespaceURI(null));
+        assertEquals("http://food", option1.lookupNamespaceURI(null));
+        assertEquals("http://food", option2.lookupNamespaceURI(null));
+        assertEquals("http://food", option2Reference.lookupNamespaceURI(null));
+        assertEquals("http://food", wafflemaker.lookupNamespaceURI(null));
+        assertEquals("http://food", nutrition.lookupNamespaceURI(null));
+        assertEquals("http://food", vitamins.lookupNamespaceURI(null));
+        assertEquals("http://food", vitaminsXmlnsA.lookupNamespaceURI(null));
+        assertEquals("http://food", comment.lookupNamespaceURI(null));
+        assertEquals("http://food", vitaminc.lookupNamespaceURI(null));
+        assertEquals("http://food", vitamincText.lookupNamespaceURI(null));
+    }
+
+    public void testLookupNamespaceURIWithXmlnsPrefix() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupNamespaceURI("xmlns"));
+        }
+    }
+
+    public void testLookupPrefixWithShadowedUri() {
+        assertEquals(null, document.lookupPrefix("http://addons"));
+        assertEquals(null, doctype.lookupPrefix("http://addons"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupPrefix("http://addons"));
+        }
+        if (png != null) {
+            assertEquals(null, png.lookupPrefix("http://addons"));
+        }
+        assertEquals(null, menu.lookupPrefix("http://addons"));
+        assertEquals("a", item.lookupPrefix("http://addons"));
+        assertEquals("a", itemXmlns.lookupPrefix("http://addons"));
+        assertEquals("a", itemXmlnsA.lookupPrefix("http://addons"));
+        assertEquals("a", name.lookupPrefix("http://addons"));
+        assertEquals("a", standard.lookupPrefix("http://addons"));
+        assertEquals("a", deluxe.lookupPrefix("http://addons"));
+        assertEquals("a", description.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText1.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText2.lookupPrefix("http://addons"));
+        assertEquals("a", descriptionText3.lookupPrefix("http://addons"));
+        assertEquals("a", option1.lookupPrefix("http://addons"));
+        assertEquals("a", option2.lookupPrefix("http://addons"));
+        assertEquals("a", option2Reference.lookupPrefix("http://addons"));
+        assertEquals("a", wafflemaker.lookupPrefix("http://addons"));
+        assertEquals("a", nutrition.lookupPrefix("http://addons"));
+        assertEquals(null, vitamins.lookupPrefix("http://addons"));
+        assertEquals(null, vitaminsXmlnsA.lookupPrefix("http://addons"));
+        assertEquals(null, comment.lookupPrefix("http://addons"));
+        assertEquals(null, vitaminc.lookupPrefix("http://addons"));
+        assertEquals(null, vitamincText.lookupPrefix("http://addons"));
+    }
+
+    public void testLookupPrefixWithUnusedUri() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupPrefix("http://unused"));
+        }
+    }
+
+    public void testLookupPrefixWithNullUri() {
+        for (Node node : allNodes) {
+            assertEquals(null, node.lookupPrefix(null));
+        }
+    }
+
+    public void testLookupPrefixWithShadowingUri() {
+        assertEquals(null, document.lookupPrefix("http://usda"));
+        assertEquals(null, doctype.lookupPrefix("http://usda"));
+        if (sp != null) {
+            assertEquals(null, sp.lookupPrefix("http://usda"));
+        }
+        if (png != null) {
+            assertEquals(null, png.lookupPrefix("http://usda"));
+        }
+        assertEquals(null, menu.lookupPrefix("http://usda"));
+        assertEquals(null, item.lookupPrefix("http://usda"));
+        assertEquals(null, itemXmlns.lookupPrefix("http://usda"));
+        assertEquals(null, itemXmlnsA.lookupPrefix("http://usda"));
+        assertEquals(null, name.lookupPrefix("http://usda"));
+        assertEquals(null, standard.lookupPrefix("http://usda"));
+        assertEquals(null, deluxe.lookupPrefix("http://usda"));
+        assertEquals(null, description.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText1.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText2.lookupPrefix("http://usda"));
+        assertEquals(null, descriptionText3.lookupPrefix("http://usda"));
+        assertEquals(null, option1.lookupPrefix("http://usda"));
+        assertEquals(null, option2.lookupPrefix("http://usda"));
+        assertEquals(null, option2Reference.lookupPrefix("http://usda"));
+        assertEquals(null, wafflemaker.lookupPrefix("http://usda"));
+        assertEquals(null, nutrition.lookupPrefix("http://usda"));
+        assertEquals("a", vitamins.lookupPrefix("http://usda"));
+        assertEquals("a", vitaminsXmlnsA.lookupPrefix("http://usda"));
+        assertEquals("a", comment.lookupPrefix("http://usda"));
+        assertEquals("a", vitaminc.lookupPrefix("http://usda"));
+        assertEquals("a", vitamincText.lookupPrefix("http://usda"));
+    }
+
+    public void testIsDefaultNamespace() {
+        assertFalse(document.isDefaultNamespace("http://food"));
+        assertFalse(doctype.isDefaultNamespace("http://food"));
+        if (sp != null) {
+            assertFalse(sp.isDefaultNamespace("http://food"));
+        }
+        if (png != null) {
+            assertFalse(png.isDefaultNamespace("http://food"));
+        }
+        assertFalse(menu.isDefaultNamespace("http://food"));
+        assertTrue(item.isDefaultNamespace("http://food"));
+        assertTrue(itemXmlns.isDefaultNamespace("http://food"));
+        assertTrue(itemXmlnsA.isDefaultNamespace("http://food"));
+        assertTrue(name.isDefaultNamespace("http://food"));
+        assertTrue(standard.isDefaultNamespace("http://food"));
+        assertTrue(deluxe.isDefaultNamespace("http://food"));
+        assertFalse(description.isDefaultNamespace("http://food"));
+        assertFalse(descriptionText1.isDefaultNamespace("http://food"));
+        assertFalse(descriptionText2.isDefaultNamespace("http://food"));
+        assertFalse(descriptionText3.isDefaultNamespace("http://food"));
+        assertTrue(option1.isDefaultNamespace("http://food"));
+        assertTrue(option2.isDefaultNamespace("http://food"));
+        assertTrue(option2Reference.isDefaultNamespace("http://food"));
+        assertTrue(wafflemaker.isDefaultNamespace("http://food"));
+        assertTrue(nutrition.isDefaultNamespace("http://food"));
+        assertTrue(vitamins.isDefaultNamespace("http://food"));
+        assertTrue(vitaminsXmlnsA.isDefaultNamespace("http://food"));
+        assertTrue(comment.isDefaultNamespace("http://food"));
+        assertTrue(vitaminc.isDefaultNamespace("http://food"));
+        assertTrue(vitamincText.isDefaultNamespace("http://food"));
+    }
+
+    /**
+     * Xerces fails this test. It returns false always for entity, notation,
+     * document fragment and document type nodes. This contradicts its own
+     * behaviour on lookupNamespaceURI(null).
+     */
+    public void testIsDefaultNamespaceNull_XercesBugs() {
+        String message = "isDefaultNamespace() should be consistent with lookupNamespaceURI(null)";
+        assertTrue(message, doctype.isDefaultNamespace(null));
+        if (sp != null) {
+            assertTrue(message, sp.isDefaultNamespace(null));
+        }
+        if (png != null) {
+            assertTrue(message, png.isDefaultNamespace(null));
+        }
+    }
+
+    public void testIsDefaultNamespaceNull() {
+        assertTrue(document.isDefaultNamespace(null));
+        assertTrue(menu.isDefaultNamespace(null));
+        assertFalse(item.isDefaultNamespace(null));
+        assertFalse(itemXmlns.isDefaultNamespace(null));
+        assertFalse(itemXmlnsA.isDefaultNamespace(null));
+        assertFalse(name.isDefaultNamespace(null));
+        assertFalse(standard.isDefaultNamespace(null));
+        assertFalse(deluxe.isDefaultNamespace(null));
+        assertFalse(description.isDefaultNamespace(null));
+        assertFalse(descriptionText1.isDefaultNamespace(null));
+        assertFalse(descriptionText2.isDefaultNamespace(null));
+        assertFalse(descriptionText3.isDefaultNamespace(null));
+        assertFalse(option1.isDefaultNamespace(null));
+        assertFalse(option2.isDefaultNamespace(null));
+        assertFalse(option2Reference.isDefaultNamespace(null));
+        assertFalse(wafflemaker.isDefaultNamespace(null));
+        assertFalse(nutrition.isDefaultNamespace(null));
+        assertFalse(vitamins.isDefaultNamespace(null));
+        assertFalse(vitaminsXmlnsA.isDefaultNamespace(null));
+        assertFalse(comment.isDefaultNamespace(null));
+        assertFalse(vitaminc.isDefaultNamespace(null));
+        assertFalse(vitamincText.isDefaultNamespace(null));
+    }
+
+    public void testDoctypeSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        doctype.setTextContent("foobar"); // strangely, this is specified to no-op
+        assertEquals(original, domToString(document));
+    }
+
+    public void testDocumentSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        document.setTextContent("foobar"); // strangely, this is specified to no-op
+        assertEquals(original, domToString(document));
+    }
+
+    public void testElementSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        nutrition.setTextContent("foobar");
+        String expected = original.replaceFirst(
+                "(?s)<nutrition>.*</nutrition>", "<nutrition>foobar</nutrition>");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testEntitySetTextContent() throws TransformerException {
+        if (sp == null) {
+            return;
+        }
+        try {
+            sp.setTextContent("foobar");
+            fail(); // is this implementation-specific behaviour?
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testNotationSetTextContent() throws TransformerException {
+        if (png == null) {
+            return;
+        }
+        String original = domToString(document);
+        png.setTextContent("foobar");
+        String expected = original.replace("image/png", "foobar");
+        assertEquals(expected, domToString(document));
+    }
+
+    /**
+     * Tests setTextContent on entity references. Although the other tests can
+     * act on a parsed DOM, this needs to use a programmatically constructed DOM
+     * because the parser may have replaced the entity reference with the
+     * corresponding text.
+     */
+    public void testEntityReferenceSetTextContent() throws TransformerException {
+        document = builder.newDocument();
+        Element root = document.createElement("menu");
+        document.appendChild(root);
+
+        EntityReference entityReference = document.createEntityReference("sp");
+        root.appendChild(entityReference);
+
+        try {
+            entityReference.setTextContent("Lite Syrup");
+            fail();
+        } catch (DOMException e) {
+        }
+    }
+
+    public void testAttributeSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        standard.setTextContent("foobar");
+        String expected = original.replace("standard=\"strawberry\"", "standard=\"foobar\"");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testTextSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        descriptionText1.setTextContent("foobar");
+        String expected = original.replace(">Belgian<!", ">foobar<!");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testCdataSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        descriptionText2.setTextContent("foobar");
+        String expected = original.replace(
+                " waffles & strawberries (< 5g ", "foobar");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testProcessingInstructionSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        wafflemaker.setTextContent("foobar");
+        String expected = original.replace(" square shape?>", " foobar?>");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testCommentSetTextContent() throws TransformerException {
+        String original = domToString(document);
+        comment.setTextContent("foobar");
+        String expected = original.replace("-- add other vitamins? --", "--foobar--");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testCoreFeature() {
+        assertTrue(domImplementation.hasFeature("Core", null));
+        assertTrue(domImplementation.hasFeature("Core", ""));
+        assertTrue(domImplementation.hasFeature("Core", "1.0"));
+        assertTrue(domImplementation.hasFeature("Core", "2.0"));
+        assertTrue(domImplementation.hasFeature("Core", "3.0"));
+        assertTrue(domImplementation.hasFeature("CORE", "3.0"));
+        assertTrue(domImplementation.hasFeature("+Core", "3.0"));
+        assertFalse(domImplementation.hasFeature("Core", "4.0"));
+    }
+
+    public void testXmlFeature() {
+        assertTrue(domImplementation.hasFeature("XML", null));
+        assertTrue(domImplementation.hasFeature("XML", ""));
+        assertTrue(domImplementation.hasFeature("XML", "1.0"));
+        assertTrue(domImplementation.hasFeature("XML", "2.0"));
+        assertTrue(domImplementation.hasFeature("XML", "3.0"));
+        assertTrue(domImplementation.hasFeature("Xml", "3.0"));
+        assertTrue(domImplementation.hasFeature("+XML", "3.0"));
+        assertFalse(domImplementation.hasFeature("XML", "4.0"));
+    }
+
+    /**
+     * The RI fails this test.
+     * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Document3-version
+     */
+    public void testXmlVersionFeature() {
+        String message = "This implementation does not support the XMLVersion feature";
+        assertTrue(message, domImplementation.hasFeature("XMLVersion", null));
+        assertTrue(message, domImplementation.hasFeature("XMLVersion", ""));
+        assertTrue(message, domImplementation.hasFeature("XMLVersion", "1.0"));
+        assertTrue(message, domImplementation.hasFeature("XMLVersion", "1.1"));
+        assertTrue(message, domImplementation.hasFeature("XMLVERSION", "1.1"));
+        assertTrue(message, domImplementation.hasFeature("+XMLVersion", "1.1"));
+        assertFalse(domImplementation.hasFeature("XMLVersion", "1.2"));
+        assertFalse(domImplementation.hasFeature("XMLVersion", "2.0"));
+        assertFalse(domImplementation.hasFeature("XMLVersion", "2.0"));
+    }
+
+    public void testLsFeature() {
+        assertTrue("This implementation does not support the LS feature",
+                domImplementation.hasFeature("LS", "3.0"));
+    }
+
+    public void testElementTraversalFeature() {
+        assertTrue("This implementation does not support the ElementTraversal feature",
+                domImplementation.hasFeature("ElementTraversal", "1.0"));
+    }
+
+    public void testIsSupported() {
+        // we don't independently test the features; instead just assume the
+        // implementation calls through to hasFeature (as tested above)
+        for (Node node : allNodes) {
+            assertTrue(node.isSupported("XML", null));
+            assertTrue(node.isSupported("XML", "3.0"));
+            assertFalse(node.isSupported("foo", null));
+            assertFalse(node.isSupported("foo", "bar"));
+        }
+    }
+
+    public void testGetFeature() {
+        // we don't independently test the features; instead just assume the
+        // implementation calls through to hasFeature (as tested above)
+        for (Node node : allNodes) {
+            assertSame(node, node.getFeature("XML", null));
+            assertSame(node, node.getFeature("XML", "3.0"));
+            assertNull(node.getFeature("foo", null));
+            assertNull(node.getFeature("foo", "bar"));
+        }
+    }
+
+    public void testNodeEqualsPositive() throws Exception {
+        DomTest copy = new DomTest();
+        copy.setUp();
+        
+        for (int i = 0; i < allNodes.size(); i++) {
+            Node a = allNodes.get(i);
+            Node b = copy.allNodes.get(i);
+            assertTrue(a.isEqualNode(b));
+        }
+    }
+
+    public void testNodeEqualsNegative() throws Exception {
+        for (Node a : allNodes) {
+            for (Node b : allNodes) {
+                assertEquals(a == b, a.isEqualNode(b));
+            }
+        }
+    }
+
+    public void testNodeEqualsNegativeRecursive() throws Exception {
+        DomTest copy = new DomTest();
+        copy.setUp();
+        copy.vitaminc.setTextContent("55%");
+
+        // changing anything about a node should break equality for all parents
+        assertFalse(document.isEqualNode(copy.document));
+        assertFalse(menu.isEqualNode(copy.menu));
+        assertFalse(item.isEqualNode(copy.item));
+        assertFalse(nutrition.isEqualNode(copy.nutrition));
+        assertFalse(vitamins.isEqualNode(copy.vitamins));
+        assertFalse(vitaminc.isEqualNode(copy.vitaminc));
+
+        // but not siblings
+        assertTrue(doctype.isEqualNode(copy.doctype));
+        assertTrue(description.isEqualNode(copy.description));
+        assertTrue(option1.isEqualNode(copy.option1));
+    }
+
+    public void testNodeEqualsNull() {
+        for (Node node : allNodes) {
+            try {
+                node.isEqualNode(null);
+                fail();
+            } catch (NullPointerException e) {
+            }
+        }
+    }
+
+    public void testIsElementContentWhitespaceWithoutDeclaration() throws Exception {
+        String xml = "<menu>    <item/>   </menu>";
+
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        Text text = (Text) factory.newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)))
+                .getDocumentElement().getChildNodes().item(0);
+        assertFalse(text.isElementContentWhitespace());
+    }
+
+    public void testIsElementContentWhitespaceWithDeclaration() throws Exception {
+        String xml = "<!DOCTYPE menu [\n"
+                + "  <!ELEMENT menu (item)*>\n"
+                + "  <!ELEMENT item (#PCDATA)>\n"
+                + "]><menu>    <item/>   </menu>";
+
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        Text text = (Text) factory.newDocumentBuilder()
+                .parse(new InputSource(new StringReader(xml)))
+                .getDocumentElement().getChildNodes().item(0);
+        assertTrue("This implementation does not recognize element content whitespace",
+                text.isElementContentWhitespace());
+    }
+
+    public void testGetWholeTextFirst() {
+        assertEquals("Belgian waffles & strawberries (< 5g of fat)",
+                descriptionText1.getWholeText());
+    }
+
+    public void testGetWholeTextMiddle() {
+        assertEquals("This implementation doesn't include preceding nodes in getWholeText()",
+                "Belgian waffles & strawberries (< 5g of fat)", descriptionText2.getWholeText());
+    }
+
+    public void testGetWholeTextLast() {
+        assertEquals("This implementation doesn't include preceding nodes in getWholeText()",
+                "Belgian waffles & strawberries (< 5g of fat)", descriptionText3.getWholeText());
+    }
+
+    public void testGetWholeTextOnly() {
+        assertEquals("60%", vitamincText.getWholeText());
+    }
+
+    public void testGetWholeTextWithEntityReference() {
+        EntityReference spReference = document.createEntityReference("sp");
+        description.insertBefore(spReference, descriptionText2);
+
+        assertEquals("This implementation doesn't resolve entity references in getWholeText()",
+                "BelgianMaple Syrup waffles & strawberries (< 5g of fat)",
+                descriptionText1.getWholeText());
+    }
+
+    public void testReplaceWholeTextFirst() throws TransformerException {
+        String original = domToString(document);
+        Text replacement = descriptionText1.replaceWholeText("Eggos");
+        assertSame(descriptionText1, replacement);
+        String expected = original.replace(
+                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "Eggos");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextMiddle() throws TransformerException {
+        String original = domToString(document);
+        Text replacement = descriptionText2.replaceWholeText("Eggos");
+        assertSame(descriptionText2, replacement);
+        String expected = original.replace(
+                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "<![CDATA[Eggos]]>");
+        assertEquals("This implementation doesn't remove preceding nodes in replaceWholeText()",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextLast() throws TransformerException {
+        String original = domToString(document);
+        Text replacement = descriptionText3.replaceWholeText("Eggos");
+        assertSame(descriptionText3, replacement);
+        String expected = original.replace(
+                "Belgian<![CDATA[ waffles & strawberries (< 5g ]]>of fat)", "Eggos");
+        assertEquals("This implementation doesn't remove preceding nodes in replaceWholeText()",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextOnly() throws TransformerException {
+        String original = domToString(document);
+        Text replacement = vitamincText.replaceWholeText("70%");
+        assertEquals(Node.TEXT_NODE, replacement.getNodeType());
+        assertSame(vitamincText, replacement);
+        String expected = original.replace("60%", "70%");
+        assertEquals(expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextFirstWithNull() throws TransformerException {
+        String original = domToString(document);
+        assertNull(descriptionText1.replaceWholeText(null));
+        String expected = original.replaceFirst(">.*</description>", "/>");
+        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextMiddleWithNull() throws TransformerException {
+        String original = domToString(document);
+        assertNull(descriptionText2.replaceWholeText(null));
+        String expected = original.replaceFirst(">.*</description>", "/>");
+        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextLastWithNull() throws TransformerException {
+        String original = domToString(document);
+        assertNull(descriptionText3.replaceWholeText(null));
+        String expected = original.replaceFirst(">.*</description>", "/>");
+        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextFirstWithEmptyString() throws TransformerException {
+        String original = domToString(document);
+        assertNull(descriptionText1.replaceWholeText(""));
+        String expected = original.replaceFirst(">.*</description>", "/>");
+        assertEquals("This implementation doesn't remove adjacent nodes in replaceWholeText(null)",
+                expected, domToString(document));
+    }
+
+    public void testReplaceWholeTextOnlyWithEmptyString() throws TransformerException {
+        String original = domToString(document);
+        assertNull(vitamincText.replaceWholeText(""));
+        String expected = original.replaceFirst(">.*</a:vitaminc>", "/>");
+        assertEquals(expected, domToString(document));
+    }
+
+    private String domToString(Document document) throws TransformerException {
+        StringWriter writer = new StringWriter();
+        transformer.transform(new DOMSource(document), new StreamResult(writer));
+        return writer.toString();
+    }
+}
diff --git a/xml/src/test/java/tests/xml/NodeTest.java b/xml/src/test/java/tests/xml/NodeTest.java
new file mode 100644
index 0000000..dc3a333
--- /dev/null
+++ b/xml/src/test/java/tests/xml/NodeTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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 tests.xml;
+
+import dalvik.annotation.TestTargetClass;
+import junit.framework.TestCase;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import tests.support.resource.Support_Resources;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+@TestTargetClass(Node.class)
+public class NodeTest extends TestCase {
+
+    /**
+     * For bug 779: Node#getNextSibling throws IndexOutOfBoundsException.
+     */
+    public void test_getNextSibling() throws Exception {
+        // Calling getNextSibling when there is no next sibling should return null.
+        // From http://code.google.com/p/android/issues/detail?id=779.
+        ByteArrayInputStream bis = new ByteArrayInputStream("<root/>".getBytes());
+        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bis);
+        Node root = document.getDocumentElement();
+        assertNull(root.getNextSibling());
+    }
+
+    public void testGetBaseUri() throws Exception {
+        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        File file = Support_Resources.resourceToTempFile("/simple.xml");
+        Document document = builder.parse(file);
+
+        String baseUri = "file:" + file.getPath();
+        assertEquals(baseUri, document.getBaseURI());
+
+        Element documentElement = document.getDocumentElement();
+        for (Node node : flattenSubtree(documentElement)) {
+            if (node.getNodeType() == Node.ELEMENT_NODE
+                    || node.getNodeType() == Node.DOCUMENT_NODE) {
+                assertEquals("Unexpected base URI for " + node, baseUri, node.getBaseURI());
+            } else {
+                assertNull("Unexpected base URI for " + node, node.getBaseURI());
+            }
+        }
+
+        // TODO: test other node types
+        // TODO: test resolution of relative paths
+        // TODO: test URI santization
+    }
+
+    private List<Node> flattenSubtree(Node subtree) {
+        List<Node> result = new ArrayList<Node>();
+        traverse(subtree, result);
+        return result;
+    }
+
+    private void traverse(Node node, List<Node> sink) {
+        sink.add(node);
+
+        NodeList children = node.getChildNodes();
+        for (int i = 0; i < children.getLength(); i++) {
+            traverse(children.item(i), sink);
+        }
+    }
+}
diff --git a/xml/src/test/java/tests/xml/NodeTests.java b/xml/src/test/java/tests/xml/NodeTests.java
deleted file mode 100644
index e46e216..0000000
--- a/xml/src/test/java/tests/xml/NodeTests.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 tests.xml;
-
-import dalvik.annotation.TestLevel;
-import dalvik.annotation.TestTargetNew;
-import dalvik.annotation.TestTargetClass;
-
-import junit.framework.TestCase;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-
-import java.io.ByteArrayInputStream;
-import javax.xml.parsers.DocumentBuilderFactory;
-
-@TestTargetClass(Node.class)
-public class NodeTests extends TestCase {
-    @TestTargetNew(
-        level = TestLevel.PARTIAL,
-        notes = "Issue #779: org.w3c.dom.Node#getNextSibling throws IndexOutOfBoundsException.",
-        method = "getNextSibling",
-        args = {}
-    )
-    public void test_getNextSibling() throws Exception {
-        // Calling getNextSibling when there is no next sibling should return null.
-        // From http://code.google.com/p/android/issues/detail?id=779.
-        ByteArrayInputStream bis = new ByteArrayInputStream("<root/>".getBytes());
-        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(bis);
-        Node root = document.getDocumentElement();
-        assertNull(root.getNextSibling());
-    }
-}