summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Wilson <jessewilson@google.com>2010-03-31 23:51:56 -0700
committerJesse Wilson <jessewilson@google.com>2010-04-01 14:42:05 -0700
commit575e89934e81c1bebaecee13d3048268648f5adc (patch)
tree245873cb76eb1f5d49aacf40fab1046eca2f25f1
parentba6fad59410d74c4dbb19715dea6f5ea09a4b14c (diff)
New method-level granularity and output streaming for vogar.
-rw-r--r--tools/runner/java/vogar/Action.java9
-rw-r--r--tools/runner/java/vogar/ActivityMode.java33
-rw-r--r--tools/runner/java/vogar/CodeFinder.java2
-rw-r--r--tools/runner/java/vogar/Console.java216
-rw-r--r--tools/runner/java/vogar/DeviceDalvikVm.java11
-rw-r--r--tools/runner/java/vogar/Driver.java121
-rw-r--r--tools/runner/java/vogar/Environment.java12
-rw-r--r--tools/runner/java/vogar/EnvironmentDevice.java5
-rw-r--r--tools/runner/java/vogar/HostMonitor.java200
-rw-r--r--tools/runner/java/vogar/JavaVm.java9
-rw-r--r--tools/runner/java/vogar/Mode.java44
-rw-r--r--tools/runner/java/vogar/NamingPatternCodeFinder.java2
-rw-r--r--tools/runner/java/vogar/Result.java2
-rw-r--r--tools/runner/java/vogar/TestProperties.java5
-rw-r--r--tools/runner/java/vogar/Vm.java17
-rw-r--r--tools/runner/java/vogar/Vogar.java80
-rw-r--r--tools/runner/java/vogar/commands/Command.java75
-rw-r--r--tools/runner/java/vogar/target/CaliperRunner.java13
-rw-r--r--tools/runner/java/vogar/target/JUnitRunner.java92
-rw-r--r--tools/runner/java/vogar/target/JtregRunner.java14
-rw-r--r--tools/runner/java/vogar/target/MainRunner.java12
-rw-r--r--tools/runner/java/vogar/target/Runner.java5
-rw-r--r--tools/runner/java/vogar/target/TargetMonitor.java107
-rw-r--r--tools/runner/java/vogar/target/TestRunner.java31
-rw-r--r--tools/runner/lib/TestActivity.java68
25 files changed, 878 insertions, 307 deletions
diff --git a/tools/runner/java/vogar/Action.java b/tools/runner/java/vogar/Action.java
index 3580ef4934..1e3de3e179 100644
--- a/tools/runner/java/vogar/Action.java
+++ b/tools/runner/java/vogar/Action.java
@@ -50,7 +50,7 @@ public final class Action {
}
/**
- * Returns the local directory containing this test's java file.
+ * Returns the local directory containing this action's java file.
*/
public File getJavaDirectory() {
return actionDirectory;
@@ -69,14 +69,14 @@ public final class Action {
}
/**
- * Returns a unique identifier for this test.
+ * Returns a unique identifier for this action.
*/
public String getName() {
return name;
}
/**
- * Returns an English description of this test, or null if no such
+ * Returns an English description of this action, or null if no such
* description is known.
*/
public String getDescription() {
@@ -84,7 +84,8 @@ public final class Action {
}
/**
- * Initializes the directory from which local files can be read by the test.
+ * Initializes the directory from which local files can be read by the
+ * action.
*/
public void setUserDir(File base) {
this.userDir = base;
diff --git a/tools/runner/java/vogar/ActivityMode.java b/tools/runner/java/vogar/ActivityMode.java
index 94adbd74d3..4572b1d149 100644
--- a/tools/runner/java/vogar/ActivityMode.java
+++ b/tools/runner/java/vogar/ActivityMode.java
@@ -25,18 +25,16 @@ import vogar.commands.Rm;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
-import java.util.concurrent.TimeoutException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
/**
- * Runs a test in the context of an android.app.Activity on a device
+ * Runs an action in the context of an android.app.Activity on a device
*/
final class ActivityMode extends Mode {
@@ -44,12 +42,12 @@ final class ActivityMode extends Mode {
private static final String TEST_ACTIVITY_CLASS = "vogar.target.TestActivity";
- ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, List<String> javacArgs,
- PrintStream tee, File localTemp, boolean cleanBefore, boolean cleanAfter,
- File deviceRunnerDir) {
+ ActivityMode(Integer debugPort, File sdkJar, List<String> javacArgs,
+ int monitorPort, File localTemp, boolean cleanBefore, boolean cleanAfter,
+ File deviceRunnerDir) {
super(new EnvironmentDevice(cleanBefore, cleanAfter,
- debugPort, localTemp, deviceRunnerDir),
- timeoutSeconds, sdkJar, javacArgs, tee);
+ debugPort, monitorPort, localTemp, deviceRunnerDir),
+ sdkJar, javacArgs, monitorPort);
}
private EnvironmentDevice getEnvironmentDevice() {
@@ -230,20 +228,11 @@ final class ActivityMode extends Mode {
properties.setProperty(TestProperties.DEVICE_RUNNER_DIR, getEnvironmentDevice().runnerDir.getPath());
}
- @Override protected List<String> executeAction(Action action)
- throws TimeoutException {
- new Command(
- "adb", "shell", "am", "start",
- "-a","android.intent.action.MAIN",
- "-n", (packageName(action) + "/" + TEST_ACTIVITY_CLASS)).executeWithTimeout(timeoutSeconds);
-
- File resultDir = new File(getEnvironmentDevice().runnerDir, action.getName());
- File resultFile = new File(resultDir, TestProperties.RESULT_FILE);
- getEnvironmentDevice().adb.waitForFile(resultFile, timeoutSeconds);
- return new Command.Builder()
- .args("adb", "shell", "cat", resultFile.getPath())
- .tee(tee)
- .build().executeWithTimeout(timeoutSeconds);
+ @Override protected Command createActionCommand(Action action) {
+ return new Command(
+ "adb", "shell", "am", "start", "-W",
+ "-a", "android.intent.action.MAIN",
+ "-n", (packageName(action) + "/" + TEST_ACTIVITY_CLASS));
}
@Override void cleanup(Action action) {
diff --git a/tools/runner/java/vogar/CodeFinder.java b/tools/runner/java/vogar/CodeFinder.java
index 4e4aee7c91..7e357ab671 100644
--- a/tools/runner/java/vogar/CodeFinder.java
+++ b/tools/runner/java/vogar/CodeFinder.java
@@ -27,7 +27,7 @@ import java.util.Set;
public interface CodeFinder {
/**
- * Returns all test runs in the given file or directory. If the returned set
+ * Returns all actions in the given file or directory. If the returned set
* is empty, no executable code of this kind were found.
*/
public Set<Action> findActions(File file);
diff --git a/tools/runner/java/vogar/Console.java b/tools/runner/java/vogar/Console.java
new file mode 100644
index 0000000000..953c660929
--- /dev/null
+++ b/tools/runner/java/vogar/Console.java
@@ -0,0 +1,216 @@
+/*
+ * 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.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Controls, formats and emits output to the command line. Command line output
+ * can be generated both by java.util.logging and by direct calls to this class.
+ */
+public class Console {
+
+ private final boolean stream;
+ private final boolean color;
+ private final String indent;
+
+ private String currentName;
+ private CurrentLine currentLine = CurrentLine.NEW;
+ private final StringBuilder bufferedOutput = new StringBuilder();
+
+ public Console(boolean stream, String indent, boolean color) {
+ this.stream = stream;
+ this.indent = indent;
+ this.color = color;
+ }
+
+ public void configureJavaLogging(boolean verbose) {
+ ConsoleHandler handler = new ConsoleHandler();
+ handler.setLevel(Level.ALL);
+ handler.setFormatter(new Formatter() {
+ @Override public String format(LogRecord r) {
+ return logRecordToString(r);
+ }
+ });
+
+ Logger logger = Logger.getLogger("vogar");
+ logger.setLevel(verbose ? Level.FINE : Level.INFO);
+ logger.addHandler(handler);
+ logger.setUseParentHandlers(false);
+ }
+
+ /**
+ * Formats an alternating sequence of regular log messages and messages
+ * streamed from a foreign process.
+ */
+ private String logRecordToString(LogRecord logRecord) {
+ String message = logRecord.getMessage();
+
+ if (logRecord.getThrown() != null) {
+ StringWriter writer = new StringWriter();
+ writer.write(message);
+ writer.write("\n");
+ logRecord.getThrown().printStackTrace(new PrintWriter(writer));
+ message = writer.toString();
+ }
+
+ newLine();
+ return message + "\n";
+ }
+
+ public void action(String name) {
+ newLine();
+ System.out.print("Action " + name);
+ currentName = name;
+ currentLine = CurrentLine.NAME;
+ }
+
+ /**
+ * Prints the beginning of the named outcome.
+ */
+ public void outcome(String name) {
+ // if the outcome and action names are the same, omit the outcome name
+ if (name.equals(currentName)) {
+ return;
+ }
+
+ currentName = name;
+ newLine();
+ System.out.print(indent + name);
+ currentLine = CurrentLine.NAME;
+ }
+
+ /**
+ * Appends the action output immediately to the stream when streaming is on,
+ * or to a buffer when streaming is off. Buffered output will be held and
+ * printed only if the outcome is unsuccessful.
+ */
+ public void streamOutput(String output) {
+ if (stream) {
+ printOutput(output);
+ } else {
+ bufferedOutput.append(output);
+ }
+ }
+
+ /**
+ * Writes the action's outcome.
+ */
+ public void printResult(Result result, boolean ok) {
+ if (ok) {
+ String prefix = (currentLine == CurrentLine.NAME) ? " " : "\n" + indent;
+ System.out.println(prefix + green("OK (" + result + ")"));
+
+ } else {
+ if (bufferedOutput.length() > 0) {
+ printOutput(bufferedOutput.toString());
+ bufferedOutput.delete(0, bufferedOutput.length());
+ }
+
+ newLine();
+ System.out.println(indent + red("FAIL (" + result + ")"));
+ }
+
+ currentName = null;
+ currentLine = CurrentLine.NEW;
+ }
+
+ /**
+ * Prints the action output with appropriate indentation.
+ */
+ private void printOutput(String streamedOutput) {
+ String[] lines = messageToLines(streamedOutput);
+
+ if (currentLine != CurrentLine.STREAMED_OUTPUT) {
+ newLine();
+ System.out.print(indent);
+ System.out.print(indent);
+ }
+ System.out.print(lines[0]);
+ currentLine = CurrentLine.STREAMED_OUTPUT;
+
+ for (int i = 1; i < lines.length; i++) {
+ newLine();
+
+ if (lines[i].length() > 0) {
+ System.out.print(indent);
+ System.out.print(indent);
+ System.out.print(lines[i]);
+ currentLine = CurrentLine.STREAMED_OUTPUT;
+ }
+ }
+ }
+
+ /**
+ * Inserts a linebreak if necessary.
+ */
+ private void newLine() {
+ if (currentLine == CurrentLine.NEW) {
+ return;
+ }
+
+ System.out.println();
+ currentLine = CurrentLine.NEW;
+ }
+
+ /**
+ * Status of a currently-in-progress line of output.
+ */
+ enum CurrentLine {
+
+ /**
+ * The line is blank.
+ */
+ NEW,
+
+ /**
+ * The line contains streamed application output. Additional streamed
+ * output may be appended without additional line separators or
+ * indentation.
+ */
+ STREAMED_OUTPUT,
+
+ /**
+ * The line contains the name of an action or outcome. The outcome's
+ * result (such as "OK") can be appended without additional line
+ * separators or indentation.
+ */
+ NAME,
+ }
+
+ /**
+ * Returns an array containing the lines of the given text.
+ */
+ private String[] messageToLines(String message) {
+ // pass Integer.MAX_VALUE so split doesn't trim trailing empty strings.
+ return message.split("\r\n|\r|\n", Integer.MAX_VALUE);
+ }
+
+ private String green(String message) {
+ return color ? ("\u001b[32;1m" + message + "\u001b[0m") : message;
+ }
+
+ private String red(String message) {
+ return color ? ("\u001b[31;1m" + message + "\u001b[0m") : message;
+ }
+}
diff --git a/tools/runner/java/vogar/DeviceDalvikVm.java b/tools/runner/java/vogar/DeviceDalvikVm.java
index b94b7f8248..2f98793e97 100644
--- a/tools/runner/java/vogar/DeviceDalvikVm.java
+++ b/tools/runner/java/vogar/DeviceDalvikVm.java
@@ -19,7 +19,6 @@ package vogar;
import vogar.commands.Dx;
import java.io.File;
-import java.io.PrintStream;
import java.util.List;
import java.util.logging.Logger;
@@ -29,11 +28,11 @@ import java.util.logging.Logger;
final class DeviceDalvikVm extends Vm {
private static final Logger logger = Logger.getLogger(DeviceDalvikVm.class.getName());
- DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar, List<String> javacArgs,
- PrintStream tee, File localTemp, List<String> additionalVmArgs,
- boolean cleanBefore, boolean cleanAfter, File runnerDir) {
- super(new EnvironmentDevice(cleanBefore, cleanAfter, debugPort, localTemp, runnerDir),
- timeoutSeconds, sdkJar, javacArgs, tee, additionalVmArgs);
+ DeviceDalvikVm(Integer debugPort, File sdkJar, List<String> javacArgs,
+ int monitorPort, File localTemp, List<String> additionalVmArgs,
+ boolean cleanBefore, boolean cleanAfter, File runnerDir) {
+ super(new EnvironmentDevice(cleanBefore, cleanAfter, debugPort, monitorPort, localTemp,
+ runnerDir), sdkJar, javacArgs, additionalVmArgs, monitorPort);
}
private EnvironmentDevice getEnvironmentDevice() {
diff --git a/tools/runner/java/vogar/Driver.java b/tools/runner/java/vogar/Driver.java
index 4b949098ff..f646fe8596 100644
--- a/tools/runner/java/vogar/Driver.java
+++ b/tools/runner/java/vogar/Driver.java
@@ -16,6 +16,7 @@
package vogar;
+import vogar.commands.Command;
import vogar.commands.Mkdir;
import java.io.File;
@@ -26,16 +27,20 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
/**
* Compiles, installs, runs and reports on actions.
*/
-final class Driver {
+final class Driver implements HostMonitor.Handler {
private static final Logger logger = Logger.getLogger(Driver.class.getName());
@@ -43,8 +48,15 @@ final class Driver {
private final ExpectationStore expectationStore;
private final List<CodeFinder> codeFinders;
private final Mode mode;
- private final String indent;
private final XmlReportPrinter reportPrinter;
+ private final Console console;
+ private final int monitorPort;
+ private final HostMonitor monitor;
+ private final long timeoutSeconds;
+ private int successes = 0;
+ private int failures = 0;
+
+ private Timer actionTimeoutTimer = new Timer("action timeout", true);
private final Map<String, Action> actions = Collections.synchronizedMap(
new LinkedHashMap<String, Action>());
@@ -58,17 +70,21 @@ final class Driver {
private int unsupportedActions = 0;
public Driver(File localTemp, Mode mode, ExpectationStore expectationStore,
- String indent, List<CodeFinder> codeFinders, XmlReportPrinter reportPrinter) {
+ List<CodeFinder> codeFinders, XmlReportPrinter reportPrinter,
+ Console console, HostMonitor monitor, int monitorPort, long timeoutSeconds) {
this.localTemp = localTemp;
this.expectationStore = expectationStore;
this.mode = mode;
- this.indent = indent;
+ this.console = console;
this.codeFinders = codeFinders;
this.reportPrinter = reportPrinter;
+ this.monitor = monitor;
+ this.monitorPort = monitorPort;
+ this.timeoutSeconds = timeoutSeconds;
}
/**
- * Builds and executes all tests in the test directory.
+ * Builds and executes the actions in the given files.
*/
public void buildAndRunAllActions(Collection<File> files) {
if (!actions.isEmpty()) {
@@ -107,7 +123,7 @@ final class Driver {
// the action-specific files.
mode.prepare(runnerJava, runnerClasspath);
- logger.info("Running " + actions.size() + " actions.");
+ logger.info("Actions: " + actions.size());
// build and install actions in a background thread. Using lots of
// threads helps for packages that contain many unsupported actions
@@ -166,57 +182,84 @@ final class Driver {
mode.cleanup(action);
}
- if (unsupportedActions > 0) {
- logger.info("Skipped " + unsupportedActions + " unsupported actions.");
- }
-
if (reportPrinter != null) {
logger.info("Printing XML Reports... ");
int numFiles = reportPrinter.generateReports(outcomes.values());
logger.info(numFiles + " XML files written.");
}
+
+ if (failures > 0 || unsupportedActions > 0) {
+ logger.info(String.format("Outcomes: %s. Passed: %d, Failed: %d, Skipped: %d",
+ (successes + failures), successes, failures, unsupportedActions));
+ } else {
+ logger.info(String.format("Outcomes: %s. All successful.",
+ (successes + failures)));
+ }
}
/**
* Executes a single action and then prints the result.
*/
- private void execute(Action action) {
+ private void execute(final Action action) {
+ console.action(action.getName());
+
Outcome earlyFailure = outcomes.get(action.getName());
- if (earlyFailure != null) {
- if (earlyFailure.getResult() == Result.UNSUPPORTED) {
- logger.fine("skipping " + action.getName());
- unsupportedActions++;
- } else {
- printResult(earlyFailure);
+ if (earlyFailure == null) {
+ final Command command = mode.createActionCommand(action);
+ Future<List<String>> consoleOut = command.executeLater();
+ final AtomicBoolean done = new AtomicBoolean();
+
+ actionTimeoutTimer.schedule(new TimerTask() {
+ @Override public void run() {
+ if (!done.get()) {
+ // TODO: set a "timout" bit somewhere so we know why this failed.
+ // currently we report ERROR for all timeouts.
+ logger.fine("killing " + action.getName() + " because it "
+ + "timed out after " + timeoutSeconds + " seconds");
+ }
+ command.destroy();
+ }
+ }, timeoutSeconds * 1000);
+
+ boolean success = monitor.monitor(monitorPort, this);
+ done.set(true);
+ if (success) {
+ return;
}
- return;
- }
- Set<Outcome> outcomes = mode.run(action);
- for (Outcome outcome : outcomes) {
- printResult(outcome);
+ try {
+ earlyFailure = new Outcome(action.getName(), action.getName(),
+ Result.ERROR, consoleOut.get());
+ } catch (Exception e) {
+ earlyFailure = new Outcome(action.getName(), Result.ERROR, e);
+ }
}
- }
- private void printResult(Outcome outcome) {
- Expectation expected = expectationStore.get(outcome.getName());
- Action action = actions.get(outcome.getActionName());
-
- if (expected.matches(outcome)) {
- logger.info("OK " + outcome.getName() + " (" + outcome.getResult() + ")");
- // In --verbose mode, show the output even on success.
- logger.fine(indent + expected.getFailureMessage(outcome).replace("\n", "\n" + indent));
- return;
+ if (earlyFailure.getResult() == Result.UNSUPPORTED) {
+ logger.fine("skipping " + action.getName());
+ unsupportedActions++;
+ } else {
+ for (String line : earlyFailure.getOutputLines()) {
+ console.streamOutput(line + "\n");
+ }
+ outcome(earlyFailure);
}
+ }
- logger.info("FAIL " + outcome.getName() + " (" + outcome.getResult() + ")");
- String description = action.getDescription();
- if (description != null) {
- logger.info(indent + "\"" + description + "\"");
+ public void outcome(Outcome outcome) {
+ Expectation expectation = expectationStore.get(outcome.getName());
+ boolean ok = expectation.matches(outcome);
+ if (ok) {
+ successes++;
+ } else {
+ failures++;
}
+ console.outcome(outcome.getName());
+ console.printResult(outcome.getResult(), ok);
+ }
- // Don't mess with compiler error output for tools (such as
- // Emacs) that are trying to parse it with regexps
- logger.info(indent + expected.getFailureMessage(outcome).replace("\n", "\n" + indent));
+ public void output(String outcomeName, String output) {
+ console.outcome(outcomeName);
+ console.streamOutput(output);
}
}
diff --git a/tools/runner/java/vogar/Environment.java b/tools/runner/java/vogar/Environment.java
index 5d7f2fb854..8ccfc7b36d 100644
--- a/tools/runner/java/vogar/Environment.java
+++ b/tools/runner/java/vogar/Environment.java
@@ -40,21 +40,21 @@ abstract class Environment {
}
/**
- * Initializes the temporary directories and test harness necessary to run
- * tests.
+ * Initializes the temporary directories and harness necessary to run
+ * actions.
*/
abstract void prepare();
/**
- * Prepares the directory from which the test will be executed. Some tests
- * expect to read data files from the current working directory; this step
- * should ensure such files are available.
+ * Prepares the directory from which the action will be executed. Some
+ * actions expect to read data files from the current working directory;
+ * this step should ensure such files are available.
*/
abstract void prepareUserDir(Action action);
/**
* Deletes files and releases any resources required for the execution of
- * the given test.
+ * the given action.
*/
void cleanup(Action action) {
if (cleanAfter) {
diff --git a/tools/runner/java/vogar/EnvironmentDevice.java b/tools/runner/java/vogar/EnvironmentDevice.java
index 91508f7304..c49bbd98d8 100644
--- a/tools/runner/java/vogar/EnvironmentDevice.java
+++ b/tools/runner/java/vogar/EnvironmentDevice.java
@@ -24,12 +24,14 @@ class EnvironmentDevice extends Environment {
final Adb adb = new Adb();
final File runnerDir;
final File vogarTemp;
+ final int monitorPort;
EnvironmentDevice (boolean cleanBefore, boolean cleanAfter,
- Integer debugPort, File localTemp, File runnerDir) {
+ Integer debugPort, int monitorPort, File localTemp, File runnerDir) {
super(cleanBefore, cleanAfter, debugPort, localTemp);
this.runnerDir = runnerDir;
this.vogarTemp = new File(runnerDir, "/vogar.tmp");
+ this.monitorPort = monitorPort;
}
@Override void prepare() {
@@ -41,6 +43,7 @@ class EnvironmentDevice extends Environment {
adb.mkdir(runnerDir);
adb.mkdir(vogarTemp);
adb.mkdir(new File("/sdcard/dalvik-cache")); // TODO: only necessary on production devices.
+ adb.forwardTcp(monitorPort, monitorPort);
if (debugPort != null) {
adb.forwardTcp(debugPort, debugPort);
}
diff --git a/tools/runner/java/vogar/HostMonitor.java b/tools/runner/java/vogar/HostMonitor.java
new file mode 100644
index 0000000000..6c30d8d2f6
--- /dev/null
+++ b/tools/runner/java/vogar/HostMonitor.java
@@ -0,0 +1,200 @@
+/*
+ * 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 org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Connects to a target process to monitor its action.
+ */
+class HostMonitor {
+
+ private static final Logger logger = Logger.getLogger(HostMonitor.class.getName());
+
+ private final int MAX_CONNECT_ATTEMPTS = 10;
+ private final int CONNECTION_ATTEMPT_DELAY_MILLIS = 1000;
+
+ /**
+ * Connect to the target process on the given port, read all of its
+ * outcomes into {@code handler}, and disconnect.
+ */
+ public boolean monitor(int port, Handler handler) {
+ Socket socket;
+ InputStream in;
+ try {
+ int attempt = 0;
+ do {
+ socket = new Socket("localhost", port);
+ in = new BufferedInputStream(socket.getInputStream());
+ if (checkStream(in)) {
+ logger.fine("action monitor connected to " + socket.getRemoteSocketAddress());
+ break;
+ }
+
+ if (attempt++ == MAX_CONNECT_ATTEMPTS) {
+ throw new IOException("Exceeded max connection attempts!");
+ }
+ logger.fine("connection " + attempt + " to localhost:" + port + " is dead; retrying...");
+ in.close();
+ socket.close();
+ try {
+ Thread.sleep(CONNECTION_ATTEMPT_DELAY_MILLIS);
+ } catch (InterruptedException e) {
+ }
+ } while (true);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Failed to connect to localhost:" + port, e);
+ return false;
+ }
+
+ try {
+ SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+ InputSource inputSource = new InputSource(in);
+ parser.parse(inputSource, new ClientXmlHandler(handler));
+ } catch (ParserConfigurationException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Connection error from localhost:" + port, e);
+ return false;
+ } catch (SAXException e) {
+ logger.log(Level.WARNING, "Received bad XML from localhost:" + port, e);
+ return false;
+ }
+
+ try {
+ socket.close();
+ } catch (IOException ignored) {
+ }
+
+ return true;
+ }
+
+ /**
+ * Somewhere between the host and client process, broken socket connections
+ * are being accepted. Before we try to do any work on such a connection,
+ * check it to make sure it's not dead!
+ *
+ * TODO: file a bug (against adb?) for this
+ */
+ private boolean checkStream(InputStream in) throws IOException {
+ in.mark(1);
+ if (in.read() == -1) {
+ return false;
+ } else {
+ in.reset();
+ return true;
+ }
+ }
+
+ /**
+ * Handles updates on the outcomes of a target process.
+ */
+ public interface Handler {
+
+ /**
+ * Receive a completed outcome.
+ */
+ void outcome(Outcome outcome);
+
+ /**
+ * Receive partial output from an action being executed.
+ */
+ void output(String outcomeName, String output);
+ }
+
+ class ClientXmlHandler extends DefaultHandler {
+ private final Handler handler;
+
+ private String currentOutcomeName;
+ private String currentActionName;
+ private Result currentResult;
+ private StringBuilder output = new StringBuilder();
+
+ ClientXmlHandler(Handler handler) {
+ this.handler = handler;
+ }
+
+ /*
+ * Our XML wire format looks like this:
+ *
+ * <?xml version='1.0' encoding='UTF-8' ?>
+ * <vogar>
+ * <outcome name="java.util.FormatterTest" action="java.util.FormatterTest">
+ * test output
+ * more test output
+ * <result value="SUCCESS" />
+ * </outcome>
+ * </vogar>
+ */
+
+ @Override public void startElement(String uri, String localName,
+ String qName, Attributes attributes) throws SAXException {
+ if (qName.equals("outcome")) {
+ if (currentOutcomeName != null) {
+ throw new IllegalStateException();
+ }
+
+ currentOutcomeName = attributes.getValue("name");
+ currentActionName = attributes.getValue("action");
+ return;
+
+ } else if (qName.equals("result")) {
+ currentResult = Result.valueOf(attributes.getValue("value"));
+ return;
+
+ } else if (!qName.equals("vogar")) {
+ throw new IllegalArgumentException("Unrecognized: " + qName);
+ }
+ }
+
+ @Override public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ if (currentOutcomeName != null) {
+ String text = new String(ch, start, length);
+ output.append(text);
+ handler.output(currentOutcomeName, text);
+ }
+ }
+
+ @Override public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ if (qName.equals("outcome")) {
+ handler.outcome(new Outcome(currentOutcomeName, currentActionName,
+ currentResult, Collections.singletonList(output.toString())));
+ currentOutcomeName = null;
+ currentActionName = null;
+ currentResult = null;
+ output.delete(0, output.length());
+ }
+ }
+ }
+}
diff --git a/tools/runner/java/vogar/JavaVm.java b/tools/runner/java/vogar/JavaVm.java
index e6dc684402..9c4e175cd5 100644
--- a/tools/runner/java/vogar/JavaVm.java
+++ b/tools/runner/java/vogar/JavaVm.java
@@ -17,7 +17,6 @@
package vogar;
import java.io.File;
-import java.io.PrintStream;
import java.util.List;
/**
@@ -27,11 +26,11 @@ final class JavaVm extends Vm {
private final File javaHome;
- JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar, List<String> javacArgs,
- PrintStream tee, File localTemp, File javaHome, List<String> additionalVmArgs,
- boolean cleanBefore, boolean cleanAfter) {
+ JavaVm(Integer debugPort, File sdkJar, List<String> javacArgs, int monitorPort,
+ File localTemp, File javaHome, List<String> additionalVmArgs,
+ boolean cleanBefore, boolean cleanAfter) {
super(new EnvironmentHost(cleanBefore, cleanAfter, debugPort, localTemp),
- timeoutSeconds, sdkJar, javacArgs, tee, additionalVmArgs);
+ sdkJar, javacArgs, additionalVmArgs, monitorPort);
this.javaHome = javaHome;
}
diff --git a/tools/runner/java/vogar/Mode.java b/tools/runner/java/vogar/Mode.java
index 708e58002e..d9032a04ba 100644
--- a/tools/runner/java/vogar/Mode.java
+++ b/tools/runner/java/vogar/Mode.java
@@ -16,6 +16,7 @@
package vogar;
+import vogar.commands.Command;
import vogar.commands.CommandFailedException;
import vogar.commands.Mkdir;
@@ -23,14 +24,12 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
-import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
-import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java.util.regex.Pattern;
@@ -46,10 +45,9 @@ abstract class Mode {
private static final Logger logger = Logger.getLogger(Mode.class.getName());
protected final Environment environment;
- protected final long timeoutSeconds;
protected final File sdkJar;
protected final List<String> javacArgs;
- protected final PrintStream tee;
+ protected final int monitorPort;
/**
* Set of Java files needed to built to tun the currently selected set of
@@ -76,12 +74,11 @@ abstract class Mode {
// TODO: jar up just the junit classes and drop the jar in our lib/ directory.
new File("out/target/common/obj/JAVA_LIBRARIES/core-tests-luni_intermediates/classes.jar").getAbsoluteFile());
- Mode(Environment environment, long timeoutSeconds, File sdkJar, List<String> javacArgs, PrintStream tee) {
+ Mode(Environment environment, File sdkJar, List<String> javacArgs, int monitorPort) {
this.environment = environment;
- this.timeoutSeconds = timeoutSeconds;
this.sdkJar = sdkJar;
this.javacArgs = javacArgs;
- this.tee = tee;
+ this.monitorPort = monitorPort;
}
/**
@@ -165,7 +162,6 @@ abstract class Mode {
Collections.singletonList("Cannot compile: " + action.getJavaFile()));
}
- String qualifiedName = action.getName();
File classesDir = environment.classesDir(action);
new Mkdir().mkdirs(classesDir);
FileOutputStream propertiesOut = new FileOutputStream(
@@ -207,39 +203,13 @@ abstract class Mode {
properties.setProperty(TestProperties.TEST_CLASS, action.getTargetClass());
properties.setProperty(TestProperties.QUALIFIED_NAME, action.getName());
properties.setProperty(TestProperties.RUNNER_CLASS, action.getRunnerClass().getName());
+ properties.setProperty(TestProperties.MONITOR_PORT, String.valueOf(monitorPort));
}
/**
- * Runs the action, and returns its outcomes.
+ * Create the command that executes the action.
*/
- Set<Outcome> run(Action action) {
- List<String> output;
- try {
- output = executeAction(action);
- } catch (TimeoutException e) {
- return Collections.singleton(new Outcome(action.getName(),
- Result.EXEC_TIMEOUT, "Exceeded timeout! (" + timeoutSeconds + "s)"));
- } catch (Exception e) {
- return Collections.singleton(new Outcome(action.getName(), Result.ERROR, e));
- }
- // we only look at the output of the last command
- if (output.isEmpty()) {
- return Collections.singleton(new Outcome(action.getName(),
- Result.ERROR, "No output returned!"));
- }
-
- Result result = TestProperties.RESULT_SUCCESS.equals(output.get(output.size() - 1))
- ? Result.SUCCESS
- : Result.EXEC_FAILED;
- return Collections.singleton(new Outcome(action.getName(),
- action.getName(), result, output.subList(0, output.size() - 1)));
- }
-
- /**
- * Run the actual action to gather output
- */
- protected abstract List<String> executeAction(Action action)
- throws TimeoutException;
+ protected abstract Command createActionCommand(Action action);
/**
* Deletes files and releases any resources required for the execution of
diff --git a/tools/runner/java/vogar/NamingPatternCodeFinder.java b/tools/runner/java/vogar/NamingPatternCodeFinder.java
index 1dd61667c1..d87a35f1cd 100644
--- a/tools/runner/java/vogar/NamingPatternCodeFinder.java
+++ b/tools/runner/java/vogar/NamingPatternCodeFinder.java
@@ -41,7 +41,7 @@ abstract class NamingPatternCodeFinder implements CodeFinder {
}
/**
- * Returns true if {@code file} contains a test class of this type.
+ * Returns true if {@code file} contains a action class of this type.
*/
protected boolean matches(File file) {
return (!file.getName().startsWith(".")
diff --git a/tools/runner/java/vogar/Result.java b/tools/runner/java/vogar/Result.java
index 1574a57266..45c88cee5e 100644
--- a/tools/runner/java/vogar/Result.java
+++ b/tools/runner/java/vogar/Result.java
@@ -22,7 +22,7 @@ package vogar;
public enum Result {
/**
- * A test that cannot be run by this harness, such as a shell script.
+ * An action that cannot be run by this harness, such as a shell script.
*/
UNSUPPORTED,
diff --git a/tools/runner/java/vogar/TestProperties.java b/tools/runner/java/vogar/TestProperties.java
index 08ab3d50a2..d35f349ee3 100644
--- a/tools/runner/java/vogar/TestProperties.java
+++ b/tools/runner/java/vogar/TestProperties.java
@@ -52,11 +52,10 @@ final public class TestProperties {
*/
public static final String DEVICE_RUNNER_DIR = "deviceRunnerDir";
-
/**
- * The output file written by TestActivity
+ * Port to accept monitor connections on.
*/
- public static final String RESULT_FILE = "result.txt";
+ public static final String MONITOR_PORT = "monitorPort";
/**
* Result value for successful test
diff --git a/tools/runner/java/vogar/Vm.java b/tools/runner/java/vogar/Vm.java
index fe94fc2ce3..b03526676e 100644
--- a/tools/runner/java/vogar/Vm.java
+++ b/tools/runner/java/vogar/Vm.java
@@ -26,8 +26,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import java.util.concurrent.TimeoutException;
-import java.util.logging.Logger;
/**
* A Java-like virtual machine for compiling and running tests.
@@ -36,26 +34,23 @@ public abstract class Vm extends Mode {
protected final List<String> additionalVmArgs;
- Vm(Environment environment, long timeoutSeconds, File sdkJar, List<String> javacArgs,
- PrintStream tee, List<String> additionalVmArgs) {
- super(environment, timeoutSeconds, sdkJar, javacArgs,tee);
+ Vm(Environment environment, File sdkJar, List<String> javacArgs,
+ List<String> additionalVmArgs, int monitorPort) {
+ super(environment, sdkJar, javacArgs, monitorPort);
this.additionalVmArgs = additionalVmArgs;
}
/**
- * Returns a VM for test execution.
+ * Returns a VM for action execution.
*/
- @Override protected List<String> executeAction(Action action)
- throws TimeoutException {
- Command command = newVmCommandBuilder(action.getUserDir())
+ @Override protected Command createActionCommand(Action action) {
+ return newVmCommandBuilder(action.getUserDir())
.classpath(getRuntimeSupportClasspath(action))
.userDir(action.getUserDir())
.debugPort(environment.debugPort)
.vmArgs(additionalVmArgs)
.mainClass(TestRunner.class.getName())
- .output(tee)
.build();
- return command.executeWithTimeout(timeoutSeconds);
}
/**
diff --git a/tools/runner/java/vogar/Vogar.java b/tools/runner/java/vogar/Vogar.java
index bfed45eb58..c12c2d52e5 100644
--- a/tools/runner/java/vogar/Vogar.java
+++ b/tools/runner/java/vogar/Vogar.java
@@ -16,23 +16,15 @@
package vogar;
-import java.io.BufferedOutputStream;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Random;
import java.util.Set;
import java.util.UUID;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Formatter;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.logging.Logger;
/**
* Command line interface for running benchmarks and tests on dalvik.
@@ -79,9 +71,11 @@ public final class Vogar {
@Option(names = { "--verbose" })
private boolean verbose;
- @Option(names = { "--tee" })
- private String teeName;
- private PrintStream tee;
+ @Option(names = { "--stream" })
+ private boolean stream;
+
+ @Option(names = { "--color" })
+ private boolean color = true;
@Option(names = { "--debug" })
private Integer debugPort;
@@ -130,8 +124,9 @@ public final class Vogar {
System.out.println(" --clean: synonym for --clean-before and --clean-after (default).");
System.out.println(" Disable with --no-clean if you want no files removed.");
System.out.println();
- System.out.println(" --tee <file>: emit output to file during execution.");
- System.out.println(" Specify '-' for stdout.");
+ System.out.println(" --color: format output in technicolor.");
+ System.out.println();
+ System.out.println(" --stream: stream output as it is emitted.");
System.out.println();
System.out.println(" --timeout-seconds <seconds>: maximum execution time of each");
System.out.println(" action before the runner aborts it. Specifying zero seconds");
@@ -265,23 +260,6 @@ public final class Vogar {
actionFiles.add(new File(actionFilename));
}
- if (teeName != null) {
- if (teeName.equals("-")) {
- tee = System.out;
- } else {
- try {
- tee = new PrintStream(new BufferedOutputStream(new FileOutputStream(teeName)));
- } catch (FileNotFoundException e) {
- System.out.println("Could not open file teeName: " + e);
- return false;
- }
- }
- }
-
- if (verbose) {
- Logger.getLogger("vogar").setLevel(Level.FINE);
- }
-
return true;
}
@@ -292,28 +270,18 @@ public final class Vogar {
private Vogar() {}
- private void prepareLogging() {
- ConsoleHandler handler = new ConsoleHandler();
- handler.setLevel(Level.ALL);
- handler.setFormatter(new Formatter() {
- @Override public String format(LogRecord r) {
- return r.getMessage() + "\n";
- }
- });
- Logger logger = Logger.getLogger("vogar");
- logger.addHandler(handler);
- logger.setUseParentHandlers(false);
- }
-
private void run() {
+ Console console = new Console(options.stream, options.indent, options.color);
+ console.configureJavaLogging(options.verbose);
+
+ int monitorPort = 8787;
Mode mode;
if (options.mode.equals(Options.MODE_DEVICE)) {
mode = new DeviceDalvikVm(
options.debugPort,
- options.timeoutSeconds,
options.sdkJar,
options.javacArgs,
- options.tee,
+ monitorPort,
localTemp,
options.vmArgs,
options.cleanBefore,
@@ -322,22 +290,21 @@ public final class Vogar {
} else if (options.mode.equals(Options.MODE_HOST)) {
mode = new JavaVm(
options.debugPort,
- options.timeoutSeconds,
options.sdkJar,
options.javacArgs,
- options.tee,
+ monitorPort,
localTemp,
options.javaHome,
options.vmArgs,
options.cleanBefore,
- options.cleanAfter);
+ options.cleanAfter
+ );
} else if (options.mode.equals(Options.MODE_ACTIVITY)) {
mode = new ActivityMode(
options.debugPort,
- options.timeoutSeconds,
options.sdkJar,
options.javacArgs,
- options.tee,
+ monitorPort,
localTemp,
options.cleanBefore,
options.cleanAfter,
@@ -347,6 +314,8 @@ public final class Vogar {
return;
}
+ HostMonitor monitor = new HostMonitor();
+
List<CodeFinder> codeFinders = Arrays.asList(
new JtregFinder(localTemp),
new JUnitFinder(),
@@ -369,11 +338,15 @@ public final class Vogar {
localTemp,
mode,
expectationStore,
- options.indent,
codeFinders,
- xmlReportPrinter);
+ xmlReportPrinter,
+ console,
+ monitor,
+ monitorPort,
+ options.timeoutSeconds);
driver.buildAndRunAllActions(options.actionFiles);
+
mode.shutdown();
}
@@ -383,7 +356,6 @@ public final class Vogar {
vogar.options.printUsage();
return;
}
- vogar.prepareLogging();
vogar.run();
}
}
diff --git a/tools/runner/java/vogar/commands/Command.java b/tools/runner/java/vogar/commands/Command.java
index a8ec2a7260..8a014b5ce3 100644
--- a/tools/runner/java/vogar/commands/Command.java
+++ b/tools/runner/java/vogar/commands/Command.java
@@ -49,7 +49,7 @@ public final class Command {
private final File workingDirectory;
private final boolean permitNonZeroExitStatus;
private final PrintStream tee;
- private Process process;
+ private volatile Process process;
public Command(String... args) {
this(Arrays.asList(args));
@@ -73,7 +73,7 @@ public final class Command {
return Collections.unmodifiableList(args);
}
- public synchronized void start() throws IOException {
+ public void start() throws IOException {
if (isStarted()) {
throw new IllegalStateException("Already started!");
}
@@ -94,15 +94,7 @@ public final class Command {
return process != null;
}
- public Process getProcess() {
- if (!isStarted()) {
- throw new IllegalStateException("Not started!");
- }
-
- return process;
- }
-
- public synchronized List<String> gatherOutput()
+ public List<String> gatherOutput()
throws IOException, InterruptedException {
if (!isStarted()) {
throw new IllegalStateException("Not started!");
@@ -130,7 +122,7 @@ public final class Command {
return outputLines;
}
- public synchronized List<String> execute() {
+ public List<String> execute() {
try {
start();
return gatherOutput();
@@ -142,37 +134,54 @@ public final class Command {
}
/**
- * Executes a command with a specified timeout. Output is returned
- * if the command succeeds. If Otherwise null is returned if the
- * command timed out.
+ * 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(long timeoutSeconds)
throws TimeoutException {
- ExecutorService outputReader
- = Executors.newFixedThreadPool(1, Threads.daemonThreadFactory());
+ if (timeoutSeconds == 0) {
+ return execute();
+ }
+
try {
- start();
- // run on a different thread to allow a timeout
- Future<List<String>> future = outputReader.submit(new Callable<List<String>>() {
- public List<String> call() throws Exception {
- return gatherOutput();
- }
- });
- if (timeoutSeconds == 0) {
- return future.get();
- }
- return future.get(timeoutSeconds, TimeUnit.SECONDS);
- } catch (IOException e) {
- throw new RuntimeException("Failed to execute process: " + args, e);
+ 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 {
- if (isStarted()) {
- getProcess().destroy(); // to release the output reader
+ 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 = Executors.newFixedThreadPool(
+ 1, Threads.daemonThreadFactory());
+ Future<List<String>> result = executor.submit(new Callable<List<String>>() {
+ public List<String> call() throws Exception {
+ start();
+ return gatherOutput();
}
- outputReader.shutdown();
+ });
+ executor.shutdown();
+ return result;
+ }
+
+ /**
+ * Destroys the underlying process and closes its associated streams.
+ */
+ public void destroy() {
+ if (process != null) {
+ process.destroy();
}
}
diff --git a/tools/runner/java/vogar/target/CaliperRunner.java b/tools/runner/java/vogar/target/CaliperRunner.java
index f3fc092b09..5b424c86d8 100644
--- a/tools/runner/java/vogar/target/CaliperRunner.java
+++ b/tools/runner/java/vogar/target/CaliperRunner.java
@@ -18,20 +18,27 @@ package vogar.target;
import com.google.caliper.Benchmark;
import com.google.caliper.Runner;
+import vogar.Result;
/**
* Runs a <a href="http://code.google.com/p/caliper/">Caliper</a> benchmark.
*/
public final class CaliperRunner implements vogar.target.Runner {
- public void prepareTest(Class<?> testClass) {}
+ private TargetMonitor monitor;
- public boolean test(Class<?> testClass) {
+ public void init(TargetMonitor monitor, String actionName,
+ Class<?> testClass) {
+ this.monitor = monitor;
+ }
+
+ public void run(String actionName, Class<?> testClass) {
+ monitor.outcomeStarted(actionName, actionName);
try {
Runner.main(testClass.asSubclass(Benchmark.class), new String[0]);
} catch (Exception ex) {
ex.printStackTrace();
}
- return false; // always print benchmarking results
+ monitor.outcomeFinished(Result.SUCCESS);
}
}
diff --git a/tools/runner/java/vogar/target/JUnitRunner.java b/tools/runner/java/vogar/target/JUnitRunner.java
index 12144034a5..767d80d677 100644
--- a/tools/runner/java/vogar/target/JUnitRunner.java
+++ b/tools/runner/java/vogar/target/JUnitRunner.java
@@ -16,19 +16,28 @@
package vogar.target;
+import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestResult;
+import junit.runner.BaseTestRunner;
import junit.runner.TestSuiteLoader;
+import junit.textui.ResultPrinter;
+import vogar.Result;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
- * Runs a JUnit test.
+ * Adapts a JUnit test for use by vogar.
*/
public final class JUnitRunner implements Runner {
- private final junit.textui.TestRunner testRunner;
+ private static final Pattern NAME_THEN_TEST_CLASS = Pattern.compile("(.*)\\(([\\w\\.$]+)\\)");
+
+ private junit.textui.TestRunner testRunner;
private Test junitTest;
- public JUnitRunner() {
+ public void init(TargetMonitor monitor, String actionName, Class<?> testClass) {
final TestSuiteLoader testSuiteLoader = new TestSuiteLoader() {
public Class load(String suiteClassName) throws ClassNotFoundException {
return JUnitRunner.class.getClassLoader().loadClass(suiteClassName);
@@ -39,19 +48,84 @@ public final class JUnitRunner implements Runner {
}
};
- testRunner = new junit.textui.TestRunner() {
+ testRunner = new junit.textui.TestRunner(
+ new MonitoringResultPrinter(monitor, actionName)) {
@Override public TestSuiteLoader getLoader() {
return testSuiteLoader;
}
};
+
+ this.junitTest = testRunner.getTest(testClass.getName());
+ }
+
+ public void run(String actionName, Class<?> testClass) {
+ testRunner.doRun(junitTest);
}
- public void prepareTest(Class<?> testClass) {
- junitTest = testRunner.getTest(testClass.getName());
+ /**
+ * Returns the vogar name like {@code tests.xml.DomTest#testFoo} for a test
+ * with a JUnit name like {@code testFoo(tests.xml.DomTest)}.
+ */
+ private String getOutcomeName(Test test) {
+ String testToString = test.toString();
+
+ Matcher matcher = NAME_THEN_TEST_CLASS.matcher(testToString);
+ if (matcher.matches()) {
+ return matcher.group(2) + "#" + matcher.group(1);
+ }
+
+ return testToString;
}
- public boolean test(Class<?> testClass) {
- TestResult result = testRunner.doRun(junitTest);
- return result.wasSuccessful();
+ /**
+ * This result printer posts test names, output and exceptions to the
+ * hosting process.
+ */
+ private class MonitoringResultPrinter extends ResultPrinter {
+ private final TargetMonitor monitor;
+ private final String actionName;
+
+ private Test current;
+ private Throwable failure;
+
+ public MonitoringResultPrinter(TargetMonitor monitor,
+ String actionName) {
+ super(System.out);
+ this.monitor = monitor;
+ this.actionName = actionName;
+ }
+
+ @Override public void addError(Test test, Throwable t) {
+ System.out.println(BaseTestRunner.getFilteredTrace(t));
+ failure = t;
+ }
+
+ @Override public void addFailure(Test test, AssertionFailedError t) {
+ System.out.println(BaseTestRunner.getFilteredTrace(t));
+ failure = t;
+ }
+
+ @Override public void endTest(Test test) {
+ if (current == null) {
+ throw new IllegalStateException();
+ }
+ monitor.outcomeFinished(
+ failure == null ? Result.SUCCESS : Result.EXEC_FAILED);
+ current = null;
+ failure = null;
+ }
+
+ @Override public void startTest(Test test) {
+ if (current != null) {
+ throw new IllegalStateException();
+ }
+ current = test;
+ monitor.outcomeStarted(getOutcomeName(test), actionName);
+ }
+
+ @Override protected void printHeader(long runTime) {}
+ @Override protected void printErrors(TestResult result) {}
+ @Override protected void printFailures(TestResult result) {}
+ @Override protected void printFooter(TestResult result) {}
}
}
diff --git a/tools/runner/java/vogar/target/JtregRunner.java b/tools/runner/java/vogar/target/JtregRunner.java
index b2710d3181..a6c7f4f377 100644
--- a/tools/runner/java/vogar/target/JtregRunner.java
+++ b/tools/runner/java/vogar/target/JtregRunner.java
@@ -16,6 +16,8 @@
package vogar.target;
+import vogar.Result;
+
import java.lang.reflect.Method;
/**
@@ -24,8 +26,11 @@ import java.lang.reflect.Method;
public final class JtregRunner implements Runner {
private Method main;
+ private TargetMonitor monitor;
- public void prepareTest(Class<?> testClass) {
+ public void init(TargetMonitor monitor, String actionName,
+ Class<?> testClass) {
+ this.monitor = monitor;
try {
main = testClass.getMethod("main", String[].class);
} catch (Exception e) {
@@ -33,13 +38,14 @@ public final class JtregRunner implements Runner {
}
}
- public boolean test(Class<?> testClass) {
+ public void run(String actionName, Class<?> testClass) {
+ monitor.outcomeStarted(actionName, actionName);
try {
main.invoke(null, new Object[] { new String[0] });
- return true;
+ monitor.outcomeFinished(Result.SUCCESS);
} catch (Throwable failure) {
failure.printStackTrace();
- return false;
+ monitor.outcomeFinished(Result.EXEC_FAILED);
}
}
}
diff --git a/tools/runner/java/vogar/target/MainRunner.java b/tools/runner/java/vogar/target/MainRunner.java
index b79dac47bd..c091795145 100644
--- a/tools/runner/java/vogar/target/MainRunner.java
+++ b/tools/runner/java/vogar/target/MainRunner.java
@@ -16,6 +16,8 @@
package vogar.target;
+import vogar.Result;
+
import java.lang.reflect.Method;
/**
@@ -23,9 +25,12 @@ import java.lang.reflect.Method;
*/
public final class MainRunner implements Runner {
+ private TargetMonitor monitor;
private Method main;
- public void prepareTest(Class<?> testClass) {
+ public void init(TargetMonitor monitor, String actionName,
+ Class<?> testClass) {
+ this.monitor = monitor;
try {
main = testClass.getMethod("main", String[].class);
} catch (Exception e) {
@@ -33,12 +38,13 @@ public final class MainRunner implements Runner {
}
}
- public boolean test(Class<?> testClass) {
+ public void run(String actionName, Class<?> testClass) {
+ monitor.outcomeStarted(actionName, actionName);
try {
main.invoke(null, new Object[] { new String[0] });
} catch (Throwable ex) {
ex.printStackTrace();
}
- return false; // always print main method output
+ monitor.outcomeFinished(Result.SUCCESS);
}
}
diff --git a/tools/runner/java/vogar/target/Runner.java b/tools/runner/java/vogar/target/Runner.java
index 528b8cdb7c..af98a00c7f 100644
--- a/tools/runner/java/vogar/target/Runner.java
+++ b/tools/runner/java/vogar/target/Runner.java
@@ -22,7 +22,8 @@ package vogar.target;
*/
public interface Runner {
- public void prepareTest(Class<?> testClass);
+ public void init(TargetMonitor monitor, String actionName,
+ Class<?> testClass);
- public boolean test(Class<?> testClass);
+ public void run(String actionName, Class<?> testClass);
}
diff --git a/tools/runner/java/vogar/target/TargetMonitor.java b/tools/runner/java/vogar/target/TargetMonitor.java
new file mode 100644
index 0000000000..c14c09ff81
--- /dev/null
+++ b/tools/runner/java/vogar/target/TargetMonitor.java
@@ -0,0 +1,107 @@
+/*
+ * 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.target;
+
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+import vogar.Result;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+/**
+ * Accepts a connection for a host process to monitor this action.
+ */
+class TargetMonitor {
+
+ private static final int ACCEPT_TIMEOUT_MILLIS = 10 * 1000;
+
+ private static final String ns = null; // no namespaces
+ ServerSocket serverSocket;
+ private Socket socket;
+ private XmlSerializer serializer;
+
+ public void await(int port) {
+ if (socket != null) {
+ throw new IllegalStateException();
+ }
+
+ try {
+ serverSocket = new ServerSocket(port);
+ serverSocket.setSoTimeout(ACCEPT_TIMEOUT_MILLIS);
+ serverSocket.setReuseAddress(true);
+ socket = serverSocket.accept();
+
+ serializer = XmlPullParserFactory.newInstance().newSerializer();
+ serializer.setOutput(socket.getOutputStream(), "UTF-8");
+ serializer.startDocument("UTF-8", null);
+ serializer.startTag(ns, "vogar");
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to accept a monitor on localhost:" + port, e);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void outcomeStarted(String outcomeName, String actionName) {
+ try {
+ serializer.startTag(ns, "outcome");
+ serializer.attribute(ns, "name", outcomeName);
+ serializer.attribute(ns, "action", actionName);
+ serializer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void output(String text) {
+ try {
+ serializer.text(text);
+ serializer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void outcomeFinished(Result result) {
+ try {
+ serializer.startTag(ns, "result");
+ serializer.attribute(ns, "value", result.name());
+ serializer.endTag(ns, "result");
+ serializer.endTag(ns, "outcome");
+ serializer.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void close() {
+ try {
+ serializer.endTag(ns, "vogar");
+ serializer.endDocument();
+ socket.close();
+ serverSocket.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ socket = null;
+ serverSocket = null;
+ serializer = null;
+ }
+}
diff --git a/tools/runner/java/vogar/target/TestRunner.java b/tools/runner/java/vogar/target/TestRunner.java
index 0271bd8bbb..0eb082c721 100644
--- a/tools/runner/java/vogar/target/TestRunner.java
+++ b/tools/runner/java/vogar/target/TestRunner.java
@@ -20,10 +20,11 @@ import vogar.TestProperties;
import java.io.IOException;
import java.io.InputStream;
+import java.io.PrintStream;
import java.util.Properties;
/**
- * Runs a test.
+ * Runs an action, in process on the target.
*/
public class TestRunner {
@@ -32,12 +33,14 @@ public class TestRunner {
protected final String qualifiedName;
protected final Class<?> testClass;
protected final Class<?> runnerClass;
+ protected final int monitorPort;
- protected TestRunner () {
+ protected TestRunner() {
properties = loadProperties();
qualifiedName = properties.getProperty(TestProperties.QUALIFIED_NAME);
testClass = classProperty(TestProperties.TEST_CLASS, Object.class);
runnerClass = classProperty(TestProperties.RUNNER_CLASS, Runner.class);
+ monitorPort = Integer.parseInt(properties.getProperty(TestProperties.MONITOR_PORT));
}
protected static Properties loadProperties() {
@@ -73,7 +76,19 @@ public class TestRunner {
}
}
- public boolean run() {
+ public void run() {
+ final TargetMonitor monitor = new TargetMonitor();
+ monitor.await(monitorPort);
+
+ PrintStream monitorPrintStream = new PrintStream(System.out) {
+ @Override public void print(String str) {
+ super.print(str);
+ monitor.output(str);
+ }
+ };
+ System.setOut(monitorPrintStream);
+ System.setErr(monitorPrintStream);
+
Runner runner;
try {
runner = (Runner) runnerClass.newInstance();
@@ -82,14 +97,18 @@ public class TestRunner {
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
- runner.prepareTest(testClass);
- return runner.test(testClass);
+ runner.init(monitor, qualifiedName, testClass);
+ runner.run(qualifiedName, testClass);
+
+ monitor.close();
}
+
+
public static void main(String[] args) {
if (args.length != 0) {
throw new RuntimeException("TestRunner doesn't take arguments");
}
- System.out.println(TestProperties.result(new TestRunner().run()));
+ new TestRunner().run();
}
}
diff --git a/tools/runner/lib/TestActivity.java b/tools/runner/lib/TestActivity.java
index c4d9070e9b..6e6d09f198 100644
--- a/tools/runner/lib/TestActivity.java
+++ b/tools/runner/lib/TestActivity.java
@@ -20,11 +20,10 @@ import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
-import vogar.TestProperties;
+import vogar.Threads;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/**
* Runs a user-supplied {@code main(String[] args)} method
@@ -44,62 +43,19 @@ public class TestActivity extends Activity {
this.view = new TextView(this);
log("TestActivity starting...");
setContentView(view);
- ActivityRunner activityRunner = new ActivityRunner();
- activityRunner.run();
- }
- private void log(String message, Throwable ex) {
- log(message + "\n" + Log.getStackTraceString(ex));
+ ExecutorService executor = Executors.newFixedThreadPool(
+ 1, Threads.daemonThreadFactory());
+ executor.submit(new Runnable() {
+ public void run() {
+ new TestRunner().run();
+ }
+ });
+ executor.shutdown();
}
+
private void log(String message) {
Log.i(TAG, message);
view.append(message + "\n");
}
-
- class ActivityRunner extends TestRunner {
-
- private final File runnerDir;
- private final Thread shutdownHook = new Thread(new ShutdownHook());
-
- ActivityRunner() {
- runnerDir = new File(properties.getProperty(TestProperties.DEVICE_RUNNER_DIR));
- }
-
- @Override public boolean run() {
- log("Using " + runnerClass + " to run " + qualifiedName);
- Runtime.getRuntime().addShutdownHook(shutdownHook);
- boolean success = super.run();
- Runtime.getRuntime().removeShutdownHook(shutdownHook);
- writeResultFile(success);
- return success;
- }
-
- private void writeResultFile (boolean success) {
- String result = TestProperties.result(success);
- File resultDir = new File(runnerDir, qualifiedName);
- File resultTemp = new File(resultDir, TestProperties.RESULT_FILE + ".temp");
- File resultFile = new File(resultDir, TestProperties.RESULT_FILE);
- log("TestActivity " + result + " " + resultFile);
- try {
- FileOutputStream resultOut = new FileOutputStream(resultTemp);
- resultOut.write(result.getBytes("UTF-8"));
- resultOut.close();
- // atomically rename since Vogar will be polling for this
- resultTemp.renameTo(resultFile);
- } catch (IOException e) {
- log("TestActivity could not create result file", e);
- }
- }
-
- /**
- * Used to trap tests that try to exit on the their own. We
- * treat this as a failure since they usually are calling
- * System.exit with a non-zero value.
- */
- class ShutdownHook implements Runnable {
- public void run() {
- writeResultFile(false);
- }
- }
- }
}