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() {
+ }
+}