DalvikRunner --tee option to send output to a file or stdout at runtime

Added --tee option so we can watch test output while the test is
running, as opposed to waiting until all the output is collected.

As part of this, Command.Builder can now optionally specifiy a
PrintStream via tee (name from tee(1)).

Added ADB.waitForFile to accompany ADB.waitForNonEmptyDirectory

Removed gross bash wait loop hack in Activity Mode by replacing
Mode.buildCommands with Mode.runTestCommand.

Fixed bug that out generated APK package names did not contain a
required "." in all cases.
diff --git a/tools/runner/java/dalvik/runner/ActivityMode.java b/tools/runner/java/dalvik/runner/ActivityMode.java
index b2b4a3b..163c72a 100644
--- a/tools/runner/java/dalvik/runner/ActivityMode.java
+++ b/tools/runner/java/dalvik/runner/ActivityMode.java
@@ -19,10 +19,12 @@
 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;
@@ -36,11 +38,11 @@
 
     private static final String TEST_ACTIVITY_CLASS   = "dalvik.runner.TestActivity";
 
-    ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp,
+    ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee, File localTemp,
             boolean cleanBefore, boolean cleanAfter, File deviceRunnerDir) {
         super(new EnvironmentDevice(cleanBefore, cleanAfter,
                 debugPort, localTemp, deviceRunnerDir),
-                timeoutSeconds, sdkJar);
+                timeoutSeconds, sdkJar, tee);
     }
 
     private EnvironmentDevice getEnvironmentDevice() {
@@ -148,11 +150,21 @@
         return dex;
     }
 
+    /**
+     * According to android.content.pm.PackageParser, package name
+     * "must have at least one '.' separator" Since the qualified name
+     * may not contain a dot, we prefix containing one to ensure we
+     * are compliant.
+     */
+    private static String packageName(TestRun testRun) {
+        return "DalvikRunner." + testRun.getQualifiedName();
+    }
+
     private File createApk (TestRun testRun, File dex) {
         String androidManifest =
             "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
             "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
-            "      package=\"" + testRun.getQualifiedName() + "\">\n" +
+            "      package=\"" + packageName(testRun) + "\">\n" +
             "    <uses-permission android:name=\"android.permission.INTERNET\" />\n" +
             "    <application>\n" +
             "        <activity android:name=\"" + TEST_ACTIVITY_CLASS + "\">\n" +
@@ -202,7 +214,7 @@
 
     private void installApk(TestRun testRun, File apkSigned) {
         // install the local apk ona the device
-        getEnvironmentDevice().adb.uninstall(testRun.getQualifiedName());
+        getEnvironmentDevice().adb.uninstall(packageName(testRun));
         getEnvironmentDevice().adb.install(apkSigned);
     }
 
@@ -211,40 +223,20 @@
         properties.setProperty(TestProperties.DEVICE_RUNNER_DIR, getEnvironmentDevice().runnerDir.getPath());
     }
 
-    @Override protected List<Command> buildCommands(TestRun testRun) {
-        List<Command> commands = new ArrayList<Command>();
-        commands.add(new Command.Builder()
-            .args("adb")
-            .args("shell")
-            .args("am")
-            .args("start")
-            .args("-a")
-            .args("android.intent.action.MAIN")
-            .args("-n")
-            .args(testRun.getQualifiedName() + "/" + TEST_ACTIVITY_CLASS).build());
+    @Override protected List<String> runTestCommand(TestRun testRun)
+            throws TimeoutException {
+        new Command(
+            "adb", "shell", "am", "start",
+            "-a","android.intent.action.MAIN",
+            "-n", (packageName(testRun) + "/" + TEST_ACTIVITY_CLASS)).executeWithTimeout(timeoutSeconds);
 
         File resultDir = new File(getEnvironmentDevice().runnerDir, testRun.getQualifiedName());
         File resultFile = new File(resultDir, TestProperties.RESULT_FILE);
-        /*
-         * The follow bash script waits for the result file to
-         * exist. It polls once a second to see if it is there with
-         * "adb shell ls". The "tr" is to remove the carriage return
-         * and newline from the adb output. When it does exist, we
-         * "adb shell cat" it so we can see the SUCCESS/FAILURE
-         * results that are expected by Mode.runTest.
-         */
-        // TODO: move loop to Java
-        commands.add(new Command.Builder()
-            .args("bash")
-            .args("-c")
-            .args(
-                    "while [ ! \"`adb shell ls " + resultFile + " | tr -d '\\r\\n'`\" = " +
-                    "        \"" + resultFile + "\" ] ; do " +
-                    "    sleep 1; " +
-                    "done; " +
-                    "adb shell cat " + resultFile).build());
-
-        return commands;
+        getEnvironmentDevice().adb.waitForFile(resultFile, timeoutSeconds);
+        return new Command.Builder()
+            .args("adb", "shell", "cat", resultFile.getPath())
+            .tee(tee)
+            .build().executeWithTimeout(timeoutSeconds);
     }
 
     @Override void cleanup(TestRun testRun) {
diff --git a/tools/runner/java/dalvik/runner/Adb.java b/tools/runner/java/dalvik/runner/Adb.java
index 075ca5f..0ab14ec 100644
--- a/tools/runner/java/dalvik/runner/Adb.java
+++ b/tools/runner/java/dalvik/runner/Adb.java
@@ -58,10 +58,22 @@
     }
 
     /**
+     * Loop until we see a file on the device. For example, wait
+     * result.txt appears.
+     */
+    public void waitForFile(File file, long timeoutSeconds) {
+        waitFor(true, file, timeoutSeconds);
+    }
+
+    /**
      * Loop until we see a non-empty directory on the device. For
      * example, wait until /sdcard is mounted.
      */
-    public void waitForNonEmptyDirectory(File path, int timeoutSeconds) {
+    public void waitForNonEmptyDirectory(File path, long timeoutSeconds) {
+        waitFor(false, path, timeoutSeconds);
+    }
+
+    private void waitFor(boolean file, File path, long timeoutSeconds) {
         final int millisPerSecond = 1000;
         final long start = System.currentTimeMillis();
         final long deadline = start + (millisPerSecond * timeoutSeconds);
@@ -82,8 +94,16 @@
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
-            if (!output.isEmpty()) {
-                return;
+            if (file) {
+                // for files, we expect one line of output that matches the filename
+                if (output.size() == 1 && output.get(0).equals(path.getPath())) {
+                    return;
+                }
+            } else {
+                // for a non empty directory, we just want any output
+                if (!output.isEmpty()) {
+                    return;
+                }
             }
         }
     }
diff --git a/tools/runner/java/dalvik/runner/Command.java b/tools/runner/java/dalvik/runner/Command.java
index 4319cf9..88ba38e 100644
--- a/tools/runner/java/dalvik/runner/Command.java
+++ b/tools/runner/java/dalvik/runner/Command.java
@@ -20,6 +20,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -44,6 +45,7 @@
     private final List<String> args;
     private final File workingDirectory;
     private final boolean permitNonZeroExitStatus;
+    private final PrintStream tee;
     private Process process;
 
     Command(String... args) {
@@ -54,12 +56,14 @@
         this.args = new ArrayList<String>(args);
         this.workingDirectory = null;
         this.permitNonZeroExitStatus = false;
+        this.tee = null;
     }
 
     private Command(Builder builder) {
         this.args = new ArrayList<String>(builder.args);
         this.workingDirectory = builder.workingDirectory;
         this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
+        this.tee = builder.tee;
     }
 
     public List<String> getArgs() {
@@ -106,6 +110,9 @@
         List<String> outputLines = new ArrayList<String>();
         String outputLine;
         while ((outputLine = in.readLine()) != null) {
+            if (tee != null) {
+                tee.println(outputLine);
+            }
             outputLines.add(outputLine);
         }
 
@@ -167,6 +174,7 @@
         private final List<String> args = new ArrayList<String>();
         private File workingDirectory;
         private boolean permitNonZeroExitStatus = false;
+        private PrintStream tee = null;
 
         public Builder args(Object... objects) {
             for (Object object : objects) {
@@ -200,6 +208,11 @@
             return this;
         }
 
+        public Builder tee(PrintStream printStream) {
+            tee = printStream;
+            return this;
+        }
+
         public Command build() {
             return new Command(this);
         }
diff --git a/tools/runner/java/dalvik/runner/DalvikRunner.java b/tools/runner/java/dalvik/runner/DalvikRunner.java
index 015fb9a..c78866e 100644
--- a/tools/runner/java/dalvik/runner/DalvikRunner.java
+++ b/tools/runner/java/dalvik/runner/DalvikRunner.java
@@ -16,8 +16,12 @@
 
 package dalvik.runner;
 
+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;
@@ -72,6 +76,10 @@
         @Option(names = { "--verbose" })
         private boolean verbose;
 
+        @Option(names = { "--tee" })
+        private String teeName;
+        private PrintStream tee;
+
         @Option(names = { "--debug" })
         private Integer debugPort;
 
@@ -116,6 +124,9 @@
             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 test output to file during execution.");
+            System.out.println("      Specify '-' for stdout.");
+            System.out.println();
             System.out.println("  --timeout-seconds <seconds>: maximum execution time of each");
             System.out.println("      test before the runner aborts it.");
             System.out.println("      Default is: " + timeoutSeconds);
@@ -235,6 +246,19 @@
                 testFiles.add(new File(testFilename));
             }
 
+            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("dalvik.runner").setLevel(Level.FINE);
             }
@@ -269,6 +293,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.vmArgs,
                     options.cleanBefore,
@@ -279,6 +304,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.javaHome,
                     options.vmArgs,
@@ -289,6 +315,7 @@
                     options.debugPort,
                     options.timeoutSeconds,
                     options.sdkJar,
+                    options.tee,
                     localTemp,
                     options.cleanBefore,
                     options.cleanAfter,
diff --git a/tools/runner/java/dalvik/runner/DeviceDalvikVm.java b/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
index 7bdf482..061e374 100644
--- a/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
+++ b/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
@@ -17,6 +17,7 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.io.PrintStream;
 import java.util.List;
 import java.util.logging.Logger;
 
@@ -26,11 +27,11 @@
 final class DeviceDalvikVm extends Vm {
     private static final Logger logger = Logger.getLogger(DeviceDalvikVm.class.getName());
 
-    DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar,
+    DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee,
             File localTemp, List<String> additionalVmArgs,
             boolean cleanBefore, boolean cleanAfter, File runnerDir) {
         super(new EnvironmentDevice(cleanBefore, cleanAfter, debugPort, localTemp, runnerDir),
-                timeoutSeconds, sdkJar, additionalVmArgs);
+                timeoutSeconds, sdkJar, tee, additionalVmArgs);
     }
 
     private EnvironmentDevice getEnvironmentDevice() {
diff --git a/tools/runner/java/dalvik/runner/JavaVm.java b/tools/runner/java/dalvik/runner/JavaVm.java
index 2dfc3e7..38e0386 100644
--- a/tools/runner/java/dalvik/runner/JavaVm.java
+++ b/tools/runner/java/dalvik/runner/JavaVm.java
@@ -17,6 +17,7 @@
 package dalvik.runner;
 
 import java.io.File;
+import java.io.PrintStream;
 import java.util.List;
 import java.util.Set;
 
@@ -27,11 +28,11 @@
 
     private final File javaHome;
 
-    JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp,
-            File javaHome, List<String> additionalVmArgs,
+    JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee,
+            File localTemp, File javaHome, List<String> additionalVmArgs,
             boolean cleanBefore, boolean cleanAfter) {
         super(new EnvironmentHost(cleanBefore, cleanAfter, debugPort, localTemp),
-                timeoutSeconds, sdkJar, additionalVmArgs);
+                timeoutSeconds, sdkJar, tee, additionalVmArgs);
         this.javaHome = javaHome;
     }
 
diff --git a/tools/runner/java/dalvik/runner/Mode.java b/tools/runner/java/dalvik/runner/Mode.java
index c2d6b14..0ad7172 100644
--- a/tools/runner/java/dalvik/runner/Mode.java
+++ b/tools/runner/java/dalvik/runner/Mode.java
@@ -20,6 +20,7 @@
 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;
@@ -44,6 +45,7 @@
     protected final Environment environment;
     protected final long timeoutSeconds;
     protected final File sdkJar;
+    protected final PrintStream tee;
 
     /**
      * Set of Java files needed to built to tun the currently selected
@@ -72,10 +74,11 @@
             // 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_intermediates/classes.jar").getAbsoluteFile());
 
-    Mode(Environment environment, long timeoutSeconds, File sdkJar) {
+    Mode(Environment environment, long timeoutSeconds, File sdkJar, PrintStream tee) {
         this.environment = environment;
         this.timeoutSeconds = timeoutSeconds;
         this.sdkJar = sdkJar;
+        this.tee = tee;
     }
 
     /**
@@ -218,20 +221,16 @@
             throw new IllegalArgumentException();
         }
 
-        final List<Command> commands = buildCommands(testRun);
-
-        List<String> output = null;
-        for (final Command command : commands) {
-            try {
-                output = command.executeWithTimeout(timeoutSeconds);
-            } catch (TimeoutException e) {
-                testRun.setResult(Result.EXEC_TIMEOUT,
-                        Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
-                return;
-            } catch (Exception e) {
-                testRun.setResult(Result.ERROR, e);
-                return;
-            }
+        List<String> output;
+        try {
+            output = runTestCommand(testRun);
+        } catch (TimeoutException e) {
+            testRun.setResult(Result.EXEC_TIMEOUT,
+                Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
+            return;
+        } catch (Exception e) {
+            testRun.setResult(Result.ERROR, e);
+            return;
         }
         // we only look at the output of the last command
         if (output.isEmpty()) {
@@ -247,9 +246,10 @@
     }
 
     /**
-     * Returns commands for test execution.
+     * Run the actual test to gather output
      */
-    protected abstract List<Command> buildCommands(TestRun testRun);
+    protected abstract List<String> runTestCommand(TestRun testRun)
+        throws TimeoutException;
 
     /**
      * Deletes files and releases any resources required for the execution of
diff --git a/tools/runner/java/dalvik/runner/Vm.java b/tools/runner/java/dalvik/runner/Vm.java
index 9f96ec5..8ff5858 100644
--- a/tools/runner/java/dalvik/runner/Vm.java
+++ b/tools/runner/java/dalvik/runner/Vm.java
@@ -19,6 +19,7 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -27,6 +28,7 @@
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 import java.util.logging.Logger;
 
 /**
@@ -39,22 +41,25 @@
     protected final List<String> additionalVmArgs;
 
     Vm(Environment environment, long timeoutSeconds, File sdkJar,
-            List<String> additionalVmArgs) {
-        super(environment, timeoutSeconds, sdkJar);
+           PrintStream tee, List<String> additionalVmArgs) {
+        super(environment, timeoutSeconds, sdkJar, tee);
         this.additionalVmArgs = additionalVmArgs;
     }
 
     /**
      * Returns a VM for test execution.
      */
-    @Override protected List<Command> buildCommands(TestRun testRun) {
-        return Collections.singletonList(newVmCommandBuilder(testRun.getUserDir())
+    @Override protected List<String> runTestCommand(TestRun testRun)
+            throws TimeoutException {
+        Command command = newVmCommandBuilder(testRun.getUserDir())
                 .classpath(getRuntimeSupportClasspath(testRun))
                 .userDir(testRun.getUserDir())
                 .debugPort(environment.debugPort)
                 .vmArgs(additionalVmArgs)
                 .mainClass(TestRunner.class.getName())
-                .build());
+                .output(tee)
+                .build();
+        return command.executeWithTimeout(timeoutSeconds);
     }
 
     /**
@@ -78,6 +83,7 @@
         private File userDir;
         private Integer debugPort;
         private String mainClass;
+        private PrintStream output;
         private List<String> vmCommand = Collections.singletonList("java");
         private List<String> vmArgs = new ArrayList<String>();
 
@@ -116,6 +122,11 @@
             return this;
         }
 
+        public VmCommandBuilder output(PrintStream output) {
+            this.output = output;
+            return this;
+        }
+
         public VmCommandBuilder vmArgs(String... vmArgs) {
             return vmArgs(Arrays.asList(vmArgs));
         }
@@ -146,6 +157,8 @@
             builder.args(vmArgs);
             builder.args(mainClass);
 
+            builder.tee(output);
+
             return builder.build();
         }
     }