Move brillopad in tradefed to new project.

Code has been unmodified except for update packages and references, and
removing CLog statements (to remove the dependency on ddmlib).  Added
.classpath and .project files for eclipse, and Android.mk files for the make
system.

Change-Id: I64d6764c08332b1c420a8f563a026aaa547e45bf
diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..adc2064
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/tradefederation"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ddb0a2d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+bin
+.settings
diff --git a/.project b/.project
new file mode 100644
index 0000000..4278633
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>loganalysis</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..ead7a6d
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,41 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Only compile source java files in this lib.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVACFLAGS += -g -Xlint
+
+LOCAL_MODULE := loganalysis
+LOCAL_MODULE_TAGS := optional
+LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt junit
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# makefile rules to copy jars to HOST_OUT/tradefed
+# so tradefed.sh can automatically add to classpath
+
+DEST_JAR := $(HOST_OUT)/tradefed/$(LOCAL_MODULE).jar
+$(DEST_JAR): $(LOCAL_BUILT_MODULE)
+	$(copy-file-to-new-target)
+
+# this dependency ensure the above rule will be executed if module is built
+$(LOCAL_INSTALLED_MODULE) : $(DEST_JAR)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/src/com/android/loganalysis/item/AnrItem.java b/src/com/android/loganalysis/item/AnrItem.java
new file mode 100644
index 0000000..d6bbae3
--- /dev/null
+++ b/src/com/android/loganalysis/item/AnrItem.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store ANR info.
+ */
+public class AnrItem extends GenericLogcatItem {
+    public static final String TYPE = "ANR";
+
+    /**
+     * An enum used to select the CPU usage category.
+     */
+    public enum CpuUsageCategory {
+        TOTAL,
+        USER,
+        KERNEL,
+        IOWAIT,
+    }
+
+    /**
+     * An enum used to select the load category.
+     */
+    public enum LoadCategory {
+        LOAD_1,
+        LOAD_5,
+        LOAD_15;
+    }
+
+    private static final String ACTIVITY = "ACTIVITY";
+    private static final String REASON = "REASON";
+    private static final String STACK = "STACK";
+    private static final String TRACE = "TRACE";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+        CpuUsageCategory.TOTAL.toString(),
+        CpuUsageCategory.USER.toString(),
+        CpuUsageCategory.KERNEL.toString(),
+        CpuUsageCategory.IOWAIT.toString(),
+        LoadCategory.LOAD_1.toString(),
+        LoadCategory.LOAD_5.toString(),
+        LoadCategory.LOAD_15.toString(),
+        ACTIVITY, REASON, STACK, TRACE));
+
+    /**
+     * The constructor for {@link AnrItem}.
+     */
+    public AnrItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the CPU usage for a given category.
+     */
+    public Double getCpuUsage(CpuUsageCategory category) {
+        return (Double) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the CPU usage for a given category.
+     */
+    public void setCpuUsage(CpuUsageCategory category, Double usage) {
+        setAttribute(category.toString(), usage);
+    }
+
+    /**
+     * Get the load for a given category.
+     */
+    public Double getLoad(LoadCategory category) {
+        return (Double) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the load for a given category.
+     */
+    public void setLoad(LoadCategory category, Double usage) {
+        setAttribute(category.toString(), usage);
+    }
+
+    /**
+     * Get the activity for the ANR.
+     */
+    public String getActivity() {
+        return (String) getAttribute(ACTIVITY);
+    }
+
+    /**
+     * Set the activity for the ANR.
+     */
+    public void setActivity(String activity) {
+        setAttribute(ACTIVITY, activity);
+    }
+
+    /**
+     * Get the reason for the ANR.
+     */
+    public String getReason() {
+        return (String) getAttribute(REASON);
+    }
+
+    /**
+     * Set the reason for the ANR.
+     */
+    public void setReason(String reason) {
+        setAttribute(REASON, reason);
+    }
+
+    /**
+     * Get the stack for the ANR.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the ANR.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+
+    /**
+     * Get the main trace for the ANR.
+     */
+    public String getTrace() {
+        return (String) getAttribute(TRACE);
+    }
+
+    /**
+     * Set the main trace for the ANR.
+     */
+    public void setTrace(String trace) {
+        setAttribute(TRACE, trace);
+    }
+}
diff --git a/src/com/android/loganalysis/item/BugreportItem.java b/src/com/android/loganalysis/item/BugreportItem.java
new file mode 100644
index 0000000..11a41fe
--- /dev/null
+++ b/src/com/android/loganalysis/item/BugreportItem.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store Bugreport info.
+ */
+public class BugreportItem extends GenericItem {
+    public static final String TYPE = "BUGREPORT";
+
+    private static final String TIME = "TIME";
+    private static final String MEM_INFO = "MEM_INFO";
+    private static final String PROCRANK = "PROCRANK";
+    private static final String SYSTEM_LOG = "SYSTEM_LOG";
+    private static final String SYSTEM_PROPS = "SYSTEM_PROPS";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            TIME, MEM_INFO, PROCRANK, SYSTEM_LOG, SYSTEM_PROPS));
+
+    /**
+     * The constructor for {@link BugreportItem}.
+     */
+    public BugreportItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the time of the bugreport.
+     */
+    public Date getTime() {
+        return (Date) getAttribute(TIME);
+    }
+
+    /**
+     * Set the time of the bugreport.
+     */
+    public void setTime(Date time) {
+        setAttribute(TIME, time);
+    }
+
+    /**
+     * Get the {@link MemInfoItem} of the bugreport.
+     */
+    public MemInfoItem getMemInfo() {
+        return (MemInfoItem) getAttribute(MEM_INFO);
+    }
+
+    /**
+     * Set the {@link MemInfoItem} of the bugreport.
+     */
+    public void setMemInfo(MemInfoItem memInfo) {
+        setAttribute(MEM_INFO, memInfo);
+    }
+
+    /**
+     * Get the {@link ProcrankItem} of the bugreport.
+     */
+    public ProcrankItem getProcrank() {
+        return (ProcrankItem) getAttribute(PROCRANK);
+    }
+
+    /**
+     * Set the {@link ProcrankItem} of the bugreport.
+     */
+    public void setProcrank(ProcrankItem procrank) {
+        setAttribute(PROCRANK, procrank);
+    }
+
+    /**
+     * Get the {@link LogcatItem} of the bugreport.
+     */
+    public LogcatItem getSystemLog() {
+        return (LogcatItem) getAttribute(SYSTEM_LOG);
+    }
+
+    /**
+     * Set the {@link LogcatItem} of the bugreport.
+     */
+    public void setSystemLog(LogcatItem systemLog) {
+        setAttribute(SYSTEM_LOG, systemLog);
+    }
+
+    /**
+     * Get the {@link SystemPropsItem} of the bugreport.
+     */
+    public SystemPropsItem getSystemProps() {
+        return (SystemPropsItem) getAttribute(SYSTEM_PROPS);
+    }
+
+    /**
+     * Set the {@link SystemPropsItem} of the bugreport.
+     */
+    public void setSystemProps(SystemPropsItem systemProps) {
+        setAttribute(SYSTEM_PROPS, systemProps);
+    }
+}
diff --git a/src/com/android/loganalysis/item/ConflictingItemException.java b/src/com/android/loganalysis/item/ConflictingItemException.java
new file mode 100644
index 0000000..75d1e33
--- /dev/null
+++ b/src/com/android/loganalysis/item/ConflictingItemException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+/**
+ * Thrown if there is conflicting information when trying to combine two items.
+ */
+public class ConflictingItemException extends Exception {
+
+    private static final long serialVersionUID = 3303627598068792143L;
+
+    /**
+     * Creates a {@link ConflictingItemException}.
+     *
+     * @param message The reason for the conflict.
+     */
+    ConflictingItemException(String message) {
+        super(message);
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericItem.java b/src/com/android/loganalysis/item/GenericItem.java
new file mode 100644
index 0000000..7da3662
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericItem.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of the {@link IItem} interface which implements helper methods.
+ */
+public class GenericItem implements IItem {
+    private Map<String, Object> mAttributes = new HashMap<String, Object>();
+    private Set<String> mAllowedAttributes;
+    private String mType = null;
+
+    protected GenericItem(String type, Set<String> allowedAttributes) {
+        mAllowedAttributes = new HashSet<String>();
+        mAllowedAttributes.addAll(allowedAttributes);
+        mType = type;
+    }
+
+    protected GenericItem(String type, Set<String> allowedAttributes,
+            Map<String, Object> attributes) {
+        this(type, allowedAttributes);
+
+        for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+            setAttribute(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return mType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) throws ConflictingItemException {
+        if (this == other) {
+            return this;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            throw new ConflictingItemException("Conflicting class types");
+        }
+
+        return new GenericItem(getType(), mAllowedAttributes, mergeAttributes(other));
+    }
+
+    /**
+     * Merges the attributes from the item and another and returns a Map of the merged attributes.
+     * <p>
+     * Goes through each field in the item preferring non-null attributes over null attributes.
+     * </p>
+     *
+     * @param other The other item
+     * @return A Map from Strings to Objects containing merged attributes.
+     * @throws ConflictingItemException If the two items are not consistent.
+     */
+    protected Map<String, Object> mergeAttributes(IItem other) throws ConflictingItemException {
+        if (this == other) {
+            return mAttributes;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            throw new ConflictingItemException("Conflicting class types");
+        }
+
+        GenericItem item = (GenericItem) other;
+        Map<String, Object> mergedAttributes = new HashMap<String, Object>();
+        for (String attribute : mAllowedAttributes) {
+            mergedAttributes.put(attribute,
+                    mergeObjects(getAttribute(attribute), item.getAttribute(attribute)));
+        }
+        return mergedAttributes;
+    }
+
+    /**
+     * {@inhertiDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        GenericItem item = (GenericItem) other;
+        for (String attribute : mAllowedAttributes) {
+            if (!areConsistent(getAttribute(attribute), item.getAttribute(attribute))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        GenericItem item = (GenericItem) other;
+        for (String attribute : mAllowedAttributes) {
+            if (!areEqual(getAttribute(attribute), item.getAttribute(attribute))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Set an attribute to a value.
+     *
+     * @param attribute The name of the attribute.
+     * @param value The value.
+     * @throws IllegalArgumentException If the attribute is not in allowedAttributes.
+     */
+    protected void setAttribute(String attribute, Object value) throws IllegalArgumentException {
+        if (!mAllowedAttributes.contains(attribute)) {
+            throw new IllegalArgumentException();
+        }
+        mAttributes.put(attribute, value);
+    }
+
+    /**
+     * Get the value of an attribute.
+     *
+     * @param attribute The name of the attribute.
+     * @return The value or null if the attribute has not been set.
+     * @throws IllegalArgumentException If the attribute is not in allowedAttributes.
+     */
+    protected Object getAttribute(String attribute) throws IllegalArgumentException {
+        if (!mAllowedAttributes.contains(attribute)) {
+            throw new IllegalArgumentException();
+        }
+        return mAttributes.get(attribute);
+    }
+
+    /**
+     * Helper method to return if two objects are equal.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return True if object1 and object2 are both null or if object1 is equal to object2, false
+     * otherwise.
+     */
+    static protected boolean areEqual(Object object1, Object object2) {
+        return object1 == null ? object2 == null : object1.equals(object2);
+    }
+
+    /**
+     * Helper method to return if two objects are consistent.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return True if either object1 or object2 is null or if object1 is equal to object2, false if
+     * both objects are not null and not equal.
+     */
+    static protected boolean areConsistent(Object object1, Object object2) {
+        return object1 == null || object2 == null ? true : object1.equals(object2);
+    }
+
+    /**
+     * Helper method used for merging two objects.
+     *
+     * @param object1 The first object
+     * @param object2 The second object
+     * @return If both objects are null, then null, else the non-null item if both items are equal.
+     * @throws ConflictingItemException If both objects are not null and they are not equal.
+     */
+    static protected Object mergeObjects(Object object1, Object object2)
+            throws ConflictingItemException {
+        if (!areConsistent(object1, object2)) {
+            throw new ConflictingItemException(String.format("%s conflicts with %s", object1,
+                    object2));
+        }
+        return object1 == null ? object2 : object1;
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericLogcatItem.java b/src/com/android/loganalysis/item/GenericLogcatItem.java
new file mode 100644
index 0000000..4a3642d
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericLogcatItem.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A generic item containing attributes for time, process, and thread and can be extended for
+ * items such as {@link AnrItem} and {@link JavaCrashItem}.
+ */
+public abstract class GenericLogcatItem extends GenericItem {
+    private static final String EVENT_TIME = "EVENT_TIME";
+    private static final String PID = "PID";
+    private static final String TID = "TID";
+    private static final String APP = "APP";
+    private static final String LAST_PREAMBLE = "LAST_PREAMBLE";
+    private static final String PROC_PREAMBLE = "PROC_PREAMBLE";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            EVENT_TIME, PID, TID, APP, LAST_PREAMBLE, PROC_PREAMBLE));
+
+    /**
+     * Constructor for {@link GenericLogcatItem}.
+     *
+     * @param type The type of the item.
+     * @param attributes A list of allowed attributes.
+     */
+    protected GenericLogcatItem(String type, Set<String> attributes) {
+        super(type, getAllAttributes(attributes));
+    }
+
+    /**
+     * Get the {@link Date} object when the event happened.
+     */
+    public Date getEventTime() {
+        return (Date) getAttribute(EVENT_TIME);
+    }
+
+    /**
+     * Set the {@link Date} object when the event happened.
+     */
+    public void setEventTime(Date time) {
+        setAttribute(EVENT_TIME, time);
+    }
+
+    /**
+     * Get the PID of the event.
+     */
+    public Integer getPid() {
+        return (Integer) getAttribute(PID);
+    }
+
+    /**
+     * Set the PID of the event.
+     */
+    public void setPid(Integer pid) {
+        setAttribute(PID, pid);
+    }
+
+    /**
+     * Get the TID of the event.
+     */
+    public Integer getTid() {
+        return (Integer) getAttribute(TID);
+    }
+
+    /**
+     * Set the TID of the event.
+     */
+    public void setTid(Integer tid) {
+        setAttribute(TID, tid);
+    }
+
+    /**
+     * Get the app or package name of the event.
+     */
+    public String getApp() {
+        return (String) getAttribute(APP);
+    }
+
+    /**
+     * Set the app or package name of the event.
+     */
+    public void setApp(String app) {
+        setAttribute(APP, app);
+    }
+
+    /**
+     * Get the last preamble for of the event.
+     */
+    public String getLastPreamble() {
+        return (String) getAttribute(LAST_PREAMBLE);
+    }
+
+    /**
+     * Set the last preamble for of the event.
+     */
+    public void setLastPreamble(String preamble) {
+        setAttribute(LAST_PREAMBLE, preamble);
+    }
+
+    /**
+     * Get the process preamble for of the event.
+     */
+    public String getProcessPreamble() {
+        return (String) getAttribute(PROC_PREAMBLE);
+    }
+
+    /**
+     * Set the process preamble for of the event.
+     */
+    public void setProcessPreamble(String preamble) {
+        setAttribute(PROC_PREAMBLE, preamble);
+    }
+
+    /**
+     * Combine an array of attributes with the internal list of attributes.
+     */
+    private static Set<String> getAllAttributes(Set<String> attributes) {
+        Set<String> allAttributes = new HashSet<String>(ATTRIBUTES);
+        allAttributes.addAll(attributes);
+        return allAttributes;
+    }
+}
diff --git a/src/com/android/loganalysis/item/GenericMapItem.java b/src/com/android/loganalysis/item/GenericMapItem.java
new file mode 100644
index 0000000..e197db2
--- /dev/null
+++ b/src/com/android/loganalysis/item/GenericMapItem.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.HashMap;
+
+/**
+ * An IItem that just represents a simple key/value map
+ */
+@SuppressWarnings("serial")
+public class GenericMapItem<K, V> extends HashMap<K,V> implements IItem {
+    private String mType = null;
+
+    /**
+     * No-op zero-arg constructor
+     */
+    public GenericMapItem() {}
+
+    /**
+     * Convenience constructor that sets the type
+     */
+    public GenericMapItem(String type) {
+        setType(type);
+    }
+
+    /**
+     * Set the self-reported type that this {@link GenericMapItem} represents.
+     */
+    public void setType(String type) {
+        mType = type;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return mType;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) {
+        // FIXME
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        // FIXME
+        return true;
+    }
+}
diff --git a/src/com/android/loganalysis/item/IItem.java b/src/com/android/loganalysis/item/IItem.java
new file mode 100644
index 0000000..d52ec5d
--- /dev/null
+++ b/src/com/android/loganalysis/item/IItem.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+/**
+ * Interface for all items that are created by any parser.
+ */
+public interface IItem {
+
+    /**
+     * Determine what type this IItem represents.  May return {@code null}
+     */
+    public String getType();
+
+    /**
+     * Merges the item and another into an item with the most complete information.
+     *
+     * <p>
+     * Goes through each field in the item preferring non-null fields over null fields.
+     * </p>
+     *
+     * @param other The other item
+     * @return The product of both items combined.
+     * @throws ConflictingItemException If the two items are not consistent.
+     */
+    public IItem merge(IItem other) throws ConflictingItemException;
+
+    /**
+     * Checks that the item and another are consistent.
+     *
+     * <p>
+     * Consistency means that no individual fields in either item conflict with the other.
+     * However, one item might contain more complete information.  Two items of different types
+     * are never consistent.
+     * </p>
+     *
+     * @param other The other item.
+     * @return True if the objects are the same type and all the fields are either equal or one of
+     * the fields is null.
+     */
+    public boolean isConsistent(IItem other);
+}
diff --git a/src/com/android/loganalysis/item/JavaCrashItem.java b/src/com/android/loganalysis/item/JavaCrashItem.java
new file mode 100644
index 0000000..042cf61
--- /dev/null
+++ b/src/com/android/loganalysis/item/JavaCrashItem.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store Java crash info.
+ */
+public class JavaCrashItem extends GenericLogcatItem {
+    public static final String TYPE = "JAVA CRASH";
+
+    private static final String EXCEPTION = "EXCEPTION";
+    private static final String MESSAGE = "MESSAGE";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            EXCEPTION, MESSAGE, STACK));
+
+    /**
+     * The constructor for {@link JavaCrashItem}.
+     */
+    public JavaCrashItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the exception for the Java crash.
+     */
+    public String getException() {
+        return (String) getAttribute(EXCEPTION);
+    }
+
+    /**
+     * Get the exception for the Java crash.
+     */
+    public void setException(String exception) {
+        setAttribute(EXCEPTION, exception);
+    }
+
+    /**
+     * Get the message for the Java crash.
+     */
+    public String getMessage() {
+        return (String) getAttribute(MESSAGE);
+    }
+
+    /**
+     * Set the message for the Java crash.
+     */
+    public void setMessage(String message) {
+        setAttribute(MESSAGE, message);
+    }
+
+    /**
+     * Get the stack for the ANR.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the ANR.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/item/LogcatItem.java b/src/com/android/loganalysis/item/LogcatItem.java
new file mode 100644
index 0000000..7bf42a2
--- /dev/null
+++ b/src/com/android/loganalysis/item/LogcatItem.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store logcat info.
+ */
+public class LogcatItem extends GenericItem {
+    public static final String TYPE = "LOGCAT";
+
+    private static final String START_TIME = "START_TIME";
+    private static final String STOP_TIME = "STOP_TIME";
+    private static final String EVENTS = "EVENTS";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            START_TIME, STOP_TIME, EVENTS));
+
+    private class ItemList extends LinkedList<IItem> {
+        private static final long serialVersionUID = 1088529764741812025L;
+    }
+
+    /**
+     * The constructor for {@link LogcatItem}.
+     */
+    public LogcatItem() {
+        super(TYPE, ATTRIBUTES);
+
+        setAttribute(EVENTS, new ItemList());
+    }
+
+    /**
+     * Get the start time of the logcat.
+     */
+    public Date getStartTime() {
+        return (Date) getAttribute(START_TIME);
+    }
+
+    /**
+     * Set the start time of the logcat.
+     */
+    public void setStartTime(Date time) {
+        setAttribute(START_TIME, time);
+    }
+
+    /**
+     * Get the stop time of the logcat.
+     */
+    public Date getStopTime() {
+        return (Date) getAttribute(STOP_TIME);
+    }
+
+    /**
+     * Set the stop time of the logcat.
+     */
+    public void setStopTime(Date time) {
+        setAttribute(STOP_TIME, time);
+    }
+
+    /**
+     * Get the list of all {@link IItem} events.
+     */
+    public List<IItem> getEvents() {
+        return (ItemList) getAttribute(EVENTS);
+    }
+
+    /**
+     * Add an {@link IItem} event to the end of the list of events.
+     */
+    public void addEvent(IItem event) {
+        ((ItemList) getAttribute(EVENTS)).add(event);
+    }
+
+    /**
+     * Get the list of all {@link AnrItem} events.
+     */
+    public List<AnrItem> getAnrs() {
+        List<AnrItem> anrs = new LinkedList<AnrItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof AnrItem) {
+                anrs.add((AnrItem) item);
+            }
+        }
+        return anrs;
+    }
+
+    /**
+     * Get the list of all {@link JavaCrashItem} events.
+     */
+    public List<JavaCrashItem> getJavaCrashes() {
+        List<JavaCrashItem> jcs = new LinkedList<JavaCrashItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof JavaCrashItem) {
+                jcs.add((JavaCrashItem) item);
+            }
+        }
+        return jcs;
+    }
+
+    /**
+     * Get the list of all {@link NativeCrashItem} events.
+     */
+    public List<NativeCrashItem> getNativeCrashes() {
+        List<NativeCrashItem> ncs = new LinkedList<NativeCrashItem>();
+        for (IItem item : getEvents()) {
+            if (item instanceof NativeCrashItem) {
+                ncs.add((NativeCrashItem) item);
+            }
+        }
+        return ncs;
+    }
+}
diff --git a/src/com/android/loganalysis/item/MemInfoItem.java b/src/com/android/loganalysis/item/MemInfoItem.java
new file mode 100644
index 0000000..3246cba
--- /dev/null
+++ b/src/com/android/loganalysis/item/MemInfoItem.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+/**
+ * An {@link IItem} used to store the memory info output.
+ */
+public class MemInfoItem extends GenericMapItem<String, Integer> {
+    private static final long serialVersionUID = 2648395553885243585L;
+
+    public static final String TYPE = "MEMORY INFO";
+
+    public MemInfoItem() {
+        super(TYPE);
+    }
+}
diff --git a/src/com/android/loganalysis/item/MonkeyLogItem.java b/src/com/android/loganalysis/item/MonkeyLogItem.java
new file mode 100644
index 0000000..4f89917
--- /dev/null
+++ b/src/com/android/loganalysis/item/MonkeyLogItem.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store monkey log info.
+ */
+public class MonkeyLogItem extends GenericItem {
+
+    private class StringSet extends HashSet<String> {
+        private static final long serialVersionUID = -2206822563602989856L;
+    }
+
+    public enum DroppedCategory {
+        KEYS,
+        POINTERS,
+        TRACKBALLS,
+        FLIPS,
+        ROTATIONS
+    }
+
+    private static final String TYPE = "MONKEY_LOG";
+
+    private static final String START_TIME = "START_TIME";
+    private static final String STOP_TIME = "STOP_TIME";
+    private static final String PACKAGES = "PACKAGES";
+    private static final String CATEGORIES = "CATEGORIES";
+    private static final String THROTTLE = "THROTTLE";
+    private static final String SEED = "SEED";
+    private static final String TARGET_COUNT = "TARGET_COUNT";
+    private static final String IGNORE_SECURITY_EXCEPTIONS = "IGNORE_SECURITY_EXCEPTIONS";
+    private static final String TOTAL_DURATION = "TOTAL_TIME";
+    private static final String START_UPTIME_DURATION = "START_UPTIME";
+    private static final String STOP_UPTIME_DURATION = "STOP_UPTIME";
+    private static final String IS_FINISHED = "IS_FINISHED";
+    private static final String NO_ACTIVITIES = "NO_ACTIVITIES";
+    private static final String INTERMEDIATE_COUNT = "INTERMEDIATE_COUNT";
+    private static final String FINAL_COUNT = "FINAL_COUNT";
+    private static final String CRASH = "CRASH";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            START_TIME, STOP_TIME, PACKAGES, CATEGORIES, THROTTLE, SEED, TARGET_COUNT,
+            IGNORE_SECURITY_EXCEPTIONS, TOTAL_DURATION, START_UPTIME_DURATION, STOP_UPTIME_DURATION,
+            IS_FINISHED, NO_ACTIVITIES, INTERMEDIATE_COUNT, FINAL_COUNT, CRASH,
+            DroppedCategory.KEYS.toString(),
+            DroppedCategory.POINTERS.toString(),
+            DroppedCategory.TRACKBALLS.toString(),
+            DroppedCategory.FLIPS.toString(),
+            DroppedCategory.ROTATIONS.toString()));
+
+    /**
+     * The constructor for {@link MonkeyLogItem}.
+     */
+    public MonkeyLogItem() {
+        super(TYPE, ATTRIBUTES);
+
+        setAttribute(PACKAGES, new StringSet());
+        setAttribute(CATEGORIES, new StringSet());
+        setAttribute(THROTTLE, 0);
+        setAttribute(IGNORE_SECURITY_EXCEPTIONS, false);
+        setAttribute(IS_FINISHED, false);
+        setAttribute(NO_ACTIVITIES, false);
+        setAttribute(INTERMEDIATE_COUNT, 0);
+    }
+
+    /**
+     * Get the start time of the monkey log.
+     */
+    public Date getStartTime() {
+        return (Date) getAttribute(START_TIME);
+    }
+
+    /**
+     * Set the start time of the monkey log.
+     */
+    public void setStartTime(Date time) {
+        setAttribute(START_TIME, time);
+    }
+
+    /**
+     * Get the stop time of the monkey log.
+     */
+    public Date getStopTime() {
+        return (Date) getAttribute(STOP_TIME);
+    }
+
+    /**
+     * Set the stop time of the monkey log.
+     */
+    public void setStopTime(Date time) {
+        setAttribute(STOP_TIME, time);
+    }
+
+    /**
+     * Get the set of packages that the monkey is run on.
+     */
+    public Set<String> getPackages() {
+        return (StringSet) getAttribute(PACKAGES);
+    }
+
+    /**
+     * Add a package to the set that the monkey is run on.
+     */
+    public void addPackage(String thePackage) {
+        ((StringSet) getAttribute(PACKAGES)).add(thePackage);
+    }
+
+    /**
+     * Get the set of categories that the monkey is run on.
+     */
+    public Set<String> getCategories() {
+        return (StringSet) getAttribute(CATEGORIES);
+    }
+
+    /**
+     * Add a category to the set that the monkey is run on.
+     */
+    public void addCategory(String category) {
+        ((StringSet) getAttribute(CATEGORIES)).add(category);
+    }
+
+    /**
+     * Get the throttle for the monkey run.
+     */
+    public int getThrottle() {
+        return (Integer) getAttribute(THROTTLE);
+    }
+
+    /**
+     * Set the throttle for the monkey run.
+     */
+    public void setThrottle(int throttle) {
+        setAttribute(THROTTLE, throttle);
+    }
+
+    /**
+     * Get the seed for the monkey run.
+     */
+    public Integer getSeed() {
+        return (Integer) getAttribute(SEED);
+    }
+
+    /**
+     * Set the seed for the monkey run.
+     */
+    public void setSeed(int seed) {
+        setAttribute(SEED, seed);
+    }
+
+    /**
+     * Get the target count for the monkey run.
+     */
+    public Integer getTargetCount() {
+        return (Integer) getAttribute(TARGET_COUNT);
+    }
+
+    /**
+     * Set the target count for the monkey run.
+     */
+    public void setTargetCount(int count) {
+        setAttribute(TARGET_COUNT, count);
+    }
+
+    /**
+     * Get if the ignore security exceptions flag is set for the monkey run.
+     */
+    public boolean getIgnoreSecurityExceptions() {
+        return (Boolean) getAttribute(IGNORE_SECURITY_EXCEPTIONS);
+    }
+
+    /**
+     * Set if the ignore security exceptions flag is set for the monkey run.
+     */
+    public void setIgnoreSecurityExceptions(boolean ignore) {
+        setAttribute(IGNORE_SECURITY_EXCEPTIONS, ignore);
+    }
+
+    /**
+     * Get the total duration of the monkey run in milliseconds.
+     */
+    public Long getTotalDuration() {
+        return (Long) getAttribute(TOTAL_DURATION);
+    }
+
+    /**
+     * Set the total duration of the monkey run in milliseconds.
+     */
+    public void setTotalDuration(long time) {
+        setAttribute(TOTAL_DURATION, time);
+    }
+
+    /**
+     * Get the start uptime duration of the monkey run in milliseconds.
+     */
+    public Long getStartUptimeDuration() {
+        return (Long) getAttribute(START_UPTIME_DURATION);
+    }
+
+    /**
+     * Set the start uptime duration of the monkey run in milliseconds.
+     */
+    public void setStartUptimeDuration(long uptime) {
+        setAttribute(START_UPTIME_DURATION, uptime);
+    }
+
+    /**
+     * Get the stop uptime duration of the monkey run in milliseconds.
+     */
+    public Long getStopUptimeDuration() {
+        return (Long) getAttribute(STOP_UPTIME_DURATION);
+    }
+
+    /**
+     * Set the stop uptime duration of the monkey run in milliseconds.
+     */
+    public void setStopUptimeDuration(long uptime) {
+        setAttribute(STOP_UPTIME_DURATION, uptime);
+    }
+
+    /**
+     * Get if the monkey run finished without crashing.
+     */
+    public boolean getIsFinished() {
+        return (Boolean) getAttribute(IS_FINISHED);
+    }
+
+    /**
+     * Set if the monkey run finished without crashing.
+     */
+    public void setIsFinished(boolean finished) {
+        setAttribute(IS_FINISHED, finished);
+    }
+
+    /**
+     * Get if the monkey run aborted due to no activies to run.
+     */
+    public boolean getNoActivities() {
+        return (Boolean) getAttribute(NO_ACTIVITIES);
+    }
+
+    /**
+     * Set if the monkey run aborted due to no activies to run.
+     */
+    public void setNoActivities(boolean noActivities) {
+        setAttribute(NO_ACTIVITIES, noActivities);
+    }
+
+
+    /**
+     * Get the intermediate count for the monkey run.
+     * <p>
+     * This count starts at 0 and increments every 100 events. This number should be within 100 of
+     * the final count.
+     * </p>
+     */
+    public int getIntermediateCount() {
+        return (Integer) getAttribute(INTERMEDIATE_COUNT);
+    }
+
+    /**
+     * Set the intermediate count for the monkey run.
+     * <p>
+     * This count starts at 0 and increments every 100 events. This number should be within 100 of
+     * the final count.
+     * </p>
+     */
+    public void setIntermediateCount(int count) {
+        setAttribute(INTERMEDIATE_COUNT, count);
+    }
+
+    /**
+     * Get the final count for the monkey run.
+     */
+    public Integer getFinalCount() {
+        return (Integer) getAttribute(FINAL_COUNT);
+    }
+
+    /**
+     * Set the final count for the monkey run.
+     */
+    public void setFinalCount(int count) {
+        setAttribute(FINAL_COUNT, count);
+    }
+
+    /**
+     * Get the dropped events count for a {@link DroppedCategory} for the monkey run.
+     */
+    public Integer getDroppedCount(DroppedCategory category) {
+        return (Integer) getAttribute(category.toString());
+    }
+
+    /**
+     * Set the dropped events count for a {@link DroppedCategory} for the monkey run.
+     */
+    public void setDroppedCount(DroppedCategory category, int count) {
+        setAttribute(category.toString(), count);
+    }
+
+    /**
+     * Get the {@link AnrItem} or {@link JavaCrashItem} for the monkey run or null if there was no
+     * crash.
+     */
+    public GenericLogcatItem getCrash() {
+        return (GenericLogcatItem) getAttribute(CRASH);
+    }
+
+    /**
+     * Set the {@link AnrItem} or {@link JavaCrashItem} for the monkey run.
+     */
+    public void setCrash(GenericLogcatItem crash) {
+        setAttribute(CRASH, crash);
+    }
+}
diff --git a/src/com/android/loganalysis/item/NativeCrashItem.java b/src/com/android/loganalysis/item/NativeCrashItem.java
new file mode 100644
index 0000000..e06e0a4
--- /dev/null
+++ b/src/com/android/loganalysis/item/NativeCrashItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link IItem} used to store native crash info.
+ */
+public class NativeCrashItem extends GenericLogcatItem {
+    public static final String TYPE = "NATIVE CRASH";
+
+    private static final String FINGERPRINT = "FINGERPRINT";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            FINGERPRINT, STACK));
+
+    /**
+     * The constructor for {@link NativeCrashItem}.
+     */
+    public NativeCrashItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the fingerprint for the crash.
+     */
+    public String getFingerprint() {
+        return (String) getAttribute(FINGERPRINT);
+    }
+
+    /**
+     * Set the fingerprint for the crash.
+     */
+    public void setFingerprint(String fingerprint) {
+        setAttribute(FINGERPRINT, fingerprint);
+    }
+
+    /**
+     * Get the stack for the crash.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the crash.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/item/ProcrankItem.java b/src/com/android/loganalysis/item/ProcrankItem.java
new file mode 100644
index 0000000..a2d7e74
--- /dev/null
+++ b/src/com/android/loganalysis/item/ProcrankItem.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * An {@link IItem} used to procrank info.
+ */
+public class ProcrankItem implements IItem {
+    public static final String TYPE = "PROCRANK";
+
+    private class ProcrankValue {
+        public String mProcessName = null;
+        public int mVss;
+        public int mRss;
+        public int mPss;
+        public int mUss;
+
+        public ProcrankValue(String processName, int vss, int rss, int pss, int uss) {
+            mProcessName = processName;
+            mVss = vss;
+            mRss = rss;
+            mPss = pss;
+            mUss = uss;
+        }
+    }
+
+    private Map<Integer, ProcrankValue> mProcrankLines = new HashMap<Integer, ProcrankValue>();
+
+    /**
+     * Add a line from the procrank output to the {@link ProcrankItem}.
+     *
+     * @param pid The PID from the output
+     * @param processName The process name from the cmdline column
+     * @param vss The VSS in KB
+     * @param rss The RSS in KB
+     * @param pss The PSS in KB
+     * @param uss The USS in KB
+     */
+    public void addProcrankLine(int pid, String processName, int vss, int rss, int pss, int uss) {
+        mProcrankLines.put(pid, new ProcrankValue(processName, vss, rss, pss, uss));
+    }
+
+    /**
+     * Get a set of PIDs seen in the procrank output.
+     */
+    public Set<Integer> getPids() {
+        return mProcrankLines.keySet();
+    }
+
+    /**
+     * Get the process name for a given PID.
+     */
+    public String getProcessName(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mProcessName;
+    }
+
+    /**
+     * Get the VSS for a given PID.
+     */
+    public Integer getVss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mVss;
+    }
+
+    /**
+     * Get the RSS for a given PID.
+     */
+    public Integer getRss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mRss;
+    }
+
+    /**
+     * Get the PSS for a given PID.
+     */
+    public Integer getPss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mPss;
+    }
+
+    /**
+     * Get the USS for a given PID.
+     */
+    public Integer getUss(int pid) {
+        if (!mProcrankLines.containsKey(pid)) {
+            return null;
+        }
+
+        return mProcrankLines.get(pid).mUss;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem merge(IItem other) throws ConflictingItemException {
+        throw new ConflictingItemException("Procrank items cannot be merged");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isConsistent(IItem other) {
+        return false;
+    }
+}
diff --git a/src/com/android/loganalysis/item/SystemPropsItem.java b/src/com/android/loganalysis/item/SystemPropsItem.java
new file mode 100644
index 0000000..67e8de8
--- /dev/null
+++ b/src/com/android/loganalysis/item/SystemPropsItem.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+/**
+ * An {@link IItem} used to store the system props info.
+ */
+public class SystemPropsItem extends GenericMapItem<String, String> {
+
+    private static final long serialVersionUID = 7280770512647682477L;
+
+    public static final String TYPE = "SYSTEM PROPERTIES";
+
+    public SystemPropsItem() {
+        super(TYPE);
+    }
+}
diff --git a/src/com/android/loganalysis/item/TracesItem.java b/src/com/android/loganalysis/item/TracesItem.java
new file mode 100644
index 0000000..1dfff76
--- /dev/null
+++ b/src/com/android/loganalysis/item/TracesItem.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.loganalysis.item;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * An {@link IItem} used to store traces info.
+ * <p>
+ * For now, this only stores info about the main stack trace from the first process. It is used to
+ * get a stack from {@code /data/anr/traces.txt} which can be used to give some context about the
+ * ANR. If there is a need, this item can be expanded to store all stacks from all processes.
+ * </p>
+ */
+public class TracesItem extends GenericItem {
+    public static final String TYPE = "TRACES_ITEM";
+
+    private static final String PID = "PID";
+    private static final String APP = "APP";
+    private static final String STACK = "STACK";
+
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            PID, APP, STACK));
+
+    /**
+     * The constructor for {@link TracesItem}.
+     */
+    public TracesItem() {
+        super(TYPE, ATTRIBUTES);
+    }
+
+    /**
+     * Get the PID of the event.
+     */
+    public Integer getPid() {
+        return (Integer) getAttribute(PID);
+    }
+
+    /**
+     * Set the PID of the event.
+     */
+    public void setPid(Integer pid) {
+        setAttribute(PID, pid);
+    }
+
+    /**
+     * Get the app or package name of the event.
+     */
+    public String getApp() {
+        return (String) getAttribute(APP);
+    }
+
+    /**
+     * Set the app or package name of the event.
+     */
+    public void setApp(String app) {
+        setAttribute(APP, app);
+    }
+
+    /**
+     * Get the stack for the crash.
+     */
+    public String getStack() {
+        return (String) getAttribute(STACK);
+    }
+
+    /**
+     * Set the stack for the crash.
+     */
+    public void setStack(String stack) {
+        setAttribute(STACK, stack);
+    }
+}
diff --git a/src/com/android/loganalysis/parser/AbstractSectionParser.java b/src/com/android/loganalysis/parser/AbstractSectionParser.java
new file mode 100644
index 0000000..c57546d
--- /dev/null
+++ b/src/com/android/loganalysis/parser/AbstractSectionParser.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+import com.android.loganalysis.util.RegexTrie;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link IParser} that splits an input file into discrete sections and passes each section to an
+ * {@link IParser} to parse.
+ * <p>
+ * Before parsing input, {@link IParser}s can be added with
+ * {@link #addSectionParser(IParser, String)}. The default parser is {@link NoopParser} but this can
+ * be overwritten by calling {@link #setParser(IParser)} before parsing the input.
+ * </p>
+ */
+public abstract class AbstractSectionParser implements IParser {
+    private RegexTrie<IParser> mSectionTrie = new RegexTrie<IParser>();
+    private IParser mCurrentParser = new NoopParser();
+    private List<String> mParseBlock = new LinkedList<String>();
+    private Map<String, IItem> mSections = new HashMap<String, IItem>();
+
+    /**
+     * A method to add a given section parser to the set of potential parsers to use.
+     *
+     * @param parser The {@link IParser} to add
+     * @param pattern The regular expression to trigger this parser
+    */
+    protected void addSectionParser(IParser parser, String pattern) {
+        if (parser == null) {
+            throw new NullPointerException("Parser is null");
+        }
+        if (pattern == null) {
+            throw new NullPointerException("Pattern is null");
+        }
+        mSectionTrie.put(parser, pattern);
+    }
+
+    /**
+     * Parse a line of input, either adding the input to the current block or switching parsers and
+     * running the current parser.
+     *
+     * @param line The line to parse
+     */
+    protected void parseLine(String line) {
+        IParser nextParser = mSectionTrie.retrieve(line);
+
+        if (nextParser == null) {
+            // no match, so buffer this for the current parser, if there is one
+            if (mCurrentParser != null) {
+                mParseBlock.add(line);
+            } else {
+                // CLog.w("Line outside of parsed section: %s", line);
+            }
+        } else {
+            runCurrentParser();
+            mParseBlock.clear();
+            mCurrentParser = nextParser;
+
+            onSwitchParser();
+        }
+    }
+
+    /**
+     * Signal that the input has finished and run the last parser.
+     */
+    protected void commit() {
+        runCurrentParser();
+    }
+
+    /**
+     * Gets the {@link IItem} for a given section.
+     *
+     * @param section The {@link IItem} type for the section.
+     * @return The {@link IItem}.
+     */
+    protected IItem getSection(String section) {
+        return mSections.get(section);
+    }
+
+    /**
+     * Set the {@link IParser}. Used to set the initial parser.
+     *
+     * @param parser The {@link IParser} to set.
+     */
+    protected void setParser(IParser parser) {
+        mCurrentParser = parser;
+    }
+
+    protected void onSwitchParser() {
+    }
+
+    /**
+     * Run the current parser and add the {@link IItem} to the sections map.
+     */
+    private void runCurrentParser() {
+        if (mCurrentParser != null) {
+            IItem item = mCurrentParser.parse(mParseBlock);
+            if (item != null && !(mCurrentParser instanceof NoopParser)) {
+                mSections.put(item.getType(), item);
+                // CLog.v("Just ran the %s parser", mCurrentParser.getClass().getSimpleName());
+            }
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/AnrParser.java b/src/com/android/loganalysis/parser/AnrParser.java
new file mode 100644
index 0000000..5102436
--- /dev/null
+++ b/src/com/android/loganalysis/parser/AnrParser.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle ANRs.
+ */
+public class AnrParser implements IParser {
+    /**
+     * Matches: ANR (application not responding) in process: app
+     * Matches: ANR in app
+     * Matches: ANR in app (class/package)
+     */
+    public static final Pattern START = Pattern.compile(
+            "^ANR (?:\\(application not responding\\) )?in (?:process: )?(\\S+).*$");
+    /**
+     * Matches: Reason: reason
+     */
+    private static final Pattern REASON = Pattern.compile("^Reason: (.*)$");
+    /**
+     * Matches: Load: 0.71 / 0.83 / 0.51
+     */
+    private static final Pattern LOAD = Pattern.compile(
+            "^Load: (\\d+\\.\\d+) / (\\d+\\.\\d+) / (\\d+\\.\\d+)$");
+
+    /**
+     * Matches: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait
+     */
+    private static final Pattern TOTAL = Pattern.compile("^(\\d+(\\.\\d+)?)% TOTAL: .*$");
+    private static final Pattern USER = Pattern.compile("^.* (\\d+(\\.\\d+)?)% user.*$");
+    private static final Pattern KERNEL = Pattern.compile("^.* (\\d+(\\.\\d+)?)% kernel.*$");
+    private static final Pattern IOWAIT = Pattern.compile("^.* (\\d+(\\.\\d+)?)% iowait.*$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link AnrItem}.
+     */
+    @Override
+    public AnrItem parse(List<String> lines) {
+        AnrItem anr = null;
+        StringBuilder stack = new StringBuilder();
+        boolean matchedTotal = false;
+
+        for (String line : lines) {
+            Matcher m = START.matcher(line);
+            // Ignore all input until the start pattern is matched.
+            if (m.matches()) {
+                anr = new AnrItem();
+                anr.setApp(m.group(1));
+            }
+
+            if (anr != null) {
+                m = REASON.matcher(line);
+                if (m.matches()) {
+                    anr.setReason(m.group(1));
+                }
+
+                m = LOAD.matcher(line);
+                if (m.matches()) {
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_1, Double.parseDouble(m.group(1)));
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_5, Double.parseDouble(m.group(2)));
+                    anr.setLoad(AnrItem.LoadCategory.LOAD_15, Double.parseDouble(m.group(3)));
+                }
+
+                m = TOTAL.matcher(line);
+                if (!matchedTotal && m.matches()) {
+                    matchedTotal = true;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.TOTAL, Double.parseDouble(m.group(1)));
+
+                    m = USER.matcher(line);
+                    Double usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.USER, usage);
+
+                    m = KERNEL.matcher(line);
+                    usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.KERNEL, usage);
+
+                    m = IOWAIT.matcher(line);
+                    usage = m.matches() ? Double.parseDouble(m.group(1)) : 0.0;
+                    anr.setCpuUsage(AnrItem.CpuUsageCategory.IOWAIT, usage);
+                }
+
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+
+        if (anr != null) {
+            anr.setStack(stack.toString().trim());
+        }
+        return anr;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/BugreportParser.java b/src/com/android/loganalysis/parser/BugreportParser.java
new file mode 100644
index 0000000..748f751
--- /dev/null
+++ b/src/com/android/loganalysis/parser/BugreportParser.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.BugreportItem;
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.IItem;
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.item.MemInfoItem;
+import com.android.loganalysis.item.ProcrankItem;
+import com.android.loganalysis.item.SystemPropsItem;
+import com.android.loganalysis.item.TracesItem;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse Android bugreports.
+ */
+public class BugreportParser extends AbstractSectionParser {
+    private static final String MEM_INFO_SECTION_REGEX = "------ MEMORY INFO .*";
+    private static final String PROCRANK_SECTION_REGEX = "------ PROCRANK .*";
+    private static final String SYSTEM_PROP_SECTION_REGEX = "------ SYSTEM PROPERTIES .*";
+    private static final String SYSTEM_LOG_SECTION_REGEX =
+            "------ (SYSTEM|MAIN|MAIN AND SYSTEM) LOG .*";
+    private static final String ANR_TRACES_SECTION_REGEX = "------ VM TRACES AT LAST ANR .*";
+    private static final String NOOP_SECTION_REGEX = "------ .*";
+
+    /**
+     * Matches: == dumpstate: 2012-04-26 12:13:14
+     */
+    private static final Pattern DATE = Pattern.compile(
+            "^== dumpstate: (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})$");
+
+    private LogcatParser mLogcatParser = new LogcatParser();
+    private BugreportItem mBugreport = null;
+
+    /**
+     * Parse a bugreport from a {@link BufferedReader} into an {@link BugreportItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link BugreportItem}.
+     * @see #parse(List)
+     */
+    public BugreportItem parse(BufferedReader input) throws IOException {
+        String line;
+
+        setup();
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+        commit();
+
+        return mBugreport;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link BugreportItem}.
+     */
+    @Override
+    public BugreportItem parse(List<String> lines) {
+        setup();
+        for (String line : lines) {
+            parseLine(line);
+        }
+        commit();
+
+        return mBugreport;
+    }
+
+    /**
+     * Sets up the parser by adding the section parsers and adding an initial {@link IParser} to
+     * parse the bugreport header.
+     */
+    protected void setup() {
+        // Set the initial parser explicitly since the header isn't part of a section.
+        setParser(new IParser() {
+            @Override
+            public BugreportItem parse(List<String> lines) {
+                BugreportItem bugreport = new BugreportItem();
+                for (String line : lines) {
+                    Matcher m = DATE.matcher(line);
+                    if (m.matches()) {
+                        bugreport.setTime(parseTime(m.group(1)));
+                    }
+                }
+                return bugreport;
+            }
+        });
+        addSectionParser(new MemInfoParser(), MEM_INFO_SECTION_REGEX);
+        addSectionParser(new ProcrankParser(), PROCRANK_SECTION_REGEX);
+        addSectionParser(new SystemPropsParser(), SYSTEM_PROP_SECTION_REGEX);
+        addSectionParser(new TracesParser(), ANR_TRACES_SECTION_REGEX);
+        addSectionParser(mLogcatParser, SYSTEM_LOG_SECTION_REGEX);
+        addSectionParser(new NoopParser(), NOOP_SECTION_REGEX);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void commit() {
+        // signal EOF
+        super.commit();
+
+        if (mBugreport != null) {
+            mBugreport.setMemInfo((MemInfoItem) getSection(MemInfoItem.TYPE));
+            mBugreport.setProcrank((ProcrankItem) getSection(ProcrankItem.TYPE));
+            mBugreport.setSystemLog((LogcatItem) getSection(LogcatItem.TYPE));
+            mBugreport.setSystemProps((SystemPropsItem) getSection(SystemPropsItem.TYPE));
+
+            if (mBugreport.getSystemLog() != null && mBugreport.getProcrank() != null) {
+                for (IItem item : mBugreport.getSystemLog().getEvents()) {
+                    if (item instanceof GenericLogcatItem &&
+                            ((GenericLogcatItem) item).getApp() == null) {
+                        GenericLogcatItem logcatItem = (GenericLogcatItem) item;
+                        logcatItem.setApp(mBugreport.getProcrank().getProcessName(
+                                logcatItem.getPid()));
+                    }
+                }
+            }
+
+            TracesItem traces = (TracesItem) getSection(TracesItem.TYPE);
+            if (traces != null && traces.getApp() != null && traces.getStack() != null &&
+                    mBugreport.getSystemLog() != null) {
+                addAnrTrace(mBugreport.getSystemLog().getAnrs(), traces.getApp(),
+                        traces.getStack());
+
+            }
+        }
+    }
+
+    /**
+     * Add the trace from {@link TracesItem} to the last seen {@link AnrItem} matching a given app.
+     */
+    private void addAnrTrace(List<AnrItem> anrs, String app, String trace) {
+        ListIterator<AnrItem> li = anrs.listIterator(anrs.size());
+
+        while (li.hasPrevious()) {
+            AnrItem anr = li.previous();
+            if (app.equals(anr.getApp())) {
+                anr.setTrace(trace);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Set the {@link BugreportItem} and the year of the {@link LogcatParser} from the bugreport
+     * header.
+     */
+    @Override
+    protected void onSwitchParser() {
+        if (mBugreport == null) {
+            mBugreport = (BugreportItem) getSection(BugreportItem.TYPE);
+            if (mBugreport.getTime() != null) {
+                mLogcatParser.setYear(new SimpleDateFormat("yyyy").format(mBugreport.getTime()));
+            }
+        }
+    }
+
+    /**
+     * Converts a {@link String} into a {@link Date}.
+     */
+    private static Date parseTime(String timeStr) {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        try {
+            return formatter.parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.e("Could not parse time string %s", timeStr);
+            return null;
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/IParser.java b/src/com/android/loganalysis/parser/IParser.java
new file mode 100644
index 0000000..43750d7
--- /dev/null
+++ b/src/com/android/loganalysis/parser/IParser.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import java.util.List;
+
+/**
+ * An interface which defines the behavior for a parser.  The parser will receive a block of data
+ * that it can consider complete.  It parses the input and returns a single {@link IItem} instance.
+ * Furthermore, the parser should be robust against invalid input -- the input format may drift over
+ * time.
+ */
+public interface IParser {
+
+    /**
+     * Parses a list of {@link String} objects and returns a {@link IItem}.
+     *
+     * @param lines A list of {@link String} objects.
+     * @return The parsed {@link IItem} object.
+     */
+    public IItem parse(List<String> lines);
+}
+
diff --git a/src/com/android/loganalysis/parser/JavaCrashParser.java b/src/com/android/loganalysis/parser/JavaCrashParser.java
new file mode 100644
index 0000000..f1c87a1
--- /dev/null
+++ b/src/com/android/loganalysis/parser/JavaCrashParser.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.JavaCrashItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle Java crashes.
+ */
+public class JavaCrashParser implements IParser {
+
+    /**
+     * Matches: java.lang.Exception
+     * Matches: java.lang.Exception: reason
+     */
+    private static final Pattern EXCEPTION = Pattern.compile("^([^\\s:]+)(: (.*))?$");
+    /**
+     * Matches: Caused by: java.lang.Exception
+     */
+    private static final Pattern CAUSEDBY = Pattern.compile("^Caused by: .+$");
+    /**
+     * Matches: \tat class.method(Class.java:1)
+     */
+    private static final Pattern AT = Pattern.compile("^\tat .+$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link JavaCrashItem}.
+     */
+    @Override
+    public JavaCrashItem parse(List<String> lines) {
+        JavaCrashItem jc = null;
+        StringBuilder stack = new StringBuilder();
+        StringBuilder message = new StringBuilder();
+        boolean inMessage = false;
+        boolean inCausedBy = false;
+        boolean inStack = false;
+
+        for (String line : lines) {
+            if (!inStack) {
+                Matcher exceptionMatch = EXCEPTION.matcher(line);
+                if (exceptionMatch.matches()) {
+                    inMessage = true;
+                    inStack = true;
+
+                    jc = new JavaCrashItem();
+                    jc.setException(exceptionMatch.group(1));
+                    if (exceptionMatch.group(3) != null) {
+                        message.append(exceptionMatch.group(3));
+                    }
+                }
+            } else {
+                // Match: Caused by: java.lang.Exception
+                Matcher causedByMatch = CAUSEDBY.matcher(line);
+                if (causedByMatch.matches()) {
+                    inMessage = false;
+                    inCausedBy = true;
+                }
+
+                // Match: \tat class.method(Class.java:1)
+                Matcher atMatch = AT.matcher(line);
+                if (atMatch.matches()) {
+                    inMessage = false;
+                    inCausedBy = false;
+                }
+
+                if (!causedByMatch.matches() && !atMatch.matches()) {
+                    if (inMessage) {
+                        message.append("\n");
+                        message.append(line);
+                    }
+                    if (!inMessage && !inCausedBy) {
+                        addMessageStack(jc, message.toString(), stack.toString());
+                        return jc;
+                    }
+                }
+            }
+
+            if (inStack) {
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+
+        addMessageStack(jc, message.toString(), stack.toString());
+        return jc;
+    }
+
+    /**
+     * Adds the message and stack to the {@link JavaCrashItem}.
+     */
+    private void addMessageStack(JavaCrashItem jc, String message, String stack) {
+        if (jc != null) {
+            if (message.length() > 0) {
+                jc.setMessage(message);
+            }
+            jc.setStack(stack.trim());
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/LogcatParser.java b/src/com/android/loganalysis/parser/LogcatParser.java
new file mode 100644
index 0000000..9edbede
--- /dev/null
+++ b/src/com/android/loganalysis/parser/LogcatParser.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle logcat.  The parser can handle the time and threadtime logcat
+ * formats.
+ * <p>
+ * Since the timestamps in the logcat do not have a year, the year can be set manually when the
+ * parser is created or through {@link #setYear(String)}.  If a year is not set, the current year
+ * will be used.
+ * </p>
+ */
+public class LogcatParser implements IParser {
+
+    /**
+     * Match a single line of `logcat -v threadtime`, such as:
+     * 05-26 11:02:36.886  5689  5689 D AndroidRuntime: CheckJNI is OFF
+     */
+    private static final Pattern THREADTIME_LINE = Pattern.compile(
+            "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" +  /* timestamp [1] */
+                "(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+" +  /* pid/tid and log level [2-4] */
+                "(.+?)\\s*: (.*)$" /* tag and message [5-6]*/);
+
+    /**
+     * Match a single line of `logcat -v time`, such as:
+     * 06-04 02:32:14.002 D/dalvikvm(  236): GC_CONCURRENT freed 580K, 51% free [...]
+     */
+    private static final Pattern TIME_LINE = Pattern.compile(
+            "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" +  /* timestamp [1] */
+                "(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$");  /* level, tag, pid, msg [2-5] */
+
+    /**
+     * Class for storing logcat meta data for a particular grouped list of lines.
+     */
+    private class LogcatData {
+        public Integer mPid = null;
+        public Integer mTid = null;
+        public Date mTime = null;
+        public String mLevel = null;
+        public String mTag = null;
+        public String mLastPreamble = null;
+        public String mProcPreamble = null;
+        public List<String> mLines = new LinkedList<String>();
+
+        public LogcatData(Integer pid, Integer tid, Date time, String level, String tag,
+                String lastPreamble, String procPreamble) {
+            mPid = pid;
+            mTid = tid;
+            mTime = time;
+            mLevel = level;
+            mTag = tag;
+            mLastPreamble = lastPreamble;
+            mProcPreamble = procPreamble;
+        }
+    }
+
+    private static final int MAX_BUFF_SIZE = 500;
+    private static final int MAX_LAST_PREAMBLE_SIZE = 15;
+    private static final int MAX_PROC_PREAMBLE_SIZE = 15;
+
+    private LinkedList<String> mRingBuffer = new LinkedList<String>();
+    private String mYear = null;
+
+    LogcatItem mLogcat = new LogcatItem();
+
+    Map<String, LogcatData> mDataMap = new HashMap<String, LogcatData>();
+    List<LogcatData> mDataList = new LinkedList<LogcatData>();
+
+    private Date mStartTime = null;
+    private Date mStopTime = null;
+
+    /**
+     * Constructor for {@link LogcatParser}.
+     */
+    public LogcatParser() {
+    }
+
+    /**
+     * Constructor for {@link LogcatParser}.
+     *
+     * @param year The year as a string.
+     */
+    public LogcatParser(String year) {
+        setYear(year);
+    }
+
+    /**
+     * Sets the year for {@link LogcatParser}.
+     *
+     * @param year The year as a string.
+     */
+    public void setYear(String year) {
+        mYear = year;
+    }
+
+    /**
+     * Parse a logcat from a {@link BufferedReader} into an {@link LogcatItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link LogcatItem}.
+     * @see #parse(List)
+     */
+    public LogcatItem parse(BufferedReader input) throws IOException {
+        String line;
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+        commit();
+
+        return mLogcat;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link LogcatItem}.
+     */
+    @Override
+    public LogcatItem parse(List<String> lines) {
+        for (String line : lines) {
+            parseLine(line);
+        }
+        commit();
+
+        return mLogcat;
+    }
+
+    /**
+     * Parse a line of input.
+     *
+     * @param line The line to parse
+     */
+    private void parseLine(String line) {
+        Integer pid = null;
+        Integer tid = null;
+        Date time = null;
+        String level = null;
+        String tag = null;
+        String msg = null;
+
+        Matcher m = THREADTIME_LINE.matcher(line);
+        Matcher tm = TIME_LINE.matcher(line);
+        if (m.matches()) {
+            time = parseTime(m.group(1));
+            pid = Integer.parseInt(m.group(2));
+            tid = Integer.parseInt(m.group(3));
+            level = m.group(4);
+            tag = m.group(5);
+            msg = m.group(6);
+        } else if (tm.matches()) {
+            time = parseTime(tm.group(1));
+            level = tm.group(2);
+            tag = tm.group(3);
+            pid = Integer.parseInt(tm.group(4));
+            msg = tm.group(5);
+        } else {
+            // CLog.w("Failed to parse line '%s'", line);
+            return;
+        }
+
+        if (mStartTime == null) {
+            mStartTime = time;
+        }
+        mStopTime = time;
+
+        // ANRs are split when START matches a line.  The newest entry is kept in the dataMap
+        // for quick lookup while all entries are added to the list.
+        if ("E".equals(level) && "ActivityManager".equals(tag)) {
+            String key = encodeLine(pid, tid, level, tag);
+            LogcatData data;
+            if (!mDataMap.containsKey(key) || AnrParser.START.matcher(msg).matches()) {
+                data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
+                        getProcPreamble(pid));
+                mDataMap.put(key, data);
+                mDataList.add(data);
+            } else {
+                data = mDataMap.get(key);
+            }
+            data.mLines.add(msg);
+        }
+
+        // PID and TID are enough to separate Java and native crashes.
+        if (("E".equals(level) && "AndroidRuntime".equals(tag)) ||
+                ("I".equals(level) && "DEBUG".equals(tag))) {
+            String key = encodeLine(pid, tid, level, tag);
+            LogcatData data;
+            if (!mDataMap.containsKey(key)) {
+                data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
+                        getProcPreamble(pid));
+                mDataMap.put(key, data);
+                mDataList.add(data);
+            } else {
+                data = mDataMap.get(key);
+            }
+            data.mLines.add(msg);
+        }
+
+        // After parsing the line, add it the the buffer for the preambles.
+        mRingBuffer.add(line);
+        if (mRingBuffer.size() > MAX_BUFF_SIZE) {
+            mRingBuffer.removeFirst();
+        }
+    }
+
+    /**
+     * Signal that the input has finished.
+     */
+    private void commit() {
+        for (LogcatData data : mDataList) {
+            GenericLogcatItem item = null;
+            if ("E".equals(data.mLevel) && "ActivityManager".equals(data.mTag)) {
+                // CLog.v("Parsing ANR: %s", data.mLines);
+                item = new AnrParser().parse(data.mLines);
+            } else if ("E".equals(data.mLevel) && "AndroidRuntime".equals(data.mTag)) {
+                // CLog.v("Parsing Java crash: %s", data.mLines);
+                item = new JavaCrashParser().parse(data.mLines);
+            } else if ("I".equals(data.mLevel) && "DEBUG".equals(data.mTag)) {
+                // CLog.v("Parsing native crash: %s", data.mLines);
+                item = new NativeCrashParser().parse(data.mLines);
+            }
+            if (item != null) {
+                item.setEventTime(data.mTime);
+                item.setPid(data.mPid);
+                item.setTid(data.mTid);
+                item.setLastPreamble(data.mLastPreamble);
+                item.setProcessPreamble(data.mProcPreamble);
+                mLogcat.addEvent(item);
+            }
+        }
+
+        mLogcat.setStartTime(mStartTime);
+        mLogcat.setStopTime(mStopTime);
+    }
+
+    /**
+     * Create an identifier that "should" be unique for a given logcat. In practice, we do use it as
+     * a unique identifier.
+     */
+    private static String encodeLine(Integer pid, Integer tid, String level, String tag) {
+        if (tid == null) {
+            return String.format("%d|%s|%s", pid, level, tag);
+        }
+        return String.format("%d|%d|%s|%s", pid, tid, level, tag);
+    }
+
+    /**
+     * Parse the timestamp and return a {@link Date}.  If year is not set, the current year will be
+     * used.
+     *
+     * @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}.
+     * @return The {@link Date}.
+     */
+    private Date parseTime(String timeStr) {
+        // If year is null, just use the current year.
+        if (mYear == null) {
+            DateFormat yearFormatter = new SimpleDateFormat("yyyy");
+            mYear = yearFormatter.format(new Date());
+        }
+
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        try {
+            return formatter.parse(String.format("%s-%s", mYear, timeStr));
+        } catch (ParseException e) {
+            // CLog.e("Could not parse time string %s", timeStr);
+            return null;
+        }
+    }
+
+    /**
+     * Get the last {@value #MAX_LAST_PREAMBLE_SIZE} lines of logcat.
+     */
+    private String getLastPreamble() {
+        final int size = mRingBuffer.size();
+        List<String> preamble;
+        if (size > getLastPreambleSize()) {
+            preamble = mRingBuffer.subList(size - getLastPreambleSize(), size);
+        } else {
+            preamble = mRingBuffer;
+        }
+        return ArrayUtil.join("\n", preamble).trim();
+    }
+
+    /**
+     * Get the last {@value #MAX_PROC_PREAMBLE_SIZE} lines of logcat which match the given pid.
+     */
+    private String getProcPreamble(int pid) {
+        LinkedList<String> preamble = new LinkedList<String>();
+
+        ListIterator<String> li = mRingBuffer.listIterator(mRingBuffer.size());
+        while (li.hasPrevious()) {
+            String line = li.previous();
+
+            Matcher m = THREADTIME_LINE.matcher(line);
+            Matcher tm = TIME_LINE.matcher(line);
+            if ((m.matches() && pid == Integer.parseInt(m.group(2))) ||
+                    (tm.matches() && pid == Integer.parseInt(tm.group(4)))) {
+                preamble.addFirst(line);
+            }
+
+            if (preamble.size() == getProcPreambleSize()) {
+                return ArrayUtil.join("\n", preamble).trim();
+            }
+        }
+        return ArrayUtil.join("\n", preamble).trim();
+    }
+
+    /**
+     * Get the number of lines in the last preamble. Exposed for unit testing.
+     */
+    int getLastPreambleSize() {
+        return MAX_LAST_PREAMBLE_SIZE;
+    }
+
+    /**
+     * Get the number of lines in the process preamble. Exposed for unit testing.
+     */
+    int getProcPreambleSize() {
+        return MAX_PROC_PREAMBLE_SIZE;
+    }
+}
diff --git a/src/com/android/loganalysis/parser/MemInfoParser.java b/src/com/android/loganalysis/parser/MemInfoParser.java
new file mode 100644
index 0000000..3aeaf05
--- /dev/null
+++ b/src/com/android/loganalysis/parser/MemInfoParser.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.MemInfoItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output from {@code /proc/meminfo}.
+ */
+public class MemInfoParser implements IParser {
+
+    /** Match a single MemoryInfo line, such as "MemFree:           65420 kB" */
+    private static final Pattern INFO_LINE = Pattern.compile("^([^:]+):\\s+(\\d+) kB");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link MemInfoItem}.
+     */
+    @Override
+    public MemInfoItem parse(List<String> block) {
+        MemInfoItem item = new MemInfoItem();
+
+        for (String line : block) {
+            Matcher m = INFO_LINE.matcher(line);
+            if (m.matches()) {
+                String key = m.group(1);
+                Integer value = Integer.parseInt(m.group(2));
+                item.put(key, value);
+            } else {
+                // CLog.w("Failed to parse line '%s'", line);
+            }
+        }
+
+        return item;
+    }
+}
diff --git a/src/com/android/loganalysis/parser/MonkeyLogParser.java b/src/com/android/loganalysis/parser/MonkeyLogParser.java
new file mode 100644
index 0000000..60cc428
--- /dev/null
+++ b/src/com/android/loganalysis/parser/MonkeyLogParser.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.GenericLogcatItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+import com.android.loganalysis.item.TracesItem;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse monkey logs.
+ */
+public class MonkeyLogParser implements IParser {
+    private static final Pattern THROTTLE = Pattern.compile(
+            "adb shell monkey.* --throttle (\\d+).*");
+    private static final Pattern SEED_AND_TARGET_COUNT = Pattern.compile(
+            ":Monkey: seed=(\\d+) count=(\\d+)");
+    private static final Pattern SECURITY_EXCEPTIONS = Pattern.compile(
+            "adb shell monkey.* --ignore-security-exceptions.*");
+
+    private static final Pattern PACKAGES = Pattern.compile(":AllowPackage: (\\S+)");
+    private static final Pattern CATEGORIES = Pattern.compile(":IncludeCategory: (\\S+)");
+
+    private static final Pattern START_UPTIME = Pattern.compile(
+            "# (.*) - device uptime = (\\d+\\.\\d+): Monkey command used for this test:");
+    private static final Pattern STOP_UPTIME = Pattern.compile(
+            "# (.*) - device uptime = (\\d+\\.\\d+): Monkey command ran for: " +
+            "(\\d+):(\\d+) \\(mm:ss\\)");
+
+    private static final Pattern INTERMEDIATE_COUNT = Pattern.compile(
+            "\\s+// Sending event #(\\d+)");
+    private static final Pattern FINISHED = Pattern.compile("// Monkey finished");
+    private static final Pattern FINAL_COUNT = Pattern.compile("Events injected: (\\d+)");
+    private static final Pattern NO_ACTIVITIES = Pattern.compile(
+            "\\*\\* No activities found to run, monkey aborted.");
+
+    private static final Pattern DROPPED_KEYS = Pattern.compile(":Dropped: .*keys=(\\d+).*");
+    private static final Pattern DROPPED_POINTERS = Pattern.compile(
+            ":Dropped: .*pointers=(\\d+).*");
+    private static final Pattern DROPPED_TRACKBALLS = Pattern.compile(
+            ":Dropped: .*trackballs=(\\d+).*");
+    private static final Pattern DROPPED_FLIPS = Pattern.compile(":Dropped: .*flips=(\\d+).*");
+    private static final Pattern DROPPED_ROTATIONS = Pattern.compile(
+            ":Dropped: .*rotations=(\\d+).*");
+
+    private static final Pattern ANR = Pattern.compile(
+            "// NOT RESPONDING: (\\S+) \\(pid (\\d+)\\)");
+    private static final Pattern JAVA_CRASH = Pattern.compile(
+            "// CRASH: (\\S+) \\(pid (\\d+)\\)");
+
+    private static final Pattern TRACES_START = Pattern.compile("anr traces:");
+    private static final Pattern TRACES_STOP = Pattern.compile("// anr traces status was \\d+");
+
+    private boolean mMatchingAnr = false;
+    private boolean mMatchingJavaCrash = false;
+    private boolean mMatchingTraces = false;
+    private List<String> mBlock = null;
+    private String mApp = null;
+    private int mPid = 0;
+
+    private MonkeyLogItem mMonkeyLog = new MonkeyLogItem();
+
+    /**
+     * Parse a monkey log from a {@link BufferedReader} into an {@link MonkeyLogItem} object.
+     *
+     * @param input a {@link BufferedReader}.
+     * @return The {@link MonkeyLogItem}.
+     * @see #parse(List)
+     */
+    public MonkeyLogItem parse(BufferedReader input) throws IOException {
+        String line;
+        while ((line = input.readLine()) != null) {
+            parseLine(line);
+        }
+
+        return mMonkeyLog;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link MonkeyLogItem}.
+     */
+    @Override
+    public MonkeyLogItem parse(List<String> lines) {
+        for (String line : lines) {
+            parseLine(line);
+        }
+
+        return mMonkeyLog;
+    }
+
+    /**
+     * Parse a line of input.
+     */
+    private void parseLine(String line) {
+        Matcher m;
+
+        if (mMatchingAnr || mMatchingJavaCrash) {
+            if (mMatchingJavaCrash) {
+                line = line.replace("// ", "");
+            }
+            if ("".equals(line)) {
+                GenericLogcatItem crash;
+                if (mMatchingAnr) {
+                    crash = new AnrParser().parse(mBlock);
+                } else {
+                    crash = new JavaCrashParser().parse(mBlock);
+                }
+                if (crash != null) {
+                    crash.setPid(mPid);
+                    crash.setApp(mApp);
+                    mMonkeyLog.setCrash(crash);
+                }
+
+                mMatchingAnr = false;
+                mMatchingJavaCrash = false;
+                mBlock = null;
+                mApp = null;
+                mPid = 0;
+            } else {
+                mBlock.add(line);
+            }
+            return;
+        }
+
+        if (mMatchingTraces) {
+            m = TRACES_STOP.matcher(line);
+            if (m.matches()) {
+                TracesItem traces = new TracesParser().parse(mBlock);
+
+                // Set the trace if the crash is an ANR and if the app for the crash and trace match
+                if (traces != null && traces.getApp() != null && traces.getStack() != null &&
+                        mMonkeyLog.getCrash() instanceof AnrItem &&
+                        traces.getApp().equals(mMonkeyLog.getCrash().getApp())) {
+                    ((AnrItem) mMonkeyLog.getCrash()).setTrace(traces.getStack());
+                }
+
+                mMatchingTraces = false;
+                mBlock = null;
+            } else {
+                mBlock.add(line);
+            }
+        }
+
+        m = THROTTLE.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setThrottle(Integer.parseInt(m.group(1)));
+        }
+        m = SEED_AND_TARGET_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setSeed(Integer.parseInt(m.group(1)));
+            mMonkeyLog.setTargetCount(Integer.parseInt(m.group(2)));
+        }
+        m = SECURITY_EXCEPTIONS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIgnoreSecurityExceptions(true);
+        }
+        m = PACKAGES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.addPackage(m.group(1));
+        }
+        m = CATEGORIES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.addCategory(m.group(1));
+        }
+        m = START_UPTIME.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setStartTime(parseTime(m.group(1)));
+            mMonkeyLog.setStartUptimeDuration((long) (Double.parseDouble(m.group(2)) * 1000));
+        }
+        m = STOP_UPTIME.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setStopTime(parseTime(m.group(1)));
+            mMonkeyLog.setStopUptimeDuration((long) (Double.parseDouble(m.group(2)) * 1000));
+            mMonkeyLog.setTotalDuration(60 * 1000 * Integer.parseInt(m.group(3)) +
+                    1000 *Integer.parseInt(m.group(4)));
+        }
+        m = INTERMEDIATE_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIntermediateCount(Integer.parseInt(m.group(1)));
+        }
+        m = FINAL_COUNT.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setFinalCount(Integer.parseInt(m.group(1)));
+        }
+        m = FINISHED.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setIsFinished(true);
+        }
+        m = NO_ACTIVITIES.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setNoActivities(true);
+        }
+        m = DROPPED_KEYS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.KEYS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_POINTERS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.POINTERS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_TRACKBALLS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.TRACKBALLS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_FLIPS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.FLIPS, Integer.parseInt(m.group(1)));
+        }
+        m = DROPPED_ROTATIONS.matcher(line);
+        if (m.matches()) {
+            mMonkeyLog.setDroppedCount(DroppedCategory.ROTATIONS, Integer.parseInt(m.group(1)));
+        }
+        m = ANR.matcher(line);
+        if (m.matches()) {
+            mApp = m.group(1);
+            mPid = Integer.parseInt(m.group(2));
+            mBlock = new LinkedList<String>();
+            mMatchingAnr = true;
+        }
+        m = JAVA_CRASH.matcher(line);
+        if (m.matches()) {
+            mApp = m.group(1);
+            mPid = Integer.parseInt(m.group(2));
+            mBlock = new LinkedList<String>();
+            mMatchingJavaCrash = true;
+        }
+        m = TRACES_START.matcher(line);
+        if (m.matches()) {
+            mBlock = new LinkedList<String>();
+            mMatchingTraces = true;
+        }
+    }
+
+    /**
+     * Parse the timestamp and return a date.
+     *
+     * @param timeStr The timestamp in the format {@code E, MM/dd/yyyy hh:mm:ss a} or
+     * {@code EEE MMM dd HH:mm:ss zzz yyyy}.
+     * @return The {@link Date}.
+     */
+    private Date parseTime(String timeStr) {
+        try {
+            return new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy").parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.v("Could not parse date %s with format EEE MMM dd HH:mm:ss zzz yyyy", timeStr);
+        }
+
+        try {
+            return new SimpleDateFormat("E, MM/dd/yyyy hh:mm:ss a").parse(timeStr);
+        } catch (ParseException e) {
+            // CLog.v("Could not parse date %s with format E, MM/dd/yyyy hh:mm:ss a", timeStr);
+        }
+
+        // CLog.e("Could not parse date %s", timeStr);
+        return null;
+    }
+
+}
diff --git a/src/com/android/loganalysis/parser/NativeCrashParser.java b/src/com/android/loganalysis/parser/NativeCrashParser.java
new file mode 100644
index 0000000..362ed8e
--- /dev/null
+++ b/src/com/android/loganalysis/parser/NativeCrashParser.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.NativeCrashItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link IParser} to handle native crashes.
+ */
+public class NativeCrashParser implements IParser {
+
+    /** Matches: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
+    private static final Pattern START = Pattern.compile("^(?:\\*\\*\\* ){15}\\*\\*\\*$");
+    /** Matches: Build fingerprint: 'fingerprint' */
+    private static final Pattern FINGERPRINT = Pattern.compile("^Build fingerprint: '(.*)'$");
+    /** Matches: pid: 957, tid: 963  >>> com.android.camera <<< */
+    private static final Pattern APP = Pattern.compile(
+            "^pid: \\d+, tid: \\d+(, name: \\S+)?  >>> (\\S+) <<<$");
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link NativeCrashItem}.
+     */
+    @Override
+    public NativeCrashItem parse(List<String> lines) {
+        NativeCrashItem nc = null;
+        StringBuilder stack = new StringBuilder();
+
+        for (String line : lines) {
+            Matcher m = START.matcher(line);
+            if (m.matches()) {
+                nc = new NativeCrashItem();
+            }
+
+            if (nc != null) {
+                m = FINGERPRINT.matcher(line);
+                if (m.matches()) {
+                    nc.setFingerprint(m.group(1));
+                }
+                m = APP.matcher(line);
+                if (m.matches()) {
+                    nc.setApp(m.group(2));
+                }
+
+                stack.append(line);
+                stack.append("\n");
+            }
+        }
+        if (nc != null) {
+            nc.setStack(stack.toString().trim());
+        }
+        return nc;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/NoopParser.java b/src/com/android/loganalysis/parser/NoopParser.java
new file mode 100644
index 0000000..2c78742
--- /dev/null
+++ b/src/com/android/loganalysis/parser/NoopParser.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import java.util.List;
+
+/**
+ * A {@link IParser} that consumes nothing.
+ */
+public class NoopParser implements IParser {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IItem parse(List<String> block) {
+        // ignore
+        return null;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/ProcrankParser.java b/src/com/android/loganalysis/parser/ProcrankParser.java
new file mode 100644
index 0000000..938a62c
--- /dev/null
+++ b/src/com/android/loganalysis/parser/ProcrankParser.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.ProcrankItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output of {@code procrank}.  Memory values returned are in units
+ * of kilobytes.
+ */
+public class ProcrankParser implements IParser {
+
+    /** Match a valid line, such as:
+     * " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps" */
+    private static final Pattern LINE_PAT = Pattern.compile(
+            "\\s*(\\d+)\\s+" + /* PID [1] */
+            "(\\d+)K\\s+(\\d+)K\\s+(\\d+)K\\s+(\\d+)K\\s+" + /* Vss Rss Pss Uss [2-5] */
+            "(\\S+)" /* process name [6] */);
+
+    /** Match the end of the Procrank table, determined by three sets of "------". */
+    private static final Pattern END_PAT = Pattern.compile("^\\s+-{6}\\s+-{6}\\s+-{6}");
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ProcrankItem parse(List<String> lines) {
+        ProcrankItem item = new ProcrankItem();
+
+        for (String line : lines) {
+            // If we have reached the end.
+            Matcher endMatcher = END_PAT.matcher(line);
+            if (endMatcher.matches()) {
+                return item;
+            }
+
+            Matcher m = LINE_PAT.matcher(line);
+            if (m.matches()) {
+                item.addProcrankLine(Integer.parseInt(m.group(1)), m.group(6),
+                        Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)),
+                        Integer.parseInt(m.group(4)), Integer.parseInt(m.group(5)));
+            }
+        }
+
+        return item;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/SystemPropsParser.java b/src/com/android/loganalysis/parser/SystemPropsParser.java
new file mode 100644
index 0000000..a1cd9da
--- /dev/null
+++ b/src/com/android/loganalysis/parser/SystemPropsParser.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.SystemPropsItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to handle the output from {@code getprop}.
+ */
+public class SystemPropsParser implements IParser {
+    /** Match a single property line, such as "[gsm.sim.operator.numeric]: []" */
+    private static final Pattern PROP_LINE = Pattern.compile("^\\[(.*)\\]: \\[(.*)\\]$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link SystemPropsItem}.
+     */
+    @Override
+    public SystemPropsItem parse(List<String> lines) {
+        SystemPropsItem item = new SystemPropsItem();
+
+        for (String line : lines) {
+            Matcher m = PROP_LINE.matcher(line);
+            if (m.matches()) {
+                item.put(m.group(1), m.group(2));
+            } else {
+                // CLog.w("Failed to parse line '%s'", line);
+            }
+        }
+        return item;
+    }
+}
+
diff --git a/src/com/android/loganalysis/parser/TracesParser.java b/src/com/android/loganalysis/parser/TracesParser.java
new file mode 100644
index 0000000..4d0f581
--- /dev/null
+++ b/src/com/android/loganalysis/parser/TracesParser.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.TracesItem;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link IParser} to parse Android traces files.
+ * <p>
+ * For now, this only extracts the main stack trace from the first process. It is used to get a
+ * stack from {@code /data/anr/traces.txt} which can be used to give some context about the ANR. If
+ * there is a need, this parser can be expanded to parse all stacks from all processes.
+ */
+public class TracesParser implements IParser {
+
+    /**
+     * Matches: ----- pid PID at YYYY-MM-DD hh:mm:ss -----
+     */
+    private static final Pattern PID = Pattern.compile(
+            "^----- pid (\\d+) at \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} -----$");
+
+    /**
+     * Matches: Cmd line: APP
+     */
+    private static final Pattern APP = Pattern.compile("^Cmd line: (\\S+)$");
+
+    /**
+     * Matches: "main" prio=5 tid=1 STATE
+     */
+    private static final Pattern STACK = Pattern.compile("^\"main\" .*$");
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The {@link TracesItem}.
+     */
+    @Override
+    public TracesItem parse(List<String> lines) {
+        TracesItem traces = new TracesItem();
+        StringBuffer stack = null;
+
+        for (String line : lines) {
+            if (stack == null) {
+                Matcher m = PID.matcher(line);
+                if (m.matches()) {
+                    traces.setPid(Integer.parseInt(m.group(1)));
+                }
+                m = APP.matcher(line);
+                if (m.matches()) {
+                    traces.setApp(m.group(1));
+                }
+                m = STACK.matcher(line);
+                if (m.matches()) {
+                    stack = new StringBuffer();
+                    stack.append(line);
+                    stack.append("\n");
+                }
+            } else if (!"".equals(line)) {
+                stack.append(line);
+                stack.append("\n");
+            } else {
+                traces.setStack(stack.toString().trim());
+                return traces;
+            }
+        }
+        if (stack == null) {
+            return null;
+        }
+        traces.setStack(stack.toString().trim());
+        return traces;
+    }
+
+}
diff --git a/src/com/android/loganalysis/util/ArrayUtil.java b/src/com/android/loganalysis/util/ArrayUtil.java
new file mode 100644
index 0000000..95b1634
--- /dev/null
+++ b/src/com/android/loganalysis/util/ArrayUtil.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Utility methods for arrays
+ */
+// TODO: Use libTF once this is copied over.
+public class ArrayUtil {
+
+    private ArrayUtil() {
+    }
+
+    /**
+     * Build an array from the provided contents.
+     *
+     * <p>
+     * The resulting array will be the concatenation of <var>arrays</var> input arrays, in their
+     * original order.
+     * </p>
+     *
+     * @param arrays the arrays to concatenate
+     * @return the newly constructed array
+     */
+    public static String[] buildArray(String[]... arrays) {
+        int length = 0;
+        for (String[] array : arrays) {
+            length += array.length;
+        }
+        String[] newArray = new String[length];
+        int offset = 0;
+        for (String[] array : arrays) {
+            System.arraycopy(array, 0, newArray, offset, array.length);
+            offset += array.length;
+        }
+        return newArray;
+    }
+
+    /**
+     * Convert a varargs list/array to an {@link List}.  This is useful for building instances of
+     * {@link List} by hand.  Note that this differs from {@link java.util.Arrays#asList} in that
+     * the returned array is mutable.
+     *
+     * @param inputAry an array, or a varargs list
+     * @return a {@link List} instance with the identical contents
+     */
+    public static <T> List<T> list(T... inputAry) {
+        List<T> retList = new ArrayList<T>(inputAry.length);
+        for (T item : inputAry) {
+            retList.add(item);
+        }
+        return retList;
+    }
+
+    private static String internalJoin(String sep, Collection<Object> pieces) {
+        StringBuilder sb = new StringBuilder();
+        boolean skipSep = true;
+        Iterator<Object> iter = pieces.iterator();
+        while (iter.hasNext()) {
+            if (skipSep) {
+                skipSep = false;
+            } else {
+                sb.append(sep);
+            }
+
+            Object obj = iter.next();
+            if (obj == null) {
+                sb.append("null");
+            } else {
+                sb.append(obj.toString());
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Turns a sequence of objects into a string, delimited by {@code sep}.  If a single
+     * {@code Collection} is passed, it is assumed that the elements of that Collection are to be
+     * joined.  Otherwise, wraps the passed {@link Object}(s) in a {@link List} and joins the
+     * generated list.
+     *
+     * @param sep the string separator to delimit the different output segments.
+     * @param pieces A {@link Collection} or a varargs {@code Array} of objects.
+     */
+    @SuppressWarnings("unchecked")
+    public static String join(String sep, Object... pieces) {
+        if ((pieces.length == 1) && (pieces[0] instanceof Collection)) {
+            // Don't re-wrap the Collection
+            return internalJoin(sep, (Collection<Object>) pieces[0]);
+        } else {
+            return internalJoin(sep, Arrays.asList(pieces));
+        }
+    }
+}
+
diff --git a/src/com/android/loganalysis/util/RegexTrie.java b/src/com/android/loganalysis/util/RegexTrie.java
new file mode 100644
index 0000000..f3c2d6a
--- /dev/null
+++ b/src/com/android/loganalysis/util/RegexTrie.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The RegexTrie is a trie where each _stored_ segment of the key is a regex {@link Pattern}.  Thus,
+ * the full _stored_ key is a List<Pattern> rather than a String as in a standard trie.  Note that
+ * the {@link #get(Object key)} method requires a List<String>, which will be matched against the
+ * {@link Pattern}s, rather than checked for equality as in a standard trie.  It will likely perform
+ * poorly for large datasets.
+ * <p />
+ * One can also use a {@code null} entry in the {@code Pattern} sequence to serve as a wildcard.  If
+ * a {@code null} is encountered, all subsequent entries in the sequence will be ignored.
+ * When the retrieval code encounters a {@code null} {@code Pattern}, it will first wait to see if a
+ * more-specific entry matches the sequence.  If one does, that more-specific entry will proceed,
+ * even if it subsequently fails to match.
+ * <p />
+ * If no more-specific entry matches, the wildcard match will add all remaining {@code String}s
+ * to the list of captures (if enabled) and return the value associated with the wildcard.
+ * <p />
+ * A short sample of the wildcard functionality:
+ * <pre>
+ * List<List<String>> captures = new LinkedList<List<String>>();
+ * RegexTrie<Integer> trie = new RegexTrie<Integer>();
+ * trie.put(2, "a", null);
+ * trie.put(4, "a", "b");
+ * trie.retrieve(captures, "a", "c", "e");
+ * // returns 2.  captures is now [[], ["c"], ["e"]]
+ * trie.retrieve(captures, "a", "b");
+ * // returns 4.  captures is now [[], []]
+ * trie.retrieve(captures, "a", "b", "c");
+ * // returns null.  captures is now [[], []]
+ * </pre>
+ */
+//TODO: Use libTF once this is copied over.
+public class RegexTrie<V> {
+    private V mValue = null;
+    private Map<CompPattern, RegexTrie<V>> mChildren =
+            new LinkedHashMap<CompPattern, RegexTrie<V>>();
+
+    /**
+     * Patterns aren't comparable by default, which prevents you from retrieving them from a
+     * HashTable.  This is a simple stub class that makes a Pattern with a working
+     * {@link CompPattern#equals()} method.
+     */
+    static class CompPattern {
+        protected final Pattern mPattern;
+
+        CompPattern(Pattern pattern) {
+            if (pattern == null) {
+                throw new NullPointerException();
+            }
+            mPattern = pattern;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            Pattern otherPat;
+            if (other instanceof Pattern) {
+                otherPat = (Pattern) other;
+            } else if (other instanceof CompPattern) {
+                CompPattern otherCPat = (CompPattern) other;
+                otherPat = otherCPat.mPattern;
+            } else {
+                return false;
+            }
+            return mPattern.toString().equals(otherPat.toString());
+        }
+
+        @Override
+        public int hashCode() {
+            return mPattern.toString().hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return String.format("CP(%s)", mPattern.toString());
+        }
+
+        public Matcher matcher(String string) {
+            return mPattern.matcher(string);
+        }
+    }
+
+    public void clear() {
+        mValue = null;
+        for (RegexTrie child : mChildren.values()) {
+            child.clear();
+        }
+        mChildren.clear();
+    }
+
+    boolean containsKey(String... strings) {
+        return retrieve(strings) != null;
+    }
+
+    V recursivePut(V value, List<CompPattern> patterns) {
+        // Cases:
+        // 1) patterns is empty -- set our value
+        // 2) patterns is non-empty -- recurse downward, creating a child if necessary
+        if (patterns.isEmpty()) {
+            V oldValue = mValue;
+            mValue = value;
+            return oldValue;
+        } else {
+            CompPattern curKey = patterns.get(0);
+            List<CompPattern> nextKeys = patterns.subList(1, patterns.size());
+
+            // Create a new child to handle
+            RegexTrie<V> nextChild = mChildren.get(curKey);
+            if (nextChild == null) {
+                nextChild = new RegexTrie<V>();
+                mChildren.put(curKey, nextChild);
+            }
+            return nextChild.recursivePut(value, nextKeys);
+        }
+    }
+
+    /**
+     * A helper method to consolidate validation before adding an entry to the trie.
+     *
+     * @param value The value to set
+     * @param patterns The sequence of {@link CompPattern}s that must be sequentially matched to
+     *        retrieve the associated {@code value}
+     */
+    private V validateAndPut(V value, List<CompPattern> pList) {
+        if (pList.size() == 0) {
+            throw new IllegalArgumentException("pattern list must be non-empty.");
+        }
+        return recursivePut(value, pList);
+    }
+
+    /**
+     * Add an entry to the trie.
+     *
+     * @param value The value to set
+     * @param patterns The sequence of {@link Pattern}s that must be sequentially matched to
+     *        retrieve the associated {@code value}
+     */
+    public V put(V value, Pattern... patterns) {
+        List<CompPattern> pList = new ArrayList<CompPattern>(patterns.length);
+        for (Pattern pat : patterns) {
+            if (pat == null) {
+                pList.add(null);
+                break;
+            }
+            pList.add(new CompPattern(pat));
+        }
+        return validateAndPut(value, pList);
+    }
+
+    /**
+     * This helper method takes a list of regular expressions as {@link String}s and compiles them
+     * on-the-fly before adding the subsequent {@link Pattern}s to the trie
+     *
+     * @param value The value to set
+     * @param patterns The sequence of regular expressions (as {@link String}s) that must be
+     *        sequentially matched to retrieve the associated {@code value}.  Each String will be
+     *        compiled as a {@link Pattern} before invoking {@link #put(V, Pattern...)}.
+     */
+    public V put(V value, String... regexen) {
+        List<CompPattern> pList = new ArrayList<CompPattern>(regexen.length);
+        for (String regex : regexen) {
+            if (regex == null) {
+                pList.add(null);
+                break;
+            }
+            Pattern pat = Pattern.compile(regex);
+            pList.add(new CompPattern(pat));
+        }
+        return validateAndPut(value, pList);
+    }
+
+    V recursiveRetrieve(List<List<String>> captures, List<String> strings) {
+        // Cases:
+        // 1) strings is empty -- return our value
+        // 2) strings is non-empty -- find the first child that matches, recurse downward
+        if (strings.isEmpty()) {
+            return mValue;
+        } else {
+            boolean wildcardMatch = false;
+            V wildcardValue = null;
+            String curKey = strings.get(0);
+            List<String> nextKeys = strings.subList(1, strings.size());
+
+            for (Map.Entry<CompPattern, RegexTrie<V>> child : mChildren.entrySet()) {
+                CompPattern pattern = child.getKey();
+                if (pattern == null) {
+                    wildcardMatch = true;
+                    wildcardValue = child.getValue().getValue();
+                    continue;
+                }
+
+                Matcher matcher = pattern.matcher(curKey);
+                if (matcher.matches()) {
+                    if (captures != null) {
+                        List<String> curCaptures = new ArrayList<String>(matcher.groupCount());
+                        for (int i = 0; i < matcher.groupCount(); i++) {
+                            // i+1 since group 0 is the entire matched string
+                            curCaptures.add(matcher.group(i+1));
+                        }
+                        captures.add(curCaptures);
+                    }
+
+                    return child.getValue().recursiveRetrieve(captures, nextKeys);
+                }
+            }
+
+            if (wildcardMatch) {
+                // Stick the rest of the query string into the captures list and return
+                if (captures != null) {
+                    for (String str : strings) {
+                        captures.add(Arrays.asList(str));
+                    }
+                }
+                return wildcardValue;
+            }
+
+            // no match
+            return null;
+        }
+    }
+
+    /**
+     * Fetch a value from the trie, by matching the provided sequence of {@link String}s to a
+     * sequence of {@link Pattern}s stored in the trie.
+     *
+     * @param strings A sequence of {@link String}s to match
+     * @return The associated value, or {@code null} if no value was found
+     */
+    public V retrieve(String... strings) {
+        return retrieve(null, strings);
+    }
+
+    /**
+     * Fetch a value from the trie, by matching the provided sequence of {@link String}s to a
+     * sequence of {@link Pattern}s stored in the trie.  This version of the method also returns
+     * a {@link List} of capture groups for each {@link Pattern} that was matched.
+     * <p />
+     * Each entry in the outer List corresponds to one level of {@code Pattern} in the trie.
+     * For each level, the list of capture groups will be stored.  If there were no captures
+     * for a particular level, an empty list will be stored.
+     * <p />
+     * Note that {@code captures} will be {@link List#clear()}ed before the retrieval begins.
+     * Also, if the retrieval fails after a partial sequence of matches, {@code captures} will
+     * still reflect the capture groups from the partial match.
+     *
+     * @param captures A {@code List<List<String>>} through which capture groups will be returned.
+     * @param strings A sequence of {@link String}s to match
+     * @return The associated value, or {@code null} if no value was found
+     */
+    public V retrieve(List<List<String>> captures, String... strings) {
+        if (strings.length == 0) {
+            throw new IllegalArgumentException("string list must be non-empty");
+        }
+        List<String> sList = Arrays.asList(strings);
+        if (captures != null) {
+            captures.clear();
+        }
+        return recursiveRetrieve(captures, sList);
+    }
+
+    private V getValue() {
+        return mValue;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{V: %s, C: %s}", mValue, mChildren);
+    }
+}
+
diff --git a/tests/.classpath b/tests/.classpath
new file mode 100644
index 0000000..32d4a0f
--- /dev/null
+++ b/tests/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/loganalysis"/>
+        <classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/easymock_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/easymock/src"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/tests/.project b/tests/.project
new file mode 100644
index 0000000..f4a1c0b
--- /dev/null
+++ b/tests/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>loganalysis-tests</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..32019a7
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,42 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Only compile source java files in this lib.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVACFLAGS += -g -Xlint
+
+LOCAL_MODULE := loganalysis-tests
+LOCAL_MODULE_TAGS := optional
+LOCAL_STATIC_JAVA_LIBRARIES := easymock junit
+LOCAL_JAVA_LIBRARIES := loganalysis
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# makefile rules to copy jars to HOST_OUT/tradefed
+# so tradefed.sh can automatically add to classpath
+
+DEST_JAR := $(HOST_OUT)/tradefed/$(LOCAL_MODULE).jar
+$(DEST_JAR): $(LOCAL_BUILT_MODULE)
+	$(copy-file-to-new-target)
+
+# this dependency ensure the above rule will be executed if module is built
+$(LOCAL_INSTALLED_MODULE) : $(DEST_JAR)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/src/com/android/loganalysis/FuncTests.java b/tests/src/com/android/loganalysis/FuncTests.java
new file mode 100644
index 0000000..7a06cbd
--- /dev/null
+++ b/tests/src/com/android/loganalysis/FuncTests.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis;
+
+import com.android.loganalysis.parser.BugreportParserFuncTest;
+import com.android.loganalysis.parser.LogcatParserFuncTest;
+import com.android.loganalysis.parser.MonkeyLogParserFuncTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * A test suite for all log analysis functional tests.
+ */
+public class FuncTests extends TestSuite {
+
+    public FuncTests() {
+        super();
+
+        addTestSuite(BugreportParserFuncTest.class);
+        addTestSuite(LogcatParserFuncTest.class);
+        addTestSuite(MonkeyLogParserFuncTest.class);
+    }
+
+    public static Test suite() {
+        return new FuncTests();
+    }
+}
diff --git a/tests/src/com/android/loganalysis/UnitTests.java b/tests/src/com/android/loganalysis/UnitTests.java
new file mode 100644
index 0000000..083d571
--- /dev/null
+++ b/tests/src/com/android/loganalysis/UnitTests.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.loganalysis;
+
+import com.android.loganalysis.item.GenericItemTest;
+import com.android.loganalysis.parser.AbstractSectionParserTest;
+import com.android.loganalysis.parser.AnrParserTest;
+import com.android.loganalysis.parser.BugreportParserTest;
+import com.android.loganalysis.parser.JavaCrashParserTest;
+import com.android.loganalysis.parser.LogcatParserTest;
+import com.android.loganalysis.parser.MemInfoParserTest;
+import com.android.loganalysis.parser.MonkeyLogParserTest;
+import com.android.loganalysis.parser.NativeCrashParserTest;
+import com.android.loganalysis.parser.ProcrankParserTest;
+import com.android.loganalysis.parser.SystemPropsParserTest;
+import com.android.loganalysis.parser.TracesParserTest;
+import com.android.loganalysis.util.ArrayUtilTest;
+import com.android.loganalysis.util.RegexTrieTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * A test suite for all Trade Federation unit tests.
+ * <p/>
+ * All tests listed here should be self-contained, and should not require any external dependencies.
+ */
+public class UnitTests extends TestSuite {
+
+    public UnitTests() {
+        super();
+
+        // item
+        addTestSuite(GenericItemTest.class);
+
+        // parser
+        addTestSuite(AbstractSectionParserTest.class);
+        addTestSuite(AnrParserTest.class);
+        addTestSuite(BugreportParserTest.class);
+        addTestSuite(JavaCrashParserTest.class);
+        addTestSuite(LogcatParserTest.class);
+        addTestSuite(MemInfoParserTest.class);
+        addTestSuite(MonkeyLogParserTest.class);
+        addTestSuite(NativeCrashParserTest.class);
+        addTestSuite(ProcrankParserTest.class);
+        addTestSuite(SystemPropsParserTest.class);
+        addTestSuite(TracesParserTest.class);
+
+        // util
+        addTestSuite(ArrayUtilTest.class);
+        addTestSuite(RegexTrieTest.class);
+    }
+
+    public static Test suite() {
+        return new UnitTests();
+    }
+}
diff --git a/tests/src/com/android/loganalysis/item/GenericItemTest.java b/tests/src/com/android/loganalysis/item/GenericItemTest.java
new file mode 100644
index 0000000..037f39a
--- /dev/null
+++ b/tests/src/com/android/loganalysis/item/GenericItemTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.item;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Unit test for {@link GenericItem}.
+ */
+public class GenericItemTest extends TestCase {
+    private static final Set<String> ATTRIBUTES = new HashSet<String>(Arrays.asList(
+            "integer", "string"));
+
+    private String mStringAttribute = "String";
+    private Integer mIntegerAttribute = 1;
+
+    /** Empty item with no attributes set */
+    private GenericItem mEmptyItem1;
+    /** Empty item with no attributes set */
+    private GenericItem mEmptyItem2;
+    /** Item with only the string attribute set */
+    private GenericItem mStringItem;
+    /** Item with only the integer attribute set */
+    private GenericItem mIntegerItem;
+    /** Item with both attributes set, product of mStringItem and mIntegerItem */
+    private GenericItem mFullItem1;
+    /** Item with both attributes set, product of mStringItem and mIntegerItem */
+    private GenericItem mFullItem2;
+    /** Item that is inconsistent with the others */
+    private GenericItem mInconsistentItem;
+
+    @Override
+    public void setUp() {
+        mEmptyItem1 = new GenericItem(null, ATTRIBUTES);
+        mEmptyItem2 = new GenericItem(null, ATTRIBUTES);
+        mStringItem = new GenericItem(null, ATTRIBUTES);
+        mStringItem.setAttribute("string", mStringAttribute);
+        mIntegerItem = new GenericItem(null, ATTRIBUTES);
+        mIntegerItem.setAttribute("integer", mIntegerAttribute);
+        mFullItem1 = new GenericItem(null, ATTRIBUTES);
+        mFullItem1.setAttribute("string", mStringAttribute);
+        mFullItem1.setAttribute("integer", mIntegerAttribute);
+        mFullItem2 = new GenericItem(null, ATTRIBUTES);
+        mFullItem2.setAttribute("string", mStringAttribute);
+        mFullItem2.setAttribute("integer", mIntegerAttribute);
+        mInconsistentItem = new GenericItem(null, ATTRIBUTES);
+        mInconsistentItem.setAttribute("string", "gnirts");
+        mInconsistentItem.setAttribute("integer", 2);
+    }
+
+    /**
+     * Test for {@link GenericItem#mergeAttributes(IItem)}.
+     */
+    public void testMergeAttributes() throws ConflictingItemException {
+        Map<String, Object> attributes;
+
+        attributes = mEmptyItem1.mergeAttributes(mEmptyItem1);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mEmptyItem2);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem2.mergeAttributes(mEmptyItem1);
+        assertNull(attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mStringItem);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mStringItem.mergeAttributes(mEmptyItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertNull(attributes.get("integer"));
+
+        attributes = mIntegerItem.mergeAttributes(mStringItem);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mEmptyItem1.mergeAttributes(mFullItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mFullItem1.mergeAttributes(mEmptyItem1);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        attributes = mFullItem1.mergeAttributes(mFullItem2);
+        assertEquals(mStringAttribute, attributes.get("string"));
+        assertEquals(mIntegerAttribute, attributes.get("integer"));
+
+        try {
+            mFullItem1.mergeAttributes(mInconsistentItem);
+            fail("Expecting a ConflictingItemException");
+        } catch (ConflictingItemException e) {
+            // Expected
+        }
+    }
+
+    /**
+     * Test for {@link GenericItem#isConsistent(IItem)}.
+     */
+    public void testIsConsistent() {
+        assertTrue(mEmptyItem1.isConsistent(mEmptyItem1));
+        assertFalse(mEmptyItem1.isConsistent(null));
+        assertTrue(mEmptyItem1.isConsistent(mEmptyItem2));
+        assertTrue(mEmptyItem2.isConsistent(mEmptyItem1));
+        assertTrue(mEmptyItem1.isConsistent(mStringItem));
+        assertTrue(mStringItem.isConsistent(mEmptyItem1));
+        assertTrue(mIntegerItem.isConsistent(mStringItem));
+        assertTrue(mEmptyItem1.isConsistent(mFullItem1));
+        assertTrue(mFullItem1.isConsistent(mEmptyItem1));
+        assertTrue(mFullItem1.isConsistent(mFullItem2));
+        assertFalse(mFullItem1.isConsistent(mInconsistentItem));
+    }
+
+    /**
+     * Test {@link GenericItem#equals(Object)}.
+     */
+    public void testEquals() {
+        assertTrue(mEmptyItem1.equals(mEmptyItem1));
+        assertFalse(mEmptyItem1.equals(null));
+        assertTrue(mEmptyItem1.equals(mEmptyItem2));
+        assertTrue(mEmptyItem2.equals(mEmptyItem1));
+        assertFalse(mEmptyItem1.equals(mStringItem));
+        assertFalse(mStringItem.equals(mEmptyItem1));
+        assertFalse(mIntegerItem.equals(mStringItem));
+        assertFalse(mEmptyItem1.equals(mFullItem1));
+        assertFalse(mFullItem1.equals(mEmptyItem1));
+        assertTrue(mFullItem1.equals(mFullItem2));
+        assertFalse(mFullItem1.equals(mInconsistentItem));
+    }
+
+    /**
+     * Test for {@link GenericItem#setAttribute(String, Object)} and
+     * {@link GenericItem#getAttribute(String)}.
+     */
+    public void testAttributes() {
+        GenericItem item = new GenericItem(null, ATTRIBUTES);
+
+        assertNull(item.getAttribute("string"));
+        assertNull(item.getAttribute("integer"));
+
+        item.setAttribute("string", mStringAttribute);
+        item.setAttribute("integer", mIntegerAttribute);
+
+        assertEquals(mStringAttribute, item.getAttribute("string"));
+        assertEquals(mIntegerAttribute, item.getAttribute("integer"));
+
+        item.setAttribute("string", null);
+        item.setAttribute("integer", null);
+
+        assertNull(item.getAttribute("string"));
+        assertNull(item.getAttribute("integer"));
+
+        try {
+            item.setAttribute("object", new Object());
+            fail("Failed to throw IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // Expected because "object" is not "string" or "integer".
+        }
+    }
+
+    /**
+     * Test for {@link GenericItem#areEqual(Object, Object)}
+     */
+    public void testAreEqual() {
+        assertTrue(GenericItem.areEqual(null, null));
+        assertTrue(GenericItem.areEqual("test", "test"));
+        assertFalse(GenericItem.areEqual(null, "test"));
+        assertFalse(GenericItem.areEqual("test", null));
+        assertFalse(GenericItem.areEqual("test", ""));
+    }
+
+    /**
+     * Test for {@link GenericItem#areConsistent(Object, Object)}
+     */
+    public void testAreConsistent() {
+        assertTrue(GenericItem.areConsistent(null, null));
+        assertTrue(GenericItem.areConsistent("test", "test"));
+        assertTrue(GenericItem.areConsistent(null, "test"));
+        assertTrue(GenericItem.areConsistent("test", null));
+        assertFalse(GenericItem.areConsistent("test", ""));
+    }
+
+    /**
+     * Test for {@link GenericItem#mergeObjects(Object, Object)}
+     */
+    public void testMergeObjects() throws ConflictingItemException {
+        assertNull(GenericItem.mergeObjects(null, null));
+        assertEquals("test", GenericItem.mergeObjects("test", "test"));
+        assertEquals("test", GenericItem.mergeObjects(null, "test"));
+        assertEquals("test", GenericItem.mergeObjects("test", null));
+
+        try {
+            assertEquals("test", GenericItem.mergeObjects("test", ""));
+            fail("Expected ConflictingItemException to be thrown");
+        } catch (ConflictingItemException e) {
+            // Expected because "test" conflicts with "".
+        }
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java b/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java
new file mode 100644
index 0000000..1092d0e
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/AbstractSectionParserTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.IItem;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link AbstractSectionParser}
+ */
+public class AbstractSectionParserTest extends TestCase {
+    AbstractSectionParser mParser = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mParser = new AbstractSectionParser() {
+            @Override
+            public IItem parse(List<String> lines) {
+                for (String line : lines) {
+                    parseLine(line);
+                }
+                commit();
+                return null;
+            }
+        };
+    }
+
+    private static class FakeBlockParser implements IParser {
+        private String mExpected = null;
+        private int mCalls = 0;
+
+        public FakeBlockParser(String expected) {
+            mExpected = expected;
+        }
+
+        public int getCalls() {
+            return mCalls;
+        }
+
+        @Override
+        public IItem parse(List<String> input) {
+            assertEquals(1, input.size());
+            assertEquals("parseBlock() got unexpected input!", mExpected, input.get(0));
+            mCalls += 1;
+            return null;
+        }
+    }
+
+    /**
+     * Verifies that {@link AbstractSectionParser} switches between parsers as expected
+     */
+    public void testSwitchParsers() {
+        final String lineFormat = "howdy, parser %d!";
+        final String linePattern = "I spy %d candles";
+        final int nParsers = 4;
+        FakeBlockParser[] parsers = new FakeBlockParser[nParsers];
+        final List<String> lines = new ArrayList<String>(2*nParsers);
+
+        for (int i = 0; i < nParsers; ++i) {
+            String line = String.format(lineFormat, i);
+            FakeBlockParser parser = new FakeBlockParser(line);
+            mParser.addSectionParser(parser, String.format(linePattern, i));
+            parsers[i] = parser;
+
+            // add the parser trigger
+            lines.add(String.format(linePattern, i));
+            // and then add the line that the parser is expecting
+            lines.add(String.format(lineFormat, i));
+        }
+
+        mParser.parse(lines);
+
+        // Verify that all the parsers were run
+        for (int i = 0; i < nParsers; ++i) {
+            assertEquals(String.format("Parser %d has wrong call count!", i), 1,
+                    parsers[i].getCalls());
+        }
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/AnrParserTest.java b/tests/src/com/android/loganalysis/parser/AnrParserTest.java
new file mode 100644
index 0000000..119f3a8
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/AnrParserTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link AnrParser}.
+ */
+public class AnrParserTest extends TestCase {
+
+    /**
+     * Test that ANRs are parsed for the header "ANR (application not responding) in process: app"
+     */
+    public void testParse_application_not_responding() {
+        List<String> lines = Arrays.asList(
+                "ANR (application not responding) in process: com.android.package",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+
+    /**
+     * Test that ANRs are parsed for the header "ANR in app"
+     */
+    public void testParse_anr_in_app() {
+        List<String> lines = Arrays.asList(
+                "ANR in com.android.package",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+
+    /**
+     * Test that ANRs are parsed for the header "ANR in app (class/package)"
+     */
+    public void testParse_anr_in_app_class_package() {
+        List<String> lines = Arrays.asList(
+                "ANR in com.android.package (com.android.package/.Activity)",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 0.71 / 0.83 / 0.51",
+                "CPU usage from 4357ms to -1434ms ago:",
+                "  22% 3378/com.android.package: 19% user + 3.6% kernel / faults: 73 minor 1 major",
+                "  16% 312/system_server: 12% user + 4.1% kernel / faults: 1082 minor 6 major",
+                "33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "CPU usage from 907ms to 1431ms later:",
+                "  14% 121/mediaserver: 11% user + 3.7% kernel / faults: 17 minor",
+                "    3.7% 183/AudioOut_2: 3.7% user + 0% kernel",
+                "  12% 312/system_server: 5.5% user + 7.4% kernel / faults: 6 minor",
+                "    5.5% 366/InputDispatcher: 0% user + 5.5% kernel",
+                "18% TOTAL: 11% user + 7.5% kernel");
+
+        AnrItem anr = new AnrParser().parse(lines);
+        assertNotNull(anr);
+        assertEquals("com.android.package", anr.getApp());
+        assertEquals("keyDispatchingTimedOut", anr.getReason());
+        assertEquals(0.71, anr.getLoad(AnrItem.LoadCategory.LOAD_1));
+        assertEquals(0.83, anr.getLoad(AnrItem.LoadCategory.LOAD_5));
+        assertEquals(0.51, anr.getLoad(AnrItem.LoadCategory.LOAD_15));
+        assertEquals(33.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.TOTAL));
+        assertEquals(21.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.USER));
+        assertEquals(11.0, anr.getCpuUsage(AnrItem.CpuUsageCategory.KERNEL));
+        assertEquals(0.3, anr.getCpuUsage(AnrItem.CpuUsageCategory.IOWAIT));
+        assertEquals(ArrayUtil.join("\n", lines), anr.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java b/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java
new file mode 100644
index 0000000..2a456d2
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/BugreportParserFuncTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.BugreportItem;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link BugreportParser}
+ */
+public class BugreportParserFuncTest extends TestCase {
+    // FIXME: Make bugreport file configurable.
+    private static final String BUGREPORT_PATH = "/tmp/bugreport.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a bugreport. The purpose of this is to
+     * assist a developer in checking why a given bugreport file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader bugreportReader = null;
+        try {
+            bugreportReader = new BufferedReader(new FileReader(BUGREPORT_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", BUGREPORT_PATH));
+        }
+        BugreportItem bugreport = null;
+        try {
+            long start = System.currentTimeMillis();
+            bugreport = new BugreportParser().parse(bugreportReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Bugreport took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (bugreportReader != null) {
+                try {
+                    bugreportReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        assertNotNull(bugreport);
+        assertNotNull(bugreport.getTime());
+
+        assertNotNull(bugreport.getSystemProps());
+        assertTrue(bugreport.getSystemProps().size() > 0);
+
+        assertNotNull(bugreport.getMemInfo());
+        assertTrue(bugreport.getMemInfo().size() > 0);
+
+        assertNotNull(bugreport.getProcrank());
+        assertTrue(bugreport.getProcrank().getPids().size() > 0);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertNotNull(bugreport.getSystemLog().getStartTime());
+        assertNotNull(bugreport.getSystemLog().getStopTime());
+
+        System.out.println(String.format("Stats for bugreport:\n" +
+                "  Time: %s\n" +
+                "  System Properties: %d items\n" +
+                "  Mem info: %d items\n" +
+                "  Procrank: %d items\n" +
+                "  System Log:\n" +
+                "    Start time: %s\n" +
+                "    Stop time: %s\n" +
+                "    %d ANR(s), %d Java Crash(es), %d Native Crash(es)",
+                bugreport.getTime(),
+                bugreport.getSystemProps().size(),
+                bugreport.getMemInfo().size(),
+                bugreport.getProcrank().getPids().size(),
+                bugreport.getSystemLog().getStartTime().toString(),
+                bugreport.getSystemLog().getStopTime().toString(),
+                bugreport.getSystemLog().getAnrs().size(),
+                bugreport.getSystemLog().getJavaCrashes().size(),
+                bugreport.getSystemLog().getNativeCrashes().size()));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/BugreportParserTest.java b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
new file mode 100644
index 0000000..b5f973e
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/BugreportParserTest.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.BugreportItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BugreportParser}
+ */
+public class BugreportParserTest extends TestCase {
+
+    /**
+     * Test that a bugreport can be parsed.
+     */
+    public void testParse() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SECTION ------",
+                "",
+                "------ MEMORY INFO (/proc/meminfo) ------",
+                "MemTotal:         353332 kB",
+                "MemFree:           65420 kB",
+                "Buffers:           20800 kB",
+                "Cached:            86204 kB",
+                "SwapCached:            0 kB",
+                "",
+                "------ PROCRANK (procrank) ------",
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                "  178   87136K   81684K   52829K   50012K  system_server",
+                " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps",
+                " 3247   61652K   61492K   33122K   30972K  com.android.browser",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]",
+                "",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "",
+                "------ SYSTEM PROPERTIES ------",
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[dalvik.vm.heapgrowthlimit]: [48m]",
+                "[dalvik.vm.heapsize]: [256m]",
+                "[gsm.version.ril-impl]: [android moto-ril-multimode 1.0]",
+                "",
+                "------ SECTION ------",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:17:08) ------",
+                "",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "",
+                "------ SECTION ------",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport);
+        assertEquals(parseTime("2012-04-25 20:45:10.000"), bugreport.getTime());
+
+        assertNotNull(bugreport.getMemInfo());
+        assertEquals(5, bugreport.getMemInfo().size());
+
+        assertNotNull(bugreport.getProcrank());
+        assertEquals(3, bugreport.getProcrank().getPids().size());
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), bugreport.getSystemLog().getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), bugreport.getSystemLog().getStopTime());
+        assertEquals(3, bugreport.getSystemLog().getEvents().size());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertNotNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+
+        assertNotNull(bugreport.getSystemProps());
+        assertEquals(4, bugreport.getSystemProps().size());
+    }
+
+    /**
+     * Test that the logcat year is set correctly from the bugreport timestamp.
+     */
+    public void testParse_set_logcat_year() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 1999-01-01 02:03:04",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "01-01 01:02:03.000     1     1 I TAG     : message",
+                "01-01 01:02:04.000     1     1 I TAG     : message",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport);
+        assertEquals(parseTime("1999-01-01 02:03:04.000"), bugreport.getTime());
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(parseTime("1999-01-01 01:02:03.000"), bugreport.getSystemLog().getStartTime());
+        assertEquals(parseTime("1999-01-01 01:02:04.000"), bugreport.getSystemLog().getStopTime());
+    }
+
+    /**
+     * Test that the trace is set correctly if there is only one ANR.
+     */
+    public void testSetAnrTrace_single() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:17:08) ------",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                bugreport.getSystemLog().getAnrs().get(0).getTrace());
+    }
+
+    /**
+     * Test that the trace is set correctly if there are multiple ANRs.
+     */
+    public void testSetAnrTrace_multiple() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:18:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:18:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:18:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:18:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:19:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.different.pacakge",
+                "04-25 17:19:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:19:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:19:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "------ VM TRACES AT LAST ANR (/data/anr/traces.txt: 2012-04-25 17:18:08) ------",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(3, bugreport.getSystemLog().getAnrs().size());
+        assertNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                bugreport.getSystemLog().getAnrs().get(1).getTrace());
+        assertNull(bugreport.getSystemLog().getAnrs().get(2).getTrace());
+    }
+
+    /**
+     * Test that the trace is set correctly if there is not traces file.
+     */
+    public void testSetAnrTrace_no_traces() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "",
+                "*** NO ANR VM TRACES FILE (/data/anr/traces.txt): No such file or directory",
+                "");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(1, bugreport.getSystemLog().getAnrs().size());
+        assertNull(bugreport.getSystemLog().getAnrs().get(0).getTrace());
+    }
+
+    /**
+     * Test that app names from logcat events are populated by matching the logcat PIDs with the
+     * PIDs from the logcat.
+     */
+    public void testSetAppsFromProcrank() {
+        List<String> lines = Arrays.asList(
+                "========================================================",
+                "== dumpstate: 2012-04-25 20:45:10",
+                "========================================================",
+                "------ PROCRANK (procrank) ------",
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                " 3064   87136K   81684K   52829K   50012K  com.android.package",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]",
+                "------ SYSTEM LOG (logcat -v threadtime -d *:v) ------",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3083 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+        assertEquals(2, bugreport.getSystemLog().getJavaCrashes().size());
+        assertEquals("com.android.package",
+                bugreport.getSystemLog().getJavaCrashes().get(0).getApp());
+        assertNull(bugreport.getSystemLog().getJavaCrashes().get(1).getApp());
+    }
+
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        return formatter.parse(timeStr);
+    }
+
+    /**
+     * Some Android devices refer to SYSTEM LOG as MAIN LOG. Check that parser recognizes this
+     * alternate syntax.
+     */
+    public void testSystemLogAsMainLog() {
+        List<String> lines = Arrays.asList(
+                "------ MAIN LOG (logcat -b main -b system -v threadtime -d *:v) ------",
+                "--------- beginning of /dev/log/system",
+                "12-11 19:48:07.945  1484  1508 D BatteryService: update start");
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+    }
+
+    /**
+     * Some Android devices refer to SYSTEM LOG as MAIN AND SYSTEM LOG. Check that parser
+     * recognizes this alternate syntax.
+     */
+    public void testSystemAndMainLog() {
+        List<String> lines = Arrays.asList(
+                "------ MAIN AND SYSTEM LOG (logcat -b main -b system -v threadtime -d *:v) ------",
+                "--------- beginning of /dev/log/system",
+                "12-17 15:15:12.877  1994  2019 D UiModeManager: updateConfigurationLocked: ");
+        BugreportItem bugreport = new BugreportParser().parse(lines);
+        assertNotNull(bugreport.getSystemLog());
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java b/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java
new file mode 100644
index 0000000..723633f
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/JavaCrashParserTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link JavaCrashParser}.
+ */
+public class JavaCrashParserTest extends TestCase {
+
+    /**
+     * Test that Java crashes are parsed with no message.
+     */
+    public void testParse_no_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertNull(jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that Java crashes are parsed with a message.
+     */
+    public void testParse_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that Java crashes are parsed if the message spans multiple lines.
+     */
+    public void testParse_multiline_message() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This message",
+                "is many lines",
+                "long.",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This message\nis many lines\nlong.", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that caused by sections of Java crashes are parsed, with no message or single or
+     * multiline messages.
+     */
+    public void testParse_caused_by() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)",
+                "Caused by: java.lang.Exception",
+                "\tat class.method4(Class.java:4)",
+                "Caused by: java.lang.Exception: This is the caused by message",
+                "\tat class.method5(Class.java:5)",
+                "Caused by: java.lang.Exception: This is a multiline",
+                "caused by message",
+                "\tat class.method6(Class.java:6)");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines), jc.getStack());
+    }
+
+    /**
+     * Test that the Java crash is cutoff if an unexpected line is handled.
+     */
+    public void testParse_cutoff() {
+        List<String> lines = Arrays.asList(
+                "java.lang.Exception: This is the message",
+                "\tat class.method1(Class.java:1)",
+                "\tat class.method2(Class.java:2)",
+                "\tat class.method3(Class.java:3)",
+                "Invalid line",
+                "java.lang.Exception: This is the message");
+
+        JavaCrashItem jc = new JavaCrashParser().parse(lines);
+        assertNotNull(jc);
+        assertEquals("java.lang.Exception", jc.getException());
+        assertEquals("This is the message", jc.getMessage());
+        assertEquals(ArrayUtil.join("\n", lines.subList(0, lines.size()-2)), jc.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java b/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java
new file mode 100644
index 0000000..7d63b19
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/LogcatParserFuncTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.LogcatItem;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link LogcatParser}
+ */
+public class LogcatParserFuncTest extends TestCase {
+    // FIXME: Make logcat file configurable.
+    private static final String LOGCAT_PATH = "/tmp/logcat.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a logcat. The purpose of this is to
+     * assist a developer in checking why a given logcat file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader logcatReader = null;
+        try {
+            logcatReader = new BufferedReader(new FileReader(LOGCAT_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", LOGCAT_PATH));
+        }
+        LogcatItem logcat = null;
+        try {
+            long start = System.currentTimeMillis();
+            logcat = new LogcatParser().parse(logcatReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Logcat took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (logcatReader != null) {
+                try {
+                    logcatReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }        }
+
+        assertNotNull(logcat);
+        assertNotNull(logcat.getStartTime());
+        assertNotNull(logcat.getStopTime());
+
+        System.out.println(String.format("Stats for logcat:\n" +
+                "  Start time: %s\n" +
+                "  Stop time: %s\n" +
+                "  %d ANR(s), %d Java Crash(es), %d Native Crash(es)",
+                logcat.getStartTime().toString(),
+                logcat.getStopTime().toString(),
+                logcat.getAnrs().size(),
+                logcat.getJavaCrashes().size(),
+                logcat.getNativeCrashes().size()));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/LogcatParserTest.java b/tests/src/com/android/loganalysis/parser/LogcatParserTest.java
new file mode 100644
index 0000000..37a39b6
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/LogcatParserTest.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.LogcatItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link LogcatParserTest}.
+ */
+public class LogcatParserTest extends TestCase {
+
+    /**
+     * Test that an ANR is parsed in the log.
+     */
+    public void testParse_anr() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getAnrs().size());
+        assertEquals(312, logcat.getAnrs().get(0).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(0).getTid().intValue());
+        assertEquals("", logcat.getAnrs().get(0).getLastPreamble());
+        assertEquals("", logcat.getAnrs().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(0).getEventTime());
+    }
+
+    /**
+     * Test that Java crashes can be parsed.
+     */
+    public void testParse_java_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals("", logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals("", logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that native crashes can be parsed.
+     */
+    public void testParse_native_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getNativeCrashes().size());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals("", logcat.getNativeCrashes().get(0).getLastPreamble());
+        assertEquals("", logcat.getNativeCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that multiple events can be parsed.
+     */
+    public void testParse_multiple_events() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 17:17:08.445   312   366 E ActivityManager: ANR (application not responding) in process: com.android.package",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Reason: keyDispatchingTimedOut",
+                "04-25 17:17:08.445   312   366 E ActivityManager: Load: 0.71 / 0.83 / 0.51",
+                "04-25 17:17:08.445   312   366 E ActivityManager: 33% TOTAL: 21% user + 11% kernel + 0.3% iowait",
+                "04-25 18:33:27.273   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "04-25 18:33:27.273   117   117 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 18:33:27.273   117   117 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 18:33:27.273   117   117 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 18:33:27.273   117   117 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"), logcat.getStopTime());
+        assertEquals(6, logcat.getEvents().size());
+        assertEquals(2, logcat.getAnrs().size());
+        assertEquals(2, logcat.getJavaCrashes().size());
+        assertEquals(2, logcat.getNativeCrashes().size());
+
+        assertEquals(312, logcat.getAnrs().get(0).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(0).getEventTime());
+
+        assertEquals(312, logcat.getAnrs().get(1).getPid().intValue());
+        assertEquals(366, logcat.getAnrs().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 17:17:08.445"), logcat.getAnrs().get(1).getEventTime());
+
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+
+        assertEquals(3065, logcat.getJavaCrashes().get(1).getPid().intValue());
+        assertEquals(3090, logcat.getJavaCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(1).getEventTime());
+
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+
+        assertEquals(117, logcat.getNativeCrashes().get(1).getPid().intValue());
+        assertEquals(117, logcat.getNativeCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 18:33:27.273"),
+                logcat.getNativeCrashes().get(1).getEventTime());
+    }
+
+    /**
+     * Test that multiple java crashes and native crashes can be parsed even when interleaved.
+     */
+    public void testParse_multiple_events_interleaved() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799   115   115 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 09:55:47.799   117   117 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 09:55:47.799   117   117 I DEBUG   : Build fingerprint: 'product:build:target'",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 09:55:47.799   117   117 I DEBUG   : pid: 3112, tid: 3112  >>> com.google.android.browser <<<",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799  3065  3090 E AndroidRuntime: \tat class.method3(Class.java:3)",
+                "04-25 09:55:47.799   115   115 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                "04-25 09:55:47.799   117   117 I DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(4, logcat.getEvents().size());
+        assertEquals(0, logcat.getAnrs().size());
+        assertEquals(2, logcat.getJavaCrashes().size());
+        assertEquals(2, logcat.getNativeCrashes().size());
+
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+
+        assertEquals(3065, logcat.getJavaCrashes().get(1).getPid().intValue());
+        assertEquals(3090, logcat.getJavaCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(1).getEventTime());
+
+        assertEquals(115, logcat.getNativeCrashes().get(0).getPid().intValue());
+        assertEquals(115, logcat.getNativeCrashes().get(0).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getNativeCrashes().get(0).getEventTime());
+
+        assertEquals(117, logcat.getNativeCrashes().get(1).getPid().intValue());
+        assertEquals(117, logcat.getNativeCrashes().get(1).getTid().intValue());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getNativeCrashes().get(1).getEventTime());
+    }
+
+    /**
+     * Test that the preambles are set correctly if there's only partial preambles.
+     */
+    public void testParse_partial_preambles() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:15:47.799   123  3082 I tag: message 1",
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:25:47.799   345  3082 I tag: message 3",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:35:47.799   456  3082 I tag: message 5",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:45:47.799   567  3082 I tag: message 7",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        List<String> expectedLastPreamble = Arrays.asList(
+                "04-25 09:15:47.799   123  3082 I tag: message 1",
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:25:47.799   345  3082 I tag: message 3",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:35:47.799   456  3082 I tag: message 5",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:45:47.799   567  3082 I tag: message 7",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8");
+
+        List<String> expectedProcPreamble = Arrays.asList(
+                "04-25 09:20:47.799  3064  3082 I tag: message 2",
+                "04-25 09:30:47.799  3064  3082 I tag: message 4",
+                "04-25 09:40:47.799  3064  3082 I tag: message 6",
+                "04-25 09:50:47.799  3064  3082 I tag: message 8");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:15:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(ArrayUtil.join("\n", expectedLastPreamble),
+                logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals(ArrayUtil.join("\n", expectedProcPreamble),
+                logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that the preambles are set correctly if there's only full preambles.
+     */
+    public void testParse_preambles() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:43:47.799  3064  3082 I tag: message 1",
+                "04-25 09:44:47.799   123  3082 I tag: message 2",
+                "04-25 09:45:47.799  3064  3082 I tag: message 3",
+                "04-25 09:46:47.799   234  3082 I tag: message 4",
+                "04-25 09:47:47.799  3064  3082 I tag: message 5",
+                "04-25 09:48:47.799   345  3082 I tag: message 6",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7",
+                "04-25 09:50:47.799   456  3082 I tag: message 8",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: java.lang.Exception",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  3064  3082 E AndroidRuntime: \tat class.method3(Class.java:3)");
+
+        List<String> expectedLastPreamble = Arrays.asList(
+                "04-25 09:48:47.799   345  3082 I tag: message 6",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7",
+                "04-25 09:50:47.799   456  3082 I tag: message 8");
+
+        List<String> expectedProcPreamble = Arrays.asList(
+                "04-25 09:45:47.799  3064  3082 I tag: message 3",
+                "04-25 09:47:47.799  3064  3082 I tag: message 5",
+                "04-25 09:49:47.799  3064  3082 I tag: message 7");
+
+        LogcatItem logcat = new LogcatParser("2012") {
+            @Override
+            int getLastPreambleSize() {
+                return 3;
+            }
+
+            @Override
+            int getProcPreambleSize() {
+                return 3;
+            }
+        }.parse(lines);
+
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:43:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertEquals(3082, logcat.getJavaCrashes().get(0).getTid().intValue());
+        assertEquals(ArrayUtil.join("\n", expectedLastPreamble),
+                logcat.getJavaCrashes().get(0).getLastPreamble());
+        assertEquals(ArrayUtil.join("\n", expectedProcPreamble),
+                logcat.getJavaCrashes().get(0).getProcessPreamble());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    /**
+     * Test that the time logcat format can be parsed.
+     */
+    public void testParse_time() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): java.lang.Exception",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method1(Class.java:1)",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method2(Class.java:2)",
+                "04-25 09:55:47.799  E/AndroidRuntime(3064): \tat class.method3(Class.java:3)");
+
+        LogcatItem logcat = new LogcatParser("2012").parse(lines);
+        assertNotNull(logcat);
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStartTime());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"), logcat.getStopTime());
+        assertEquals(1, logcat.getEvents().size());
+        assertEquals(1, logcat.getJavaCrashes().size());
+        assertEquals(3064, logcat.getJavaCrashes().get(0).getPid().intValue());
+        assertNull(logcat.getJavaCrashes().get(0).getTid());
+        assertEquals(parseTime("2012-04-25 09:55:47.799"),
+                logcat.getJavaCrashes().get(0).getEventTime());
+    }
+
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        return formatter.parse(timeStr);
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java b/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java
new file mode 100644
index 0000000..2069d12
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MemInfoParserTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.MemInfoItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link MemInfoParser}
+ */
+public class MemInfoParserTest extends TestCase {
+    public void testMemInfoParser() {
+        List<String> inputBlock = Arrays.asList(
+                "MemTotal:         353332 kB",
+                "MemFree:           65420 kB",
+                "Buffers:           20800 kB",
+                "Cached:            86204 kB",
+                "SwapCached:            0 kB");
+        MemInfoParser parser = new MemInfoParser();
+        MemInfoItem output = parser.parse(inputBlock);
+
+        assertEquals(5, output.size());
+        assertEquals((Integer)353332, output.get("MemTotal"));
+        assertEquals((Integer)65420, output.get("MemFree"));
+        assertEquals((Integer)20800, output.get("Buffers"));
+        assertEquals((Integer)86204, output.get("Cached"));
+        assertEquals((Integer)0, output.get("SwapCached"));
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java b/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java
new file mode 100644
index 0000000..58fcebe
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MonkeyLogParserFuncTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Functional tests for {@link MonkeyLogParser}
+ */
+public class MonkeyLogParserFuncTest extends TestCase {
+    // FIXME: Make monkey log file configurable.
+    private static final String MONKEY_LOG_PATH = "/tmp/monkey_log.txt";
+
+    /**
+     * A test that is intended to force Brillopad to parse a monkey log. The purpose of this is to
+     * assist a developer in checking why a given monkey log file might not be parsed correctly by
+     * Brillopad.
+     */
+    public void testParse() {
+        BufferedReader monkeyLogReader = null;
+        try {
+            monkeyLogReader = new BufferedReader(new FileReader(MONKEY_LOG_PATH));
+        } catch (FileNotFoundException e) {
+            fail(String.format("File not found at %s", MONKEY_LOG_PATH));
+        }
+        MonkeyLogItem monkeyLog = null;
+        try {
+            long start = System.currentTimeMillis();
+            monkeyLog = new MonkeyLogParser().parse(monkeyLogReader);
+            long stop = System.currentTimeMillis();
+            System.out.println(String.format("Monkey log took %d ms to parse.", stop - start));
+        } catch (IOException e) {
+            fail(String.format("IOException: %s", e.toString()));
+        } finally {
+            if (monkeyLogReader != null) {
+                try {
+                    monkeyLogReader.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }        }
+
+        assertNotNull(monkeyLog);
+        assertNotNull(monkeyLog.getStartTime());
+        assertNotNull(monkeyLog.getStopTime());
+        assertNotNull(monkeyLog.getTargetCount());
+        assertNotNull(monkeyLog.getThrottle());
+        assertNotNull(monkeyLog.getSeed());
+        assertNotNull(monkeyLog.getIgnoreSecurityExceptions());
+        assertTrue(monkeyLog.getPackages().size() > 0);
+        assertTrue(monkeyLog.getCategories().size() > 0);
+        assertNotNull(monkeyLog.getIsFinished());
+        assertNotNull(monkeyLog.getIntermediateCount());
+        assertNotNull(monkeyLog.getTotalDuration());
+        assertNotNull(monkeyLog.getStartUptimeDuration());
+        assertNotNull(monkeyLog.getStopUptimeDuration());
+
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("Stats for monkey log:\n");
+        sb.append(String.format("  Start time: %s\n", monkeyLog.getStartTime()));
+        sb.append(String.format("  Stop time: %s\n", monkeyLog.getStopTime()));
+        sb.append(String.format("  Parameters: target-count=%d, throttle=%d, seed=%d, " +
+                "ignore-security-exceptions=%b\n",
+                monkeyLog.getTargetCount(), monkeyLog.getThrottle(), monkeyLog.getSeed(),
+                monkeyLog.getIgnoreSecurityExceptions()));
+        sb.append(String.format("  Packages: %s\n", monkeyLog.getPackages()));
+        sb.append(String.format("  Categories: %s\n", monkeyLog.getCategories()));
+        if (monkeyLog.getNoActivities()) {
+            sb.append("  Status: no-activities=true\n");
+        } else {
+            sb.append(String.format("  Status: finished=%b, final-count=%d, " +
+                    "intermediate-count=%d\n", monkeyLog.getIsFinished(), monkeyLog.getFinalCount(),
+                    monkeyLog.getIntermediateCount()));
+
+            sb.append("  Dropped events:");
+            for (DroppedCategory drop : DroppedCategory.values()) {
+                sb.append(String.format(" %s=%d,", drop.toString(),
+                        monkeyLog.getDroppedCount(drop)));
+            }
+            sb.deleteCharAt(sb.length()-1);
+            sb.append("\n");
+        }
+        sb.append(String.format("  Run time: duration=%d ms, delta-uptime=%d (%d - %d) ms\n",
+                monkeyLog.getTotalDuration(),
+                monkeyLog.getStopUptimeDuration() - monkeyLog.getStartUptimeDuration(),
+                monkeyLog.getStopUptimeDuration(), monkeyLog.getStartUptimeDuration()));
+
+        if (monkeyLog.getCrash() != null && monkeyLog.getCrash() instanceof AnrItem) {
+            sb.append(String.format("  Stopped due to ANR\n"));
+        }
+        if (monkeyLog.getCrash() != null && monkeyLog.getCrash() instanceof JavaCrashItem) {
+            sb.append(String.format("  Stopped due to Java crash\n"));
+        }
+        System.out.println(sb.toString());
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java b/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java
new file mode 100644
index 0000000..d5aef86
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/MonkeyLogParserTest.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.AnrItem;
+import com.android.loganalysis.item.JavaCrashItem;
+import com.android.loganalysis.item.MonkeyLogItem;
+import com.android.loganalysis.item.MonkeyLogItem.DroppedCategory;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Unit tests for {@link MonkeyLogParser}
+ */
+public class MonkeyLogParserTest extends TestCase {
+
+    /**
+     * Test that a monkey can be parsed if there are no crashes.
+     */
+    public void testParse_success() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Wednesday, 04/25/2012 01:37:12 AM - device uptime = 242.13: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.browser  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 528 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=528 count=10000",
+                ":AllowPackage: com.google.android.browser",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.browser/com.android.browser.BrowserActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.browser/com.android.browser.BrowserActivity } in package com.google.android.browser",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_DOWN): 23    // KEYCODE_DPAD_CENTER",
+                ":Sending Key (ACTION_UP): 23    // KEYCODE_DPAD_CENTER",
+                "Sleeping for 100 milliseconds",
+                ":Sending Trackball (ACTION_MOVE): 0:(-5.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(3.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-1.0,3.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(4.0,-2.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(1.0,4.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-4.0,2.0)",
+                "    //[calendar_time:2012-04-25 01:42:20.140  system_uptime:535179]",
+                "    // Sending event #9900",
+                ":Sending Trackball (ACTION_MOVE): 0:(2.0,-4.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-2.0,0.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(2.0,2.0)",
+                ":Sending Trackball (ACTION_MOVE): 0:(-5.0,4.0)",
+                "Events injected: 10000",
+                ":Dropped: keys=5 pointers=6 trackballs=7 flips=8 rotations=9",
+                "// Monkey finished",
+                "",
+                "# Wednesday, 04/25/2012 01:42:09 AM - device uptime = 539.21: Monkey command ran for: 04:57 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-25 01:37:12"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-25 01:42:09"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.browser"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(528, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 57 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(242130, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(539210, monkeyLog.getStopUptimeDuration().longValue());
+        assertTrue(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(9900, monkeyLog.getIntermediateCount());
+        assertEquals(10000, monkeyLog.getFinalCount().intValue());
+        assertEquals(5, monkeyLog.getDroppedCount(DroppedCategory.KEYS).intValue());
+        assertEquals(6, monkeyLog.getDroppedCount(DroppedCategory.POINTERS).intValue());
+        assertEquals(7, monkeyLog.getDroppedCount(DroppedCategory.TRACKBALLS).intValue());
+        assertEquals(8, monkeyLog.getDroppedCount(DroppedCategory.FLIPS).intValue());
+        assertEquals(9, monkeyLog.getDroppedCount(DroppedCategory.ROTATIONS).intValue());
+        assertNull(monkeyLog.getCrash());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there is an ANR.
+     */
+    public void testParse_anr() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tuesday, 04/24/2012 05:23:30 PM - device uptime = 216.48: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.youtube  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 993 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=993 count=10000",
+                ":AllowPackage: com.google.android.youtube",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.youtube/.app.honeycomb.Shell%24HomeActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.youtube/.app.honeycomb.Shell$HomeActivity } in package com.google.android.youtube",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_UP): 21    // KEYCODE_DPAD_LEFT",
+                "Sleeping for 100 milliseconds",
+                ":Sending Key (ACTION_DOWN): 22    // KEYCODE_DPAD_RIGHT",
+                ":Sending Key (ACTION_UP): 22    // KEYCODE_DPAD_RIGHT",
+                "    //[calendar_time:2012-04-25 00:27:27.155  system_uptime:454996]",
+                "    // Sending event #5300",
+                ":Sending Key (ACTION_UP): 19    // KEYCODE_DPAD_UP",
+                "Sleeping for 100 milliseconds",
+                ":Sending Trackball (ACTION_MOVE): 0:(4.0,3.0)",
+                ":Sending Key (ACTION_DOWN): 20    // KEYCODE_DPAD_DOWN",
+                ":Sending Key (ACTION_UP): 20    // KEYCODE_DPAD_DOWN",
+                "// NOT RESPONDING: com.google.android.youtube (pid 3301)",
+                "ANR in com.google.android.youtube (com.google.android.youtube/.app.honeycomb.phone.WatchActivity)",
+                "Reason: keyDispatchingTimedOut",
+                "Load: 1.0 / 1.05 / 0.6",
+                "CPU usage from 4794ms to -1502ms ago with 99% awake:",
+                "  18% 3301/com.google.android.youtube: 16% user + 2.3% kernel / faults: 268 minor 9 major",
+                "  13% 313/system_server: 9.2% user + 4.4% kernel / faults: 906 minor 3 major",
+                "  10% 117/surfaceflinger: 4.9% user + 5.5% kernel / faults: 1 minor",
+                "  10% 120/mediaserver: 6.8% user + 3.6% kernel / faults: 1189 minor",
+                "34% TOTAL: 19% user + 13% kernel + 0.2% iowait + 1% softirq",
+                "",
+                "procrank:",
+                "// procrank status was 0",
+                "anr traces:",
+                "",
+                "",
+                "----- pid 2887 at 2012-04-25 17:17:08 -----",
+                "Cmd line: com.google.android.youtube",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 2887 -----",
+                "// anr traces status was 0",
+                "** Monkey aborted due to error.",
+                "Events injected: 5322",
+                ":Sending rotation degree=0, persist=false",
+                ":Dropped: keys=1 pointers=0 trackballs=0 flips=0 rotations=0",
+                "## Network stats: elapsed time=252942ms (0ms mobile, 252942ms wifi, 0ms not connected)",
+                "** System appears to have crashed at event 5322 of 10000 using seed 993",
+                "",
+                "# Tuesday, 04/24/2012 05:27:44 PM - device uptime = 471.37: Monkey command ran for: 04:14 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:23:30"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:27:44"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.youtube"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(993, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 14 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(216480, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(471370, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(5300, monkeyLog.getIntermediateCount());
+        assertEquals(5322, monkeyLog.getFinalCount().intValue());
+        assertNotNull(monkeyLog.getCrash());
+        assertTrue(monkeyLog.getCrash() instanceof AnrItem);
+        assertEquals("com.google.android.youtube", monkeyLog.getCrash().getApp());
+        assertEquals(3301, monkeyLog.getCrash().getPid().intValue());
+        assertEquals("keyDispatchingTimedOut", ((AnrItem) monkeyLog.getCrash()).getReason());
+        assertEquals(ArrayUtil.join("\n", expectedStack),
+                ((AnrItem) monkeyLog.getCrash()).getTrace());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there is a JavaCrash.
+     */
+    public void testParse_java_crash() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tuesday, 04/24/2012 05:05:50 PM - device uptime = 232.65: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.apps.maps  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 501 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=501 count=10000",
+                ":AllowPackage: com.google.android.apps.maps",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                ":Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.apps.maps/com.google.android.maps.LatitudeActivity;end",
+                "    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.apps.maps/com.google.android.maps.LatitudeActivity } in package com.google.android.apps.maps",
+                "Sleeping for 100 milliseconds",
+                ":Sending Touch (ACTION_DOWN): 0:(332.0,70.0)",
+                ":Sending Touch (ACTION_UP): 0:(332.55292,76.54678)",
+                "    //[calendar_time:2012-04-25 00:06:38.419  system_uptime:280799]",
+                "    // Sending event #1600",
+                ":Sending Touch (ACTION_MOVE): 0:(1052.2666,677.64594)",
+                ":Sending Touch (ACTION_UP): 0:(1054.7593,687.3757)",
+                "Sleeping for 100 milliseconds",
+                "// CRASH: com.google.android.apps.maps (pid 3161)",
+                "// Short Msg: java.lang.Exception",
+                "// Long Msg: java.lang.Exception: This is the message",
+                "// Build Label: google/yakju/maguro:JellyBean/JRN24B/338896:userdebug/dev-keys",
+                "// Build Changelist: 338896",
+                "// Build Time: 1335309051000",
+                "// java.lang.Exception: This is the message",
+                "// \tat class.method1(Class.java:1)",
+                "// \tat class.method2(Class.java:2)",
+                "// \tat class.method3(Class.java:3)",
+                "// ",
+                "** Monkey aborted due to error.",
+                "Events injected: 1649",
+                ":Sending rotation degree=0, persist=false",
+                ":Dropped: keys=0 pointers=0 trackballs=0 flips=0 rotations=0",
+                "## Network stats: elapsed time=48897ms (0ms mobile, 48897ms wifi, 0ms not connected)",
+                "** System appears to have crashed at event 1649 of 10000 using seed 501",
+                "",
+                "# Tuesday, 04/24/2012 05:06:40 PM - device uptime = 282.53: Monkey command ran for: 00:49 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:05:50"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:06:40"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.apps.maps"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(501, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(49 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(232650, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(282530, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertFalse(monkeyLog.getNoActivities());
+        assertEquals(1600, monkeyLog.getIntermediateCount());
+        assertEquals(1649, monkeyLog.getFinalCount().intValue());
+        assertNotNull(monkeyLog.getCrash());
+        assertTrue(monkeyLog.getCrash() instanceof JavaCrashItem);
+        assertEquals("com.google.android.apps.maps", monkeyLog.getCrash().getApp());
+        assertEquals(3161, monkeyLog.getCrash().getPid().intValue());
+        assertEquals("java.lang.Exception", ((JavaCrashItem) monkeyLog.getCrash()).getException());
+    }
+
+    /**
+     * Test that a monkey can be parsed if there are no activities to run.
+     */
+    public void testParse_no_activities() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Wednesday, 04/25/2012 01:37:12 AM - device uptime = 242.13: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.browser  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 528 -v -v -v 10000 ",
+                "",
+                ":Monkey: seed=528 count=10000",
+                ":AllowPackage: com.google.android.browser",
+                ":IncludeCategory: android.intent.category.LAUNCHER",
+                "** No activities found to run, monkey aborted.",
+                "",
+                "# Wednesday, 04/25/2012 01:42:09 AM - device uptime = 539.21: Monkey command ran for: 04:57 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-25 01:37:12"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-25 01:42:09"), monkeyLog.getStopTime());
+        assertEquals(1, monkeyLog.getPackages().size());
+        assertTrue(monkeyLog.getPackages().contains("com.google.android.browser"));
+        assertEquals(1, monkeyLog.getCategories().size());
+        assertTrue(monkeyLog.getCategories().contains("android.intent.category.LAUNCHER"));
+        assertEquals(100, monkeyLog.getThrottle());
+        assertEquals(528, monkeyLog.getSeed().intValue());
+        assertEquals(10000, monkeyLog.getTargetCount().intValue());
+        assertTrue(monkeyLog.getIgnoreSecurityExceptions());
+        assertEquals(4 * 60 * 1000 + 57 * 1000, monkeyLog.getTotalDuration().longValue());
+        assertEquals(242130, monkeyLog.getStartUptimeDuration().longValue());
+        assertEquals(539210, monkeyLog.getStopUptimeDuration().longValue());
+        assertFalse(monkeyLog.getIsFinished());
+        assertTrue(monkeyLog.getNoActivities());
+        assertEquals(0, monkeyLog.getIntermediateCount());
+        assertNull(monkeyLog.getFinalCount());
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.KEYS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.POINTERS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.TRACKBALLS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.FLIPS));
+        assertNull(monkeyLog.getDroppedCount(DroppedCategory.ROTATIONS));
+        assertNull(monkeyLog.getCrash());
+    }
+
+    /**
+     * Test that the other date format can be parsed.
+     */
+    public void testAlternateDateFormat() throws ParseException {
+        List<String> lines = Arrays.asList(
+                "# Tue Apr 24 17:05:50 PST 2012 - device uptime = 232.65: Monkey command used for this test:",
+                "adb shell monkey -p com.google.android.apps.maps  -c android.intent.category.SAMPLE_CODE -c android.intent.category.CAR_DOCK -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.INFO  --ignore-security-exceptions --throttle 100  -s 501 -v -v -v 10000 ",
+                "",
+                "# Tue Apr 24 17:06:40 PST 2012 - device uptime = 282.53: Monkey command ran for: 00:49 (mm:ss)",
+                "",
+                "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
+                "");
+
+        MonkeyLogItem monkeyLog = new MonkeyLogParser().parse(lines);
+        assertNotNull(monkeyLog);
+        // FIXME: Add test back once time situation has been worked out.
+        // assertEquals(parseTime("2012-04-24 17:05:50"), monkeyLog.getStartTime());
+        // assertEquals(parseTime("2012-04-24 17:06:40"), monkeyLog.getStopTime());
+    }
+
+    @SuppressWarnings("unused")
+    private Date parseTime(String timeStr) throws ParseException {
+        DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        return formatter.parse(timeStr);
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java b/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java
new file mode 100644
index 0000000..fa63499
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/NativeCrashParserTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.NativeCrashItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link NativeCrashParser}.
+ */
+public class NativeCrashParserTest extends TestCase {
+
+    /**
+     * Test that native crashes are parsed.
+     */
+    public void testParseage() {
+        List<String> lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000",
+                " r0 00000000  r1 007d9064  r2 007d9063  r3 00000004",
+                " r4 006bf518  r5 0091e3b0  r6 00000000  r7 9e3779b9",
+                " r8 000006c1  r9 000006c3  10 00000000  fp 67d246c1",
+                " ip d2363b58  sp 50ed71d8  lr 4edfc89b  pc 4edfc6a0  cpsr 20000030",
+                " d0  00640065005f0065  d1  0072006f00740069",
+                " d2  00730075006e006b  d3  0066006900670000",
+                " d4  00e6d48800e6d3b8  d5  02d517a000e6d518",
+                " d6  0000270f02d51860  d7  0000000002d51a80",
+                " d8  41d3dc5261e7893b  d9  3fa999999999999a",
+                " d10 0000000000000000  d11 0000000000000000",
+                " d12 0000000000000000  d13 0000000000000000",
+                " d14 0000000000000000  d15 0000000000000000",
+                " d16 4070000000000000  d17 40c3878000000000",
+                " d18 412310f000000000  d19 3f91800dedacf040",
+                " d20 0000000000000000  d21 0000000000000000",
+                " d22 4010000000000000  d23 0000000000000000",
+                " d24 3ff0000000000000  d25 0000000000000000",
+                " d26 0000000000000000  d27 8000000000000000",
+                " d28 0000000000000000  d29 3ff0000000000000",
+                " d30 0000000000000000  d31 3ff0000000000000",
+                " scr 20000013",
+                "",
+                "         #00  pc 001236a0  /system/lib/libwebcore.so",
+                "         #01  pc 00123896  /system/lib/libwebcore.so",
+                "         #02  pc 00123932  /system/lib/libwebcore.so",
+                "         #03  pc 00123e3a  /system/lib/libwebcore.so",
+                "         #04  pc 00123e84  /system/lib/libwebcore.so",
+                "         #05  pc 003db92a  /system/lib/libwebcore.so",
+                "         #06  pc 003dd01c  /system/lib/libwebcore.so",
+                "         #07  pc 002ffb92  /system/lib/libwebcore.so",
+                "         #08  pc 0031c120  /system/lib/libwebcore.so",
+                "         #09  pc 0031c134  /system/lib/libwebcore.so",
+                "         #10  pc 0013fb98  /system/lib/libwebcore.so",
+                "         #11  pc 0015b026  /system/lib/libwebcore.so",
+                "         #12  pc 0015b164  /system/lib/libwebcore.so",
+                "         #13  pc 0015f4cc  /system/lib/libwebcore.so",
+                "         #14  pc 00170472  /system/lib/libwebcore.so",
+                "         #15  pc 0016ecb6  /system/lib/libwebcore.so",
+                "         #16  pc 0027120e  /system/lib/libwebcore.so",
+                "         #17  pc 0026efec  /system/lib/libwebcore.so",
+                "         #18  pc 0026fcd8  /system/lib/libwebcore.so",
+                "         #19  pc 00122efa  /system/lib/libwebcore.so",
+                "",
+                "code around pc:",
+                "4edfc680 4a14b5f7 0601f001 23000849 3004f88d  ...J....I..#...0",
+                "4edfc690 460a9200 3006f8ad e00e4603 3a019f00  ...F...0.F.....:",
+                "4edfc6a0 5c04f833 f83319ed 042c7c02 2cc7ea84  3..\\..3..|,....,",
+                "4edfc6b0 0405ea8c 24d4eb04 33049400 d1ed2a00  .......$...3.*..",
+                "4edfc6c0 f830b126 46681021 ff72f7ff f7ff4668  &.0.!.hF..r.hF..",
+                "",
+                "code around lr:",
+                "4edfc878 f9caf7ff 60209e03 9605e037 5b04f856  ...... `7...V..[",
+                "4edfc888 d0302d00 d13b1c6b 68a8e02d f7ff6869  .-0.k.;.-..hih..",
+                "4edfc898 6128fef3 b010f8d5 99022500 ea0146aa  ..(a.....%...F..",
+                "4edfc8a8 9b01080b 0788eb03 3028f853 b9bdb90b  ........S.(0....",
+                "4edfc8b8 3301e015 4638d005 f7ff9905 b970ff15  ...3..8F......p.",
+                "",
+                "stack:",
+                "    50ed7198  01d02c08  [heap]",
+                "    50ed719c  40045881  /system/lib/libc.so",
+                "    50ed71a0  400784c8",
+                "    50ed71a4  400784c8",
+                "    50ed71a8  02b40c68  [heap]",
+                "    50ed71ac  02b40c90  [heap]",
+                "    50ed71b0  50ed7290",
+                "    50ed71b4  006bf518  [heap]",
+                "    50ed71b8  00010000",
+                "    50ed71bc  50ed72a4",
+                "    50ed71c0  7da5a695",
+                "    50ed71c4  50ed7290",
+                "    50ed71c8  00000000",
+                "    50ed71cc  00000008",
+                "    50ed71d0  df0027ad",
+                "    50ed71d4  00000000",
+                "#00 50ed71d8  9e3779b9",
+                "    50ed71dc  00002000",
+                "    50ed71e0  00004000",
+                "    50ed71e4  006bf518  [heap]",
+                "    50ed71e8  0091e3b0  [heap]",
+                "    50ed71ec  01d72588  [heap]",
+                "    50ed71f0  00000000",
+                "    50ed71f4  4edfc89b  /system/lib/libwebcore.so",
+                "#01 50ed71f8  01d70a78  [heap]",
+                "    50ed71fc  02b6afa8  [heap]",
+                "    50ed7200  00003fff",
+                "    50ed7204  01d70a78  [heap]",
+                "    50ed7208  00004000",
+                "    50ed720c  01d72584  [heap]",
+                "    50ed7210  00000000",
+                "    50ed7214  00000006",
+                "    50ed7218  006bf518  [heap]",
+                "    50ed721c  50ed72a4",
+                "    50ed7220  7da5a695",
+                "    50ed7224  50ed7290",
+                "    50ed7228  000016b8",
+                "    50ed722c  00000008",
+                "    50ed7230  01d70a78  [heap]",
+                "    50ed7234  4edfc937  /system/lib/libwebcore.so",
+                "debuggerd committing suicide to free the zombie!",
+                "debuggerd");
+
+        NativeCrashItem nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+        assertEquals("google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys",
+                nc.getFingerprint());
+        assertEquals(ArrayUtil.join("\n", lines), nc.getStack());
+    }
+
+    /**
+     * Test that both types of native crash app lines are parsed.
+     */
+    public void testParseApp() {
+        List<String> lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        NativeCrashItem nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+
+        lines = Arrays.asList(
+                "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***",
+                "Build fingerprint: 'google/soju/crespo:4.0.4/IMM76D/299849:userdebug/test-keys'",
+                "pid: 2058, tid: 2523, name: com.google.android.browser  >>> com.google.android.browser <<<",
+                "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000");
+
+        nc = new NativeCrashParser().parse(lines);
+        assertNotNull(nc);
+        assertEquals("com.google.android.browser", nc.getApp());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java b/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java
new file mode 100644
index 0000000..a6c66b7
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/ProcrankParserTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.ProcrankItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ProcrankParser}
+ */
+public class ProcrankParserTest extends TestCase {
+    public void testProcRankParser() {
+        List<String> inputBlock = Arrays.asList(
+                "  PID      Vss      Rss      Pss      Uss  cmdline",
+                "  178   87136K   81684K   52829K   50012K  system_server",
+                " 1313   78128K   77996K   48603K   45812K  com.google.android.apps.maps",
+                " 3247   61652K   61492K   33122K   30972K  com.android.browser",
+                "  334   55740K   55572K   29629K   28360K  com.android.launcher",
+                " 2072   51348K   51172K   24263K   22812K  android.process.acore",
+                " 1236   51440K   51312K   22911K   20608K  com.android.settings",
+                "                 51312K   22911K   20608K  invalid.format",
+                "                          ------   ------  ------",
+                "                          203624K  163604K  TOTAL",
+                "RAM: 731448K total, 415804K free, 9016K buffers, 108548K cached",
+                "[procrank: 1.6s elapsed]");
+        ProcrankParser parser = new ProcrankParser();
+        ProcrankItem procrank = parser.parse(inputBlock);
+
+        // Ensures that only valid lines are parsed. Only 6 of the 11 lines under the header are
+        // valid.
+        assertEquals(6, procrank.getPids().size());
+
+        // Make sure all expected rows are present, and do a diagonal check of values
+        assertEquals((Integer) 87136, procrank.getVss(178));
+        assertEquals((Integer) 77996, procrank.getRss(1313));
+        assertEquals((Integer) 33122, procrank.getPss(3247));
+        assertEquals((Integer) 28360, procrank.getUss(334));
+        assertEquals("android.process.acore", procrank.getProcessName(2072));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java b/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java
new file mode 100644
index 0000000..a34ee86
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/SystemPropsParserTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.SystemPropsItem;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link SystemPropsParser}
+ */
+public class SystemPropsParserTest extends TestCase {
+    public void testSimpleParse() {
+        List<String> inputBlock = Arrays.asList(
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[dalvik.vm.heapgrowthlimit]: [48m]",
+                "[dalvik.vm.heapsize]: [256m]",
+                "[gsm.version.ril-impl]: [android moto-ril-multimode 1.0]");
+        SystemPropsParser parser = new SystemPropsParser();
+        SystemPropsItem map = parser.parse(inputBlock);
+
+        assertEquals(4, map.size());
+        assertEquals("m=y", map.get("dalvik.vm.dexopt-flags"));
+        assertEquals("48m", map.get("dalvik.vm.heapgrowthlimit"));
+        assertEquals("256m", map.get("dalvik.vm.heapsize"));
+        assertEquals("android moto-ril-multimode 1.0", map.get("gsm.version.ril-impl"));
+    }
+
+    /**
+     * Make sure that a parse error on one line doesn't prevent the rest of the lines from being
+     * parsed
+     */
+    public void testParseError() {
+        List<String> inputBlock = Arrays.asList(
+                "[dalvik.vm.dexopt-flags]: [m=y]",
+                "[ends with newline]: [yup",
+                "]",
+                "[dalvik.vm.heapsize]: [256m]");
+        SystemPropsParser parser = new SystemPropsParser();
+        SystemPropsItem map = parser.parse(inputBlock);
+
+        assertEquals(2, map.size());
+        assertEquals("m=y", map.get("dalvik.vm.dexopt-flags"));
+        assertEquals("256m", map.get("dalvik.vm.heapsize"));
+    }
+}
+
diff --git a/tests/src/com/android/loganalysis/parser/TracesParserTest.java b/tests/src/com/android/loganalysis/parser/TracesParserTest.java
new file mode 100644
index 0000000..b5636e0
--- /dev/null
+++ b/tests/src/com/android/loganalysis/parser/TracesParserTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.parser;
+
+import com.android.loganalysis.item.TracesItem;
+import com.android.loganalysis.util.ArrayUtil;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link TracesParser}
+ */
+public class TracesParserTest extends TestCase {
+
+    /**
+     * Test that the parser parses the correct stack.
+     */
+    public void testTracesParser() {
+        List<String> lines = Arrays.asList(
+                "",
+                "",
+                "----- pid 2887 at 2012-05-02 16:43:41 -----",
+                "Cmd line: com.android.package",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "\"Task_1\" prio=5 tid=27 WAIT",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=4789 nice=10 sched=0/0 cgrp=bg_non_interactive handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=0 stm=0 core=0",
+                "  at class.method1(Class.java:1)",
+                "  - waiting on <0x00000001> (a java.lang.Thread) held by tid=27 (Task_1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "\"Task_2\" prio=5 tid=26 NATIVE",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=4343 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=6 stm=3 core=0",
+                "  #00  pc 00001234  /system/lib/lib.so (addr+8)",
+                "  #01  pc 00001235  /system/lib/lib.so (addr+16)",
+                "  #02  pc 00001236  /system/lib/lib.so (addr+24)",
+                "  at class.method1(Class.java:1)",
+                "",
+                "----- end 2887 -----",
+                "",
+                "",
+                "----- pid 256 at 2012-05-02 16:43:41 -----",
+                "Cmd line: system",
+                "",
+                "DALVIK THREADS:",
+                "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)",
+                "",
+                "\"main\" prio=5 tid=1 NATIVE",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=256 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=175 stm=41 core=0",
+                "  #00  pc 00001234  /system/lib/lib.so (addr+8)",
+                "  #01  pc 00001235  /system/lib/lib.so (addr+16)",
+                "  #02  pc 00001236  /system/lib/lib.so (addr+24)",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)",
+                "",
+                "----- end 256 -----",
+                "");
+
+        List<String> expectedStack = Arrays.asList(
+                "\"main\" prio=5 tid=1 SUSPENDED",
+                "  | group=\"main\" sCount=1 dsCount=0 obj=0x00000001 self=0x00000001",
+                "  | sysTid=2887 nice=0 sched=0/0 cgrp=foreground handle=0000000001",
+                "  | schedstat=( 0 0 0 ) utm=5954 stm=1017 core=0",
+                "  at class.method1(Class.java:1)",
+                "  at class.method2(Class.java:2)",
+                "  at class.method2(Class.java:2)");
+
+        TracesItem traces = new TracesParser().parse(lines);
+        assertEquals(2887, traces.getPid().intValue());
+        assertEquals("com.android.package", traces.getApp());
+        assertEquals(ArrayUtil.join("\n", expectedStack), traces.getStack());
+    }
+}
diff --git a/tests/src/com/android/loganalysis/util/ArrayUtilTest.java b/tests/src/com/android/loganalysis/util/ArrayUtilTest.java
new file mode 100644
index 0000000..9c68293
--- /dev/null
+++ b/tests/src/com/android/loganalysis/util/ArrayUtilTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.util;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ArrayUtil}
+ */
+public class ArrayUtilTest extends TestCase {
+
+    /**
+     * Simple test for {@link ArrayUtil#buildArray(String[]...)}
+     */
+    public void testBuildArray_arrays() {
+        String[] newArray = ArrayUtil.buildArray(new String[] {"1", "2"}, new String[] {"3"},
+                new String[] {"4"});
+        assertEquals(4, newArray.length);
+        for (int i = 0; i < 4; i++) {
+            assertEquals(Integer.toString(i+1), newArray[i]);
+        }
+    }
+
+    /**
+     * Make sure that Collections aren't double-wrapped
+     */
+    public void testJoinCollection() {
+        List<String> list = Arrays.asList("alpha", "beta", "gamma");
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", list);
+        assertEquals(expected, str);
+    }
+
+    /**
+     * Make sure that Arrays aren't double-wrapped
+     */
+    public void testJoinArray() {
+        String[] ary = new String[] {"alpha", "beta", "gamma"};
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", (Object[]) ary);
+        assertEquals(expected, str);
+    }
+
+    /**
+     * Make sure that join on varargs arrays work as expected
+     */
+    public void testJoinNormal() {
+        final String expected = "alpha, beta, gamma";
+        String str = ArrayUtil.join(", ", "alpha", "beta", "gamma");
+        assertEquals(expected, str);
+    }
+}
diff --git a/tests/src/com/android/loganalysis/util/RegexTrieTest.java b/tests/src/com/android/loganalysis/util/RegexTrieTest.java
new file mode 100644
index 0000000..a124ac2
--- /dev/null
+++ b/tests/src/com/android/loganalysis/util/RegexTrieTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.loganalysis.util;
+
+import com.android.loganalysis.util.RegexTrie.CompPattern;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Set of unit tests to verify the behavior of the RegexTrie
+ */
+public class RegexTrieTest extends TestCase {
+    private RegexTrie<Integer> mTrie = null;
+    private static final Integer STORED_VAL = 42;
+    private static final List<String> NULL_LIST = Arrays.asList((String)null);
+
+    @Override
+    public void setUp() throws Exception {
+        mTrie = new RegexTrie<Integer>();
+    }
+
+    private void dumpTrie(RegexTrie trie) {
+        System.err.format("Trie is '%s'\n", trie.toString());
+    }
+
+    public void testStringPattern() {
+        mTrie.put(STORED_VAL, "[p]art1", "[p]art2", "[p]art3");
+        Integer retrieved = mTrie.retrieve("part1", "part2", "part3");
+        assertEquals(STORED_VAL, retrieved);
+    }
+
+    public void testAlternation_single() {
+        mTrie.put(STORED_VAL, "alpha|beta");
+        Integer retrieved;
+        retrieved = mTrie.retrieve("alpha");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("beta");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha|beta");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("gamma");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("alph");
+        assertNull(retrieved);
+    }
+
+    public void testAlternation_multiple() {
+        mTrie.put(STORED_VAL, "a|alpha", "b|beta");
+        Integer retrieved;
+        retrieved = mTrie.retrieve("a", "b");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("a", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha", "b");
+        assertEquals(STORED_VAL, retrieved);
+        retrieved = mTrie.retrieve("alpha", "beta");
+        assertEquals(STORED_VAL, retrieved);
+
+        retrieved = mTrie.retrieve("alpha");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("beta");
+        assertNull(retrieved);
+        retrieved = mTrie.retrieve("alpha", "bet");
+        assertNull(retrieved);
+    }
+
+    public void testGroups_fullMatch() {
+        mTrie.put(STORED_VAL, "a|(alpha)", "b|(beta)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "b");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(NULL_LIST, groups.get(0));
+        assertEquals(NULL_LIST, groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(NULL_LIST, groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "b");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(NULL_LIST, groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "beta");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+    }
+
+    public void testGroups_partialMatch() {
+        mTrie.put(STORED_VAL, "a|(alpha)", "b|(beta)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "alpha");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "beta");
+        assertNull(retrieved);
+        assertEquals(0, groups.size());
+
+        retrieved = mTrie.retrieve(groups, "alpha", "bet");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "betar");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+
+        retrieved = mTrie.retrieve(groups, "alpha", "beta", "gamma");
+        assertNull(retrieved);
+        assertEquals(2, groups.size());
+        assertEquals(Arrays.asList("alpha"), groups.get(0));
+        assertEquals(Arrays.asList("beta"), groups.get(1));
+    }
+
+    /**
+     * Make sure that the wildcard functionality works
+     */
+    public void testWildcard() {
+        mTrie.put(STORED_VAL, "a", null);
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "b", "c");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(3, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("b"), groups.get(1));
+        assertEquals(Arrays.asList("c"), groups.get(2));
+
+        retrieved = mTrie.retrieve(groups, "a");
+        assertNull(retrieved);
+        assertEquals(1, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+    }
+
+    /**
+     * Make sure that if a wildcard and a more specific match could both match, that the more
+     * specific match takes precedence
+     */
+    public void testWildcard_precedence() {
+        // Do one before and one after the wildcard to check for ordering effects
+        mTrie.put(STORED_VAL + 1, "a", "(b)");
+        mTrie.put(STORED_VAL, "a", null);
+        mTrie.put(STORED_VAL + 2, "a", "(c)");
+        Integer retrieved;
+        List<List<String>> groups = new ArrayList<List<String>>();
+
+        retrieved = mTrie.retrieve(groups, "a", "d");
+        assertEquals(STORED_VAL, retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("d"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "b");
+        assertEquals((Integer)(STORED_VAL + 1), retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("b"), groups.get(1));
+
+        retrieved = mTrie.retrieve(groups, "a", "c");
+        assertEquals((Integer)(STORED_VAL + 2), retrieved);
+        assertEquals(2, groups.size());
+        assertTrue(groups.get(0).isEmpty());
+        assertEquals(Arrays.asList("c"), groups.get(1));
+    }
+
+    /**
+     * Verify a bugfix: make sure that no NPE results from calling #retrieve with a wildcard but
+     * without a place to retrieve captures.
+     */
+    public void testWildcard_noCapture() throws NullPointerException {
+        mTrie.put(STORED_VAL, "a", null);
+        String[] key = new String[] {"a", "b", "c"};
+
+        mTrie.retrieve(key);
+        mTrie.retrieve(null, key);
+        // test passes if no exceptions were thrown
+    }
+
+    public void testMultiChild() {
+        mTrie.put(STORED_VAL + 1, "a", "b");
+        mTrie.put(STORED_VAL + 2, "a", "c");
+        dumpTrie(mTrie);
+
+        Object retrieved;
+        retrieved = mTrie.retrieve("a", "b");
+        assertEquals(STORED_VAL + 1, retrieved);
+        retrieved = mTrie.retrieve("a", "c");
+        assertEquals(STORED_VAL + 2, retrieved);
+    }
+
+    /**
+     * Make sure that {@link CompPattern#equals} works as expected.  Shake a proverbial fist at Java
+     */
+    public void testCompPattern_equality() {
+        String regex = "regex";
+        Pattern p1 = Pattern.compile(regex);
+        Pattern p2 = Pattern.compile(regex);
+        Pattern pOther = Pattern.compile("other");
+        CompPattern cp1 = new CompPattern(p1);
+        CompPattern cp2 = new CompPattern(p2);
+        CompPattern cpOther = new CompPattern(pOther);
+
+        // This is the problem with Pattern as implemented
+        assertFalse(p1.equals(p2));
+        assertFalse(p2.equals(p1));
+
+        // Make sure that wrapped patterns with the same regex are considered equivalent
+        assertTrue(cp2.equals(p1));
+        assertTrue(cp2.equals(p2));
+        assertTrue(cp2.equals(cp1));
+
+        // And make sure that wrapped patterns with different regexen are still considered different
+        assertFalse(cp2.equals(pOther));
+        assertFalse(cp2.equals(cpOther));
+    }
+
+    public void testCompPattern_hashmap() {
+        HashMap<CompPattern, Integer> map = new HashMap<CompPattern, Integer>();
+        String regex = "regex";
+        Pattern p1 = Pattern.compile(regex);
+        Pattern p2 = Pattern.compile(regex);
+        Pattern pOther = Pattern.compile("other");
+        CompPattern cp1 = new CompPattern(p1);
+        CompPattern cp2 = new CompPattern(p2);
+        CompPattern cpOther = new CompPattern(pOther);
+
+        map.put(cp1, STORED_VAL);
+        assertTrue(map.containsKey(cp1));
+        assertTrue(map.containsKey(cp2));
+        assertFalse(map.containsKey(cpOther));
+
+        map.put(cpOther, STORED_VAL);
+        assertEquals(map.size(), 2);
+        assertTrue(map.containsKey(cp1));
+        assertTrue(map.containsKey(cp2));
+        assertTrue(map.containsKey(cpOther));
+    }
+}
+