Add vogar expectation file support for CTS.

Bug 3181338

Change-Id: I97e1f8781d7b2781241aec13f1452c51ed6b91cd
diff --git a/libs/vogar-expect/src/vogar/AnnotatedOutcome.java b/libs/vogar-expect/src/vogar/AnnotatedOutcome.java
new file mode 100644
index 0000000..a27ab9e
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/AnnotatedOutcome.java
@@ -0,0 +1,147 @@
+/*
+ * 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 vogar;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+
+/**
+ * Contains an outcome for a test, along with some metadata pertaining to the history of this test,
+ * including a list of previous outcomes, an outcome corresponding to the tag Vogar is being run
+ * with, if applicable, and the expectation for this test, so that result value information is
+ * available.
+ */
+public final class AnnotatedOutcome {
+    public static Ordering<AnnotatedOutcome> ORDER_BY_NAME = new Ordering<AnnotatedOutcome>() {
+        @Override public int compare(AnnotatedOutcome a, AnnotatedOutcome b) {
+            return a.getName().compareTo(b.getName());
+       }
+    };
+
+    private final Expectation expectation;
+    private final Outcome outcome;
+    /** a list of previous outcomes for the same action, sorted in chronological order */
+    private final SortedMap<Long, Outcome> previousOutcomes;
+    /** will be null if not comparing to a tag */
+    private final String tagName;
+    private final Outcome tagOutcome;
+    private final boolean hasMetadata;
+
+    AnnotatedOutcome(Outcome outcome, Expectation expectation,
+            SortedMap<Long, Outcome> previousOutcomes, String tagName, Outcome tagOutcome,
+            boolean hasMetadata) {
+        if (previousOutcomes == null) {
+            throw new NullPointerException();
+        }
+        this.expectation = expectation;
+        this.outcome = outcome;
+        this.previousOutcomes = previousOutcomes;
+        this.tagName = tagName;
+        this.tagOutcome = tagOutcome;
+        this.hasMetadata = hasMetadata;
+    }
+
+    public Outcome getOutcome() {
+        return outcome;
+    }
+
+    public String getName() {
+        return outcome.getName();
+    }
+
+    public ResultValue getResultValue() {
+        return outcome.getResultValue(expectation);
+    }
+
+    public List<ResultValue> getPreviousResultValues() {
+        List<ResultValue> previousResultValues = new ArrayList<ResultValue>();
+        for (Outcome previousOutcome : previousOutcomes.values()) {
+            previousResultValues.add(previousOutcome.getResultValue(expectation));
+        }
+        return previousResultValues;
+    }
+
+    /**
+     * Returns the most recent result value of a run of this test (before the current run).
+     */
+    public ResultValue getMostRecentResultValue(ResultValue defaultValue) {
+        List<ResultValue> previousResultValues = getPreviousResultValues();
+        return previousResultValues.isEmpty() ?
+                defaultValue :
+                previousResultValues.get(previousResultValues.size() - 1);
+    }
+
+    public boolean hasTag() {
+        return tagOutcome != null;
+    }
+
+    public String getTagName() {
+        return tagName;
+    }
+
+    public ResultValue getTagResultValue() {
+        return tagOutcome == null ? null : tagOutcome.getResultValue(expectation);
+    }
+
+    /**
+     * Returns true if the outcome is noteworthy given the result value and previous history.
+     */
+    public boolean isNoteworthy() {
+        return getResultValue() != ResultValue.OK || recentlyChanged() || changedSinceTag();
+    }
+
+    public boolean outcomeChanged() {
+        List<Outcome> previousOutcomesList = getOutcomeList();
+        return previousOutcomesList.isEmpty()
+                || !outcome.equals(previousOutcomesList.get(previousOutcomesList.size() - 1));
+    }
+
+    private ArrayList<Outcome> getOutcomeList() {
+        return new ArrayList<Outcome>(previousOutcomes.values());
+    }
+
+    /**
+     * Returns true if the outcome recently changed in result value.
+     */
+    private boolean recentlyChanged() {
+        List<ResultValue> previousResultValues = getPreviousResultValues();
+        if (previousResultValues.isEmpty()) {
+            return false;
+        }
+        return previousResultValues.get(previousResultValues.size() - 1) != getResultValue();
+    }
+
+    private boolean changedSinceTag() {
+        ResultValue tagResultValue = getTagResultValue();
+        return tagResultValue != null && tagResultValue != getResultValue();
+    }
+
+    /**
+     * Returns a Long representing the time the outcome was last run. Returns {@code defaultValue}
+     * if the outcome is not known to have run before.
+     */
+    public Long lastRun(Long defaultValue) {
+        if (!hasMetadata) {
+            return defaultValue;
+        }
+        List<Long> runTimes = Lists.newArrayList(previousOutcomes.keySet());
+        return runTimes.isEmpty() ? defaultValue : runTimes.get(runTimes.size() - 1);
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/Expectation.java b/libs/vogar-expect/src/vogar/Expectation.java
new file mode 100644
index 0000000..f065f42
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Expectation.java
@@ -0,0 +1,117 @@
+/*
+ * 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 vogar;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * The expected result of an action execution. This is typically encoded in the
+ * expectations text file, which has the following format:
+ * <pre>
+ * test java.io.StreamTokenizer.Reset
+ * result UNSUPPORTED
+ * pattern .*should get token \[, but get -1.*
+ *
+ * # should we fix this?
+ * test java.util.Arrays.CopyMethods
+ * result COMPILE_FAILED
+ * pattern .*cannot find symbol.*
+ * </pre>
+ */
+public final class Expectation {
+
+    /** The pattern to use when no expected output is specified */
+    public static final Pattern MATCH_ALL_PATTERN
+            = Pattern.compile(".*", Pattern.MULTILINE | Pattern.DOTALL);
+
+    /** The expectation of a general successful run. */
+    public static final Expectation SUCCESS = new Expectation(Result.SUCCESS, MATCH_ALL_PATTERN,
+            Collections.<String>emptySet(), "", -1);
+
+    /** Justification for this expectation */
+    private final String description;
+
+    /** The action's expected result, such as {@code EXEC_FAILED}. */
+    private final Result result;
+
+    /** The pattern the expected output will match. */
+    private final Pattern pattern;
+
+    /** Attributes of this test. */
+    private final Set<String> tags;
+
+    /** The tracking bug ID */
+    private final long bug;
+
+    /** True if the identified bug still active. */
+    private boolean bugIsOpen = false;
+
+    public Expectation(Result result, Pattern pattern, Set<String> tags, String description, long bug) {
+        if (result == null || description == null || pattern == null) {
+            throw new IllegalArgumentException(
+                    "result=" + result + " description=" + description + " pattern=" + pattern);
+        }
+
+        this.description = description;
+        this.result = result;
+        this.pattern = pattern;
+        this.tags = new LinkedHashSet<String>(tags);
+        this.bug = bug;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public long getBug() {
+        return bug;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public Set<String> getTags() {
+        return tags;
+    }
+
+    /**
+     * Set the current status of this expectation's bug. When a bug is open,
+     * any result (success or failure) is permitted.
+     */
+    public void setBugIsOpen(boolean bugIsOpen) {
+        this.bugIsOpen = bugIsOpen;
+    }
+
+    /**
+     * Returns true if {@code outcome} matches this expectation.
+     */
+    public boolean matches(Outcome outcome) {
+        return patternMatches(outcome) && (bugIsOpen || result == outcome.getResult());
+    }
+
+    private boolean patternMatches(Outcome outcome) {
+        return pattern.matcher(outcome.getOutput()).matches();
+    }
+
+    @Override public String toString() {
+        return "Expectation[description=" + description + " pattern=" + pattern.pattern() + "]";
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/ExpectationStore.java b/libs/vogar-expect/src/vogar/ExpectationStore.java
new file mode 100644
index 0000000..cfa20e9
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ExpectationStore.java
@@ -0,0 +1,255 @@
+/*
+ * 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 vogar;
+
+//import com.google.caliper.internal.gson.stream.JsonReader;
+
+import com.android.json.stream.JsonReader;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import vogar.commands.Command;
+import vogar.util.Log;
+
+/**
+ * A database of expected outcomes. Entries in this database come in two forms.
+ * <ul>
+ *   <li>Outcome expectations name an outcome (or its prefix, such as
+ *       "java.util"), its expected result, and an optional pattern to match
+ *       the expected output.
+ *   <li>Failure expectations include a pattern that may match the output of any
+ *       outcome. These expectations are useful for hiding failures caused by
+ *       cross-cutting features that aren't supported.
+ * </ul>
+ *
+ * <p>If an outcome matches both an outcome expectation and a failure
+ * expectation, the outcome expectation will be returned.
+ */
+public final class ExpectationStore {
+    private static final int PATTERN_FLAGS = Pattern.MULTILINE | Pattern.DOTALL;
+    private final Map<String, Expectation> outcomes = new LinkedHashMap<String, Expectation>();
+    private final Map<String, Expectation> failures = new LinkedHashMap<String, Expectation>();
+
+    private ExpectationStore() {}
+
+    /**
+     * Finds the expected result for the specified action or outcome name. This
+     * returns a value for all names, even if no explicit expectation was set.
+     */
+    public Expectation get(String name) {
+        Expectation byName = getByNameOrPackage(name);
+        return byName != null ? byName : Expectation.SUCCESS;
+    }
+
+    /**
+     * Finds the expected result for the specified outcome after it has
+     * completed. Unlike {@code get()}, this also takes into account the
+     * outcome's output.
+     *
+     * <p>For outcomes that have both a name match and an output match,
+     * exact name matches are preferred, then output matches, then inexact
+     * name matches.
+     */
+    public Expectation get(Outcome outcome) {
+        Expectation exactNameMatch = outcomes.get(outcome.getName());
+        if (exactNameMatch != null) {
+            return exactNameMatch;
+        }
+
+        for (Map.Entry<String, Expectation> entry : failures.entrySet()) {
+            if (entry.getValue().matches(outcome)) {
+                return entry.getValue();
+            }
+        }
+
+        Expectation byName = getByNameOrPackage(outcome.getName());
+        return byName != null ? byName : Expectation.SUCCESS;
+    }
+
+    private Expectation getByNameOrPackage(String name) {
+        while (true) {
+            Expectation expectation = outcomes.get(name);
+            if (expectation != null) {
+                return expectation;
+            }
+
+            int dotOrHash = Math.max(name.lastIndexOf('.'), name.lastIndexOf('#'));
+            if (dotOrHash == -1) {
+                return null;
+            }
+
+            name = name.substring(0, dotOrHash);
+        }
+    }
+
+    public static ExpectationStore parse(Set<File> expectationFiles, ModeId mode) throws IOException {
+        ExpectationStore result = new ExpectationStore();
+        for (File f : expectationFiles) {
+            if (f.exists()) {
+                result.parse(f, mode);
+            }
+        }
+        return result;
+    }
+
+    public void parse(File expectationsFile, ModeId mode) throws IOException {
+        Log.verbose("loading expectations file " + expectationsFile);
+
+        int count = 0;
+        JsonReader reader = null;
+        try {
+            reader = new JsonReader(new FileReader(expectationsFile));
+            reader.setLenient(true);
+            reader.beginArray();
+            while (reader.hasNext()) {
+                readExpectation(reader, mode);
+                count++;
+            }
+            reader.endArray();
+
+            Log.verbose("loaded " + count + " expectations from " + expectationsFile);
+        } finally {
+            if (reader != null) {
+                reader.close();
+            }
+        }
+    }
+
+    private void readExpectation(JsonReader reader, ModeId mode) throws IOException {
+        boolean isFailure = false;
+        Result result = Result.SUCCESS;
+        Pattern pattern = Expectation.MATCH_ALL_PATTERN;
+        Set<String> names = new LinkedHashSet<String>();
+        Set<String> tags = new LinkedHashSet<String>();
+        Set<ModeId> modes = null;
+        String description = "";
+        long buganizerBug = -1;
+
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (name.equals("result")) {
+                result = Result.valueOf(reader.nextString());
+            } else if (name.equals("name")) {
+                names.add(reader.nextString());
+            } else if (name.equals("names")) {
+                readStrings(reader, names);
+            } else if (name.equals("failure")) {
+                isFailure = true;
+                names.add(reader.nextString());
+            } else if (name.equals("pattern")) {
+                pattern = Pattern.compile(reader.nextString(), PATTERN_FLAGS);
+            } else if (name.equals("substring")) {
+                pattern = Pattern.compile(".*" + Pattern.quote(reader.nextString()) + ".*", PATTERN_FLAGS);
+            } else if (name.equals("tags")) {
+                readStrings(reader, tags);
+            } else if (name.equals("description")) {
+                Iterable<String> split = Splitter.on("\n").omitEmptyStrings().trimResults().split(reader.nextString());
+                description = Joiner.on("\n").join(split);
+            } else if (name.equals("bug")) {
+                buganizerBug = reader.nextLong();
+            } else if (name.equals("modes")) {
+                modes = readModes(reader);
+            } else {
+                Log.warn("Unhandled name in expectations file: " + name);
+                reader.skipValue();
+            }
+        }
+        reader.endObject();
+
+        if (names.isEmpty()) {
+            throw new IllegalArgumentException("Missing 'name' or 'failure' key in " + reader);
+        }
+        if (modes != null && !modes.contains(mode)) {
+            return;
+        }
+
+        Expectation expectation = new Expectation(result, pattern, tags, description, buganizerBug);
+        Map<String, Expectation> map = isFailure ? failures : outcomes;
+        for (String name : names) {
+            if (map.put(name, expectation) != null) {
+                throw new IllegalArgumentException("Duplicate expectations for " + name);
+            }
+        }
+    }
+
+    private void readStrings(JsonReader reader, Set<String> output) throws IOException {
+        reader.beginArray();
+        while (reader.hasNext()) {
+            output.add(reader.nextString());
+        }
+        reader.endArray();
+    }
+
+    private Set<ModeId> readModes(JsonReader reader) throws IOException {
+        Set<ModeId> result = new LinkedHashSet<ModeId>();
+        reader.beginArray();
+        while (reader.hasNext()) {
+            result.add(ModeId.valueOf(reader.nextString().toUpperCase()));
+        }
+        reader.endArray();
+        return result;
+    }
+
+    /**
+     * Sets the bugIsOpen status on all expectations by querying an external bug
+     * tracker.
+     */
+    public void loadBugStatuses(String openBugsCommand) {
+        Iterable<Expectation> allExpectations = Iterables.concat(outcomes.values(), failures.values());
+
+        // figure out what bug IDs we're interested in
+        Set<String> bugs = new LinkedHashSet<String>();
+        for (Expectation expectation : allExpectations) {
+            if (expectation.getBug() != -1) {
+                bugs.add(Long.toString(expectation.getBug()));
+            }
+        }
+        if (bugs.isEmpty()) {
+            return;
+        }
+
+        // query the external app for open bugs
+        List<String> openBugs = new Command.Builder()
+                .args(openBugsCommand)
+                .args(bugs)
+                .execute();
+        Set<Long> openBugsSet = new LinkedHashSet<Long>();
+        for (String bug : openBugs) {
+            openBugsSet.add(Long.parseLong(bug));
+        }
+
+        Log.verbose("tracking " + openBugsSet.size() + " open bugs: " + openBugs);
+
+        // update our expectations with that set
+        for (Expectation expectation : allExpectations) {
+            if (openBugsSet.contains(expectation.getBug())) {
+                expectation.setBugIsOpen(true);
+            }
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/ModeId.java b/libs/vogar-expect/src/vogar/ModeId.java
new file mode 100644
index 0000000..3b24cc1
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ModeId.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+public enum ModeId {
+    DEVICE, JVM, ACTIVITY, SIM, HOST;
+
+    public boolean acceptsVmArgs() {
+        return this != ACTIVITY;
+    }
+
+    public boolean isHost() {
+        return this == JVM || this == SIM || this == HOST;
+    }
+
+    public boolean requiresAndroidSdk() {
+        return this == DEVICE || this == ACTIVITY || this == SIM || this == HOST;
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/Outcome.java b/libs/vogar-expect/src/vogar/Outcome.java
new file mode 100644
index 0000000..3d7c68f
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Outcome.java
@@ -0,0 +1,173 @@
+/*
+ * 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 vogar;
+
+import com.google.common.collect.Lists;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import vogar.util.Strings;
+
+/**
+ * An outcome of an action. Some actions may have multiple outcomes. For
+ * example, JUnit tests have one outcome for each test method.
+ */
+public final class Outcome {
+
+    private final String outcomeName;
+    private final Result result;
+    private final String output;
+    private final Date date;
+
+    public Outcome(String outcomeName, Result result, List<String> outputLines) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLines(outputLines);
+        this.date = new Date();
+    }
+
+    public Outcome(String outcomeName, Result result, String outputLine, Date date) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLine(outputLine);
+        this.date = date;
+    }
+
+    public Outcome(String outcomeName, Result result, String outputLine) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLine(outputLine);
+        this.date = new Date();
+    }
+
+    public Outcome(String outcomeName, Result result, Throwable throwable) {
+        this.outcomeName = outcomeName;
+        this.result = result;
+        this.output = sanitizeOutputLines(throwableToLines(throwable));
+        this.date = new Date();
+    }
+
+    private String sanitizeOutputLines(List<String> outputLines) {
+        List<String> sanitizedStrings = Lists.newArrayList();
+        for (String line : outputLines) {
+            sanitizedStrings.add(sanitizeOutputLine(line));
+        }
+        return Strings.join(sanitizedStrings, "\n");
+    }
+
+    private String sanitizeOutputLine(String outputLine) {
+        return Strings.xmlSanitize(outputLine.replaceAll("\r\n?", "\n"));
+    }
+
+    public Date getDate() {
+        return date;
+    }
+
+    public String getName() {
+        return outcomeName;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public String getOutput() {
+        return output;
+    }
+
+    public List<String> getOutputLines() {
+        return Arrays.asList(output.split("\n"));
+    }
+
+    private static List<String> throwableToLines(Throwable t) {
+        StringWriter writer = new StringWriter();
+        PrintWriter out = new PrintWriter(writer);
+        t.printStackTrace(out);
+        return Arrays.asList(writer.toString().split("\\n"));
+    }
+
+    /**
+     * Returns the action's suite name, such as java.lang.Integer or
+     * java.lang.IntegerTest.
+     */
+    public String getSuiteName() {
+        int split = split(outcomeName);
+        return split == -1 ? "defaultpackage" : outcomeName.substring(0, split);
+    }
+
+    /**
+     * Returns the specific action name, such as BitTwiddle or testBitTwiddle.
+     */
+    public String getTestName() {
+        int split = split(outcomeName);
+        return split == -1 ? outcomeName : outcomeName.substring(split + 1);
+    }
+
+    private static int split(String name) {
+        int lastHash = name.indexOf('#');
+        return lastHash == -1 ? name.lastIndexOf('.') : lastHash;
+    }
+
+    /**
+     * Returns whether the result indicates that the contents of the Outcome are important.
+     *
+     * For example, for a test skipped because it is unsupported, we don't care about the result.
+     */
+    private boolean matters() {
+        return result != Result.UNSUPPORTED;
+    }
+
+    public ResultValue getResultValue(Expectation expectation) {
+        if (matters()) {
+            return expectation.matches(this) ? ResultValue.OK : ResultValue.FAIL;
+        }
+        return ResultValue.IGNORE;
+    }
+
+    /**
+     * Returns a filesystem db path for this outcome. For example, a path for an outcome with name
+     * "foo.bar.baz#testName" would be "foo/bar/baz/testName".
+     */
+    public String getPath() {
+        return outcomeName.replaceAll("[\\.#]", "/");
+    }
+
+    @Override public boolean equals(Object o) {
+        if (o instanceof Outcome) {
+            Outcome outcome = (Outcome) o;
+            return outcomeName.equals(outcome.outcomeName)
+                    && result == outcome.result
+                    && output.equals(outcome.output);
+        }
+        return false;
+    }
+
+    @Override public int hashCode() {
+        int hashCode = 17;
+        hashCode = 37 * hashCode + outcomeName.hashCode();
+        hashCode  = 37 * hashCode + result.hashCode();
+        hashCode = 37 * hashCode + output.hashCode();
+        return hashCode;
+    }
+
+    @Override public String toString() {
+        return "Outcome[name=" + outcomeName + " output=" + output + "]";
+    }
+
+}
diff --git a/libs/vogar-expect/src/vogar/Result.java b/libs/vogar-expect/src/vogar/Result.java
new file mode 100644
index 0000000..45c88ce
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/Result.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar;
+
+/**
+ * The result of a test or benchmark execution.
+ */
+public enum Result {
+
+    /**
+     * An action that cannot be run by this harness, such as a shell script.
+     */
+    UNSUPPORTED,
+
+    COMPILE_FAILED,
+    EXEC_FAILED,
+    EXEC_TIMEOUT,
+    ERROR,
+    SUCCESS
+}
diff --git a/libs/vogar-expect/src/vogar/ResultValue.java b/libs/vogar-expect/src/vogar/ResultValue.java
new file mode 100644
index 0000000..2e450f4
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/ResultValue.java
@@ -0,0 +1,26 @@
+/*
+ * 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 vogar;
+
+/**
+ * Represents an evaluation of the goodness of a result.
+ */
+public enum ResultValue {
+    OK,
+    IGNORE,
+    FAIL
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Command.java b/libs/vogar-expect/src/vogar/commands/Command.java
new file mode 100644
index 0000000..d60d77e
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Command.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import vogar.util.Log;
+import vogar.util.Strings;
+import vogar.util.Threads;
+
+/**
+ * An out of process executable.
+ */
+public final class Command {
+    private final List<String> args;
+    private final Map<String, String> env;
+    private final File workingDirectory;
+    private final boolean permitNonZeroExitStatus;
+    private final PrintStream tee;
+    private final boolean nativeOutput;
+    private volatile Process process;
+
+    public Command(String... args) {
+        this(Arrays.asList(args));
+    }
+
+    public Command(List<String> args) {
+        this.args = new ArrayList<String>(args);
+        this.env = Collections.emptyMap();
+        this.workingDirectory = null;
+        this.permitNonZeroExitStatus = false;
+        this.tee = null;
+        this.nativeOutput = false;
+    }
+
+    private Command(Builder builder) {
+        this.args = new ArrayList<String>(builder.args);
+        this.env = builder.env;
+        this.workingDirectory = builder.workingDirectory;
+        this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
+        this.tee = builder.tee;
+        if (builder.maxLength != -1) {
+            String string = toString();
+            if (string.length() > builder.maxLength) {
+                throw new IllegalStateException("Maximum command length " + builder.maxLength
+                                                + " exceeded by: " + string);
+            }
+        }
+        this.nativeOutput = builder.nativeOutput;
+    }
+
+    public void start() throws IOException {
+        if (isStarted()) {
+            throw new IllegalStateException("Already started!");
+        }
+
+        Log.verbose("executing " + this);
+
+        ProcessBuilder processBuilder = new ProcessBuilder()
+                .command(args)
+                .redirectErrorStream(true);
+        if (workingDirectory != null) {
+            processBuilder.directory(workingDirectory);
+        }
+
+        processBuilder.environment().putAll(env);
+
+        process = processBuilder.start();
+    }
+
+    public boolean isStarted() {
+        return process != null;
+    }
+
+    public InputStream getInputStream() {
+        if (!isStarted()) {
+            throw new IllegalStateException("Not started!");
+        }
+
+        return process.getInputStream();
+    }
+
+    public List<String> gatherOutput()
+            throws IOException, InterruptedException {
+        if (!isStarted()) {
+            throw new IllegalStateException("Not started!");
+        }
+
+        BufferedReader in = new BufferedReader(
+                new InputStreamReader(getInputStream(), "UTF-8"));
+        List<String> outputLines = new ArrayList<String>();
+        String outputLine;
+        while ((outputLine = in.readLine()) != null) {
+            if (tee != null) {
+                tee.println(outputLine);
+            }
+            if (nativeOutput) {
+                Log.nativeOutput(outputLine);
+            }
+            outputLines.add(outputLine);
+        }
+
+        if (process.waitFor() != 0 && !permitNonZeroExitStatus) {
+            StringBuilder message = new StringBuilder();
+            for (String line : outputLines) {
+                message.append("\n").append(line);
+            }
+            throw new CommandFailedException(args, outputLines);
+        }
+
+        return outputLines;
+    }
+
+    public List<String> execute() {
+        try {
+            start();
+            return gatherOutput();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to execute process: " + args, e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while executing process: " + args, e);
+        }
+    }
+
+    /**
+     * Executes a command with a specified timeout. If the process does not
+     * complete normally before the timeout has elapsed, it will be destroyed.
+     *
+     * @param timeoutSeconds how long to wait, or 0 to wait indefinitely
+     * @return the command's output, or null if the command timed out
+     */
+    public List<String> executeWithTimeout(int timeoutSeconds)
+            throws TimeoutException {
+        if (timeoutSeconds == 0) {
+            return execute();
+        }
+
+        try {
+            return executeLater().get(timeoutSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted while executing process: " + args, e);
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        } finally {
+            destroy();
+        }
+    }
+
+    /**
+     * Executes the command on a new background thread. This method returns
+     * immediately.
+     *
+     * @return a future to retrieve the command's output.
+     */
+    public Future<List<String>> executeLater() {
+        ExecutorService executor = Threads.fixedThreadsExecutor("command", 1);
+        Future<List<String>> result = executor.submit(new Callable<List<String>>() {
+            public List<String> call() throws Exception {
+                start();
+                return gatherOutput();
+            }
+        });
+        executor.shutdown();
+        return result;
+    }
+
+    /**
+     * Destroys the underlying process and closes its associated streams.
+     */
+    public void destroy() {
+        if (process == null) {
+            return;
+        }
+
+        process.destroy();
+        try {
+            process.waitFor();
+            int exitValue = process.exitValue();
+            Log.verbose("received exit value " + exitValue
+                    + " from destroyed command " + this);
+        } catch (IllegalThreadStateException destroyUnsuccessful) {
+            Log.warn("couldn't destroy " + this);
+        } catch (InterruptedException e) {
+            Log.warn("couldn't destroy " + this);
+        }
+    }
+
+    @Override public String toString() {
+        String envString = !env.isEmpty() ? (Strings.join(env.entrySet(), " ") + " ") : "";
+        return envString + Strings.join(args, " ");
+    }
+
+    public static class Builder {
+        private final List<String> args = new ArrayList<String>();
+        private final Map<String, String> env = new LinkedHashMap<String, String>();
+        private File workingDirectory;
+        private boolean permitNonZeroExitStatus = false;
+        private PrintStream tee = null;
+        private boolean nativeOutput;
+        private int maxLength = -1;
+
+        public Builder args(Object... objects) {
+            for (Object object : objects) {
+                args(object.toString());
+            }
+            return this;
+        }
+
+        public Builder setNativeOutput(boolean nativeOutput) {
+            this.nativeOutput = nativeOutput;
+            return this;
+        }
+
+        public Builder args(String... args) {
+            return args(Arrays.asList(args));
+        }
+
+        public Builder args(Collection<String> args) {
+            this.args.addAll(args);
+            return this;
+        }
+
+        public Builder env(String key, String value) {
+            env.put(key, value);
+            return this;
+        }
+
+        /**
+         * Sets the working directory from which the command will be executed.
+         * This must be a <strong>local</strong> directory; Commands run on
+         * remote devices (ie. via {@code adb shell}) require a local working
+         * directory.
+         */
+        public Builder workingDirectory(File workingDirectory) {
+            this.workingDirectory = workingDirectory;
+            return this;
+        }
+
+        public Builder tee(PrintStream printStream) {
+            tee = printStream;
+            return this;
+        }
+
+        public Builder maxLength(int maxLength) {
+            this.maxLength = maxLength;
+            return this;
+        }
+
+        public Command build() {
+            return new Command(this);
+        }
+
+        public List<String> execute() {
+            return build().execute();
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/commands/CommandFailedException.java b/libs/vogar-expect/src/vogar/commands/CommandFailedException.java
new file mode 100644
index 0000000..3e08c11
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/CommandFailedException.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.util.List;
+
+/**
+ * Thrown when an out of process executable does not return normally.
+ */
+public class CommandFailedException extends RuntimeException {
+
+    private final List<String> args;
+    private final List<String> outputLines;
+
+    public CommandFailedException(List<String> args, List<String> outputLines) {
+        super(formatMessage(args, outputLines));
+        this.args = args;
+        this.outputLines = outputLines;
+    }
+
+    public List<String> getArgs() {
+        return args;
+    }
+
+    public List<String> getOutputLines() {
+        return outputLines;
+    }
+
+    public static String formatMessage(List<String> args, List<String> outputLines) {
+        StringBuilder result = new StringBuilder();
+        result.append("Command failed:");
+        for (String arg : args) {
+            result.append(" ").append(arg);
+        }
+        for (String outputLine : outputLines) {
+            result.append("\n  ").append(outputLine);
+        }
+        return result.toString();
+    }
+
+    private static final long serialVersionUID = 0;
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Mkdir.java b/libs/vogar-expect/src/vogar/commands/Mkdir.java
new file mode 100644
index 0000000..fc08f1b
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Mkdir.java
@@ -0,0 +1,29 @@
+/*
+ * 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 vogar.commands;
+
+import java.io.File;
+
+/**
+ * A mkdir command.
+ */
+public final class Mkdir {
+
+    public void mkdirs(File directory) {
+        new Command("mkdir", "-p", directory.getPath()).execute();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/commands/Rm.java b/libs/vogar-expect/src/vogar/commands/Rm.java
new file mode 100644
index 0000000..5b39144
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/commands/Rm.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.commands;
+
+import java.io.File;
+
+/**
+ * A rm command.
+ */
+public final class Rm {
+
+    public void file(File file) {
+        new Command("rm", "-f", file.getPath()).execute();
+    }
+
+    public void directoryTree(File directory) {
+        new Command("rm", "-rf", directory.getPath()).execute();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/IoUtils.java b/libs/vogar-expect/src/vogar/util/IoUtils.java
new file mode 100644
index 0000000..4f1fba1
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/IoUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+
+public final class IoUtils {
+
+    public static void closeQuietly(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+    public static void closeQuietly(Socket c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignored) {
+            }
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Log.java b/libs/vogar-expect/src/vogar/util/Log.java
new file mode 100644
index 0000000..99c0807
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Log.java
@@ -0,0 +1,68 @@
+/*
+ * 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 vogar.util;
+
+import java.util.List;
+
+public class Log {
+
+    private static LogOutput sLogoutput = null;
+
+    public static void setOutput(LogOutput logOutput) {
+        sLogoutput = logOutput;
+    }
+
+    public static void verbose(String s) {
+        if (sLogoutput != null) {
+            sLogoutput.verbose(s);
+        }
+    }
+
+    public static void warn(String message) {
+        if (sLogoutput != null) {
+            sLogoutput.warn(message);
+        }
+    }
+
+    /**
+     * Warns, and also puts a list of strings afterwards.
+     */
+    public static void warn(String message, List<String> list) {
+        if (sLogoutput != null) {
+            sLogoutput.warn(message, list);
+        }
+    }
+
+    public static void info(String s) {
+        if (sLogoutput != null) {
+            sLogoutput.info(s);
+        }
+    }
+
+    public static void info(String message, Throwable throwable) {
+        if (sLogoutput != null) {
+            sLogoutput.info(message, throwable);
+        }
+    }
+
+    public static void nativeOutput(String outputLine) {
+        if (sLogoutput != null) {
+            sLogoutput.nativeOutput(outputLine);
+        }
+
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/LogOutput.java b/libs/vogar-expect/src/vogar/util/LogOutput.java
new file mode 100644
index 0000000..8123a81
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/LogOutput.java
@@ -0,0 +1,38 @@
+/*
+ * 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 vogar.util;
+
+import java.util.List;
+
+public interface LogOutput {
+
+    void verbose(String s);
+
+    void warn(String message);
+
+    /**
+     * Warns, and also puts a list of strings afterwards.
+     */
+    void warn(String message, List<String> list);
+
+    void info(String s);
+
+    void info(String message, Throwable throwable);
+
+    void nativeOutput(String outputLine);
+
+}
diff --git a/libs/vogar-expect/src/vogar/util/MarkResetConsole.java b/libs/vogar-expect/src/vogar/util/MarkResetConsole.java
new file mode 100644
index 0000000..d88ce31
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/MarkResetConsole.java
@@ -0,0 +1,87 @@
+/*
+ * 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 vogar.util;
+
+import java.io.PrintStream;
+
+/**
+ * A console that can erase output back to a previously marked position.
+ */
+public final class MarkResetConsole {
+
+    private final PrintStream out;
+    private int row;
+    private final StringBuilder rowContent = new StringBuilder();
+
+    public MarkResetConsole(PrintStream out) {
+        this.out = out;
+    }
+
+    public void println(String text) {
+        print(text + "\n");
+    }
+
+    public void print(String text) {
+        for (int i = 0; i < text.length(); i++) {
+            if (text.charAt(i) == '\n') {
+                row++;
+                rowContent.delete(0, rowContent.length());
+            } else {
+                rowContent.append(text.charAt(i));
+            }
+        }
+
+        out.print(text);
+        out.flush();
+    }
+
+    public Mark mark() {
+        return new Mark();
+    }
+
+    public class Mark {
+        private final int markRow = row;
+        private final String markRowContent = rowContent.toString();
+
+        private Mark() {}
+
+        public void reset() {
+            /*
+             * ANSI escapes
+             * http://en.wikipedia.org/wiki/ANSI_escape_code
+             *
+             *  \u001b[K   clear the rest of the current line
+             *  \u001b[nA  move the cursor up n lines
+             *  \u001b[nB  move the cursor down n lines
+             *  \u001b[nC  move the cursor right n lines
+             *  \u001b[nD  move the cursor left n columns
+             */
+
+            for (int r = row; r > markRow; r--) {
+                // clear the line, up a line
+                System.out.print("\u001b[0G\u001b[K\u001b[1A");
+            }
+
+            // clear the line, reprint the line
+            out.print("\u001b[0G\u001b[K");
+            out.print(markRowContent);
+            rowContent.delete(0, rowContent.length());
+            rowContent.append(markRowContent);
+            row = markRow;
+        }
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Strings.java b/libs/vogar-expect/src/vogar/util/Strings.java
new file mode 100644
index 0000000..f92edd8
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Strings.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package vogar.util;
+
+//import com.google.common.collect.Lists;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for strings.
+ */
+public class Strings {
+
+    private static final Pattern XML_INVALID_CHARS
+            = Pattern.compile("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]+");
+
+    public static String readStream(Reader reader) throws IOException {
+        StringBuilder result = new StringBuilder();
+        BufferedReader in = new BufferedReader(reader);
+        String line;
+        while ((line = in.readLine()) != null) {
+            result.append(line);
+            result.append('\n');
+        }
+        in.close();
+        return result.toString();
+    }
+
+    public static String readFile(File f) throws IOException {
+        return readStream(new InputStreamReader(new FileInputStream(f), "UTF-8"));
+    }
+
+    public static List<String> readFileLines(File f) throws IOException {
+        BufferedReader in =
+                new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"));
+        List<String> list = new ArrayList<String>();
+        String line;
+        while ((line = in.readLine()) != null) {
+            list.add(line);
+        }
+        in.close();
+        return list;
+    }
+
+    public static String join(String delimiter, Object... objects) {
+        return join(Arrays.asList(objects), delimiter);
+    }
+
+    public static String join(Iterable<?> objects, String delimiter) {
+        Iterator<?> i = objects.iterator();
+        if (!i.hasNext()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        result.append(i.next());
+        while(i.hasNext()) {
+            result.append(delimiter).append(i.next());
+        }
+        return result.toString();
+    }
+
+    public static String[] objectsToStrings(Object[] objects) {
+        String[] result = new String[objects.length];
+        int i = 0;
+        for (Object o : objects) {
+            result[i++] = o.toString();
+        }
+        return result;
+    }
+
+    public static String[] objectsToStrings(Collection<?> objects) {
+        return objectsToStrings(objects.toArray());
+    }
+
+    /**
+     * Replaces XML-invalid characters with the corresponding U+XXXX code point escapes.
+     */
+    public static String xmlSanitize(String text) {
+        StringBuffer result = new StringBuffer();
+        Matcher matcher = XML_INVALID_CHARS.matcher(text);
+        while (matcher.find()) {
+            matcher.appendReplacement(result, "");
+            result.append(escapeCodePoint(matcher.group()));
+        }
+        matcher.appendTail(result);
+        return result.toString();
+    }
+
+    private static String escapeCodePoint(CharSequence cs) {
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < cs.length(); ++i) {
+            result.append(String.format("U+%04X", (int) cs.charAt(i)));
+        }
+        return result.toString();
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/Threads.java b/libs/vogar-expect/src/vogar/util/Threads.java
new file mode 100644
index 0000000..83410d5
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/Threads.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package vogar.util;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility methods for working with threads.
+ */
+public final class Threads {
+    private Threads() {}
+
+    public static ThreadFactory daemonThreadFactory(final String name) {
+        return new ThreadFactory() {
+            private int nextId = 0;
+            public synchronized Thread newThread(Runnable r) {
+                Thread thread = new Thread(r, name + "-" + (nextId++));
+                thread.setDaemon(true);
+                return thread;
+            }
+        };
+    }
+
+    public static ExecutorService threadPerCpuExecutor(String name) {
+        return fixedThreadsExecutor(name, Runtime.getRuntime().availableProcessors());
+    }
+
+    public static ExecutorService fixedThreadsExecutor(String name, int count) {
+        ThreadFactory threadFactory = daemonThreadFactory(name);
+
+        return new ThreadPoolExecutor(count, count, 10, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>(Integer.MAX_VALUE), threadFactory) {
+            @Override protected void afterExecute(Runnable runnable, Throwable throwable) {                if (throwable != null) {
+                    Log.info("Unexpected failure from " + runnable, throwable);
+                }
+            }
+        };
+    }
+}
diff --git a/libs/vogar-expect/src/vogar/util/TimeUtilities.java b/libs/vogar-expect/src/vogar/util/TimeUtilities.java
new file mode 100644
index 0000000..c5a7e3b
--- /dev/null
+++ b/libs/vogar-expect/src/vogar/util/TimeUtilities.java
@@ -0,0 +1,117 @@
+/*
+ * 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 vogar.util;
+
+/**
+ * Utilities to make it easier to work with ISO 8601 dates and times.
+ * This is a subset of the original class from http://software.jessies.org/salma-hayek/ --- please submit fixes upstream.
+ */
+public class TimeUtilities {
+    /**
+     * Returns the ISO 8601-format String corresponding to the given duration (measured in milliseconds).
+     */
+    public static String msToIsoString(long duration) {
+        long milliseconds = duration % 1000;
+        duration /= 1000;
+        long seconds = duration % 60;
+        duration /= 60;
+        long minutes = duration % 60;
+        duration /= 60;
+        long hours = duration;
+
+        StringBuilder result = new StringBuilder("P");
+        if (hours != 0) {
+            result.append(hours);
+            result.append('H');
+        }
+        if (result.length() > 1 || minutes != 0) {
+            result.append(minutes);
+            result.append('M');
+        }
+        result.append(seconds);
+        if (milliseconds != 0) {
+            result.append('.');
+            result.append(milliseconds);
+        }
+        result.append('S');
+        return result.toString();
+    }
+    
+    /**
+     * Returns a string representation of the given number of milliseconds.
+     */
+    public static String msToString(long ms) {
+        return nsToString(ms * 1000000);
+    }
+    
+    /**
+     * Returns a string representation of the given number of nanoseconds.
+     */
+    public static String nsToString(long ns) {
+        if (ns < 1000L) {
+            return Long.toString(ns) + "ns";
+        } else if (ns < 1000000L) {
+            return Long.toString(ns/1000L) + "us";
+        } else if (ns < 1000000000L) {
+            return Long.toString(ns/1000000L) + "ms";
+        } else if (ns < 60000000000L) {
+            return String.format("%.2fs", nsToS(ns));
+        } else {
+            long duration = ns;
+            long nanoseconds = duration % 1000;
+            duration /= 1000;
+            long microseconds = duration % 1000;
+            duration /= 1000;
+            long milliseconds = duration % 1000;
+            duration /= 1000;
+            long seconds = duration % 60;
+            duration /= 60;
+            long minutes = duration % 60;
+            duration /= 60;
+            long hours = duration % 24;
+            duration /= 24;
+            long days = duration;
+            
+            StringBuilder result = new StringBuilder();
+            if (days != 0) {
+                result.append(days);
+                result.append('d');
+            }
+            if (result.length() > 1 || hours != 0) {
+                result.append(hours);
+                result.append('h');
+            }
+            if (result.length() > 1 || minutes != 0) {
+                result.append(minutes);
+                result.append('m');
+            }
+            result.append(seconds);
+            result.append('s');
+            return result.toString();
+        }
+    }
+    
+    /**
+     * Converts nanoseconds into (fractional) seconds.
+     */
+    public static double nsToS(long ns) {
+        return ((double) ns)/1000000000.0;
+    }
+
+    private TimeUtilities() {
+    }
+}