diff options
author | Jesse Wilson <jessewilson@google.com> | 2010-03-31 23:51:56 -0700 |
---|---|---|
committer | Jesse Wilson <jessewilson@google.com> | 2010-04-01 14:42:05 -0700 |
commit | 575e89934e81c1bebaecee13d3048268648f5adc (patch) | |
tree | 245873cb76eb1f5d49aacf40fab1046eca2f25f1 /tools/runner/java | |
parent | ba6fad59410d74c4dbb19715dea6f5ea09a4b14c (diff) |
New method-level granularity and output streaming for vogar.
Diffstat (limited to 'tools/runner/java')
24 files changed, 866 insertions, 251 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(); } } |