Supporting expected results for the jtreg test runner.

This is necessary to suppress false positives when dalvik
intentionally differs from the RI. This came up in the fix
for issue 2224903.

Also adding additional features to the jtreg runner to make
it useful as an iterative development tool:
 - debugger support, to debug through running tests
 - verbose output, to aid in diagnosing hangs

Also adding a few ".expected" files for known test failures.
Hopefully this is scalable.

Renaming Run to TestRun and extracting Result as a top-level enum.
diff --git a/libcore/tools/dalvik_jtreg/Android.mk b/libcore/tools/dalvik_jtreg/Android.mk
index f65db2a..8ca5015 100644
--- a/libcore/tools/dalvik_jtreg/Android.mk
+++ b/libcore/tools/dalvik_jtreg/Android.mk
@@ -8,9 +8,11 @@
         java/dalvik/jtreg/CommandFailedException.java \
         java/dalvik/jtreg/Dx.java \
         java/dalvik/jtreg/Dalvikvm.java \
+        java/dalvik/jtreg/ExpectedResult.java \
         java/dalvik/jtreg/Javac.java \
         java/dalvik/jtreg/JtregRunner.java \
         java/dalvik/jtreg/Run.java \
+        java/dalvik/jtreg/Strings.java \
         java/dalvik/jtreg/TestDescriptions.java \
         java/dalvik/jtreg/TestRunner.java \
         java/dalvik/jtreg/TestToDex.java \
diff --git a/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.Big.expected b/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.Big.expected
new file mode 100644
index 0000000..4d1156b
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.Big.expected
@@ -0,0 +1,2 @@
+result=COMPILE_FAILED
+pattern=.*cannot find symbol.*
\ No newline at end of file
diff --git a/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.CopyMethods.expected b/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.CopyMethods.expected
new file mode 100644
index 0000000..4d1156b
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/expectations/java.util.Arrays.CopyMethods.expected
@@ -0,0 +1,2 @@
+result=COMPILE_FAILED
+pattern=.*cannot find symbol.*
\ No newline at end of file
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Adb.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Adb.java
index 71ea690..b7813d8 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Adb.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Adb.java
@@ -32,4 +32,9 @@
         new Command("adb", "push", local.toString(), remote.toString())
                 .execute();
     }
+
+    public void forwardTcp(int localPort, int devicePort) {
+        new Command("adb", "forward", "tcp:" + localPort, "tcp:" + devicePort)
+                .execute();
+    }
 }
\ No newline at end of file
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
index 5558ced..fb644bf 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
@@ -47,21 +47,7 @@
     }
 
     static String path(Object[] objects) {
-        StringBuilder result = new StringBuilder();
-        result.append(objects[0]);
-        for (int i = 1; i < objects.length; i++) {
-            result.append(":").append(objects[i]);
-        }
-        return result.toString();
-    }
-
-    static String[] objectsToStrings(Object[] objects) {
-        String[] result = new String[objects.length];
-        int i = 0;
-        for (Object o : objects) {
-            result[i++] = o.toString();
-        }
-        return result;
+        return Strings.join(objects, ":");
     }
 
     static class Builder {
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Dx.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Dx.java
index 0e50ef6..21bcbbe 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Dx.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Dx.java
@@ -28,7 +28,7 @@
                 .args("dx")
                 .args("--dex")
                 .args("--output=" + output)
-                .args(Command.objectsToStrings(inputs))
+                .args(Strings.objectsToStrings(inputs))
                 .execute();
     }
 }
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/ExpectedResult.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/ExpectedResult.java
new file mode 100644
index 0000000..79ee36c
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/ExpectedResult.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.jtreg;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * The expected outcome of a test execution. This is typically encoded in a
+ * properties file named by the test name and the {@code .expected} suffix; for
+ * example, {@code java.util.Arrays.CopyMethods.expected}.
+ */
+class ExpectedResult {
+
+    /**
+     * Property identifier for the test's expected result, such as {@code
+     * EXEC_FAILED}. This property is required.
+     */
+    static final String RESULT = "result";
+
+    /**
+     * Property identifier for a regular expression that is the expected output
+     * will match. This property is optional.
+     */
+    static final String PATTERN = "pattern";
+
+    /**
+     * The expectation of a general successful test run.
+     */
+    static final ExpectedResult SUCCESS = new ExpectedResult(
+            Result.SUCCESS, ".*");
+
+    private final Result result;
+    private final String pattern;
+
+    private ExpectedResult(File expectationFile) throws IOException {
+        Properties properties = new Properties();
+        FileInputStream in = new FileInputStream(expectationFile);
+        properties.load(in);
+        in.close();
+
+        result = Result.valueOf(properties.getProperty(RESULT));
+        pattern = properties.getProperty(PATTERN);
+    }
+
+    private ExpectedResult(Result result, String pattern) {
+        this.result = result;
+        this.pattern = pattern;
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+
+    public static ExpectedResult forRun(Set<File> searchDirectories,
+            String qualifiedName) throws IOException {
+        for (File expectationDir : searchDirectories) {
+            File expectationFile = new File(expectationDir, qualifiedName + ".expected");
+            if (expectationFile.exists()) {
+                return new ExpectedResult(expectationFile);
+            }
+        }
+
+        return SUCCESS;
+    }
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Javac.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Javac.java
index 4a66abb..c168d5d 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Javac.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Javac.java
@@ -51,7 +51,7 @@
     }
 
     public List<String> compile(File... files) {
-        return builder.args(Command.objectsToStrings(files))
+        return builder.args(Strings.objectsToStrings(files))
                 .execute();
     }
 }
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
index 7fc23f0..0d00b7a 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
@@ -1,4 +1,18 @@
-// Copyright 2009 Google Inc. All Rights Reserved.
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
 package dalvik.jtreg;
 
@@ -6,21 +20,29 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
+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;
+import java.util.regex.Pattern;
 
 /**
  * Runs a directory's worth of jtreg tests on a device.
  */
 public final class JtregRunner {
 
+    private static final Logger logger = Logger.getLogger(JtregRunner.class.getName());
+
     private final File localTemp = new File("/tmp/" + UUID.randomUUID());
     private final File deviceTemp = new File("/data/jtreg" + UUID.randomUUID());
 
@@ -28,6 +50,9 @@
     private final File directoryToScan;
     private final TestToDex testToDex;
 
+    private Integer debugPort;
+    private Set<File> expectationDirs = new LinkedHashSet<File>();
+
     private File deviceTestRunner;
 
     public JtregRunner(File sdkJar, File directoryToScan) {
@@ -35,35 +60,58 @@
         this.testToDex = new TestToDex(sdkJar, localTemp);
     }
 
+    /**
+     * Builds and executes all tests in the test directory.
+     */
     public void buildAndRunAllTests() throws Exception {
         localTemp.mkdirs();
 
-        prepareDevice();
         List<TestDescription> tests = testToDex.findTests(directoryToScan);
+        final BlockingQueue<TestRun> readyToRun = new ArrayBlockingQueue<TestRun>(4);
 
-        // TODO: investigate why tests don't work when run in parallel on device
-        ExecutorService executor = Executors.newFixedThreadPool(1);
-        List<Future<Run>> futures = new ArrayList<Future<Run>>();
+        // build and install tests in a background thread. Using lots of
+        // threads helps for packages that contain many unsupported tests
+        ExecutorService builders = Executors.newFixedThreadPool(8);
         for (final TestDescription testDescription : tests) {
-            futures.add(executor.submit(new Callable<Run>() {
-                public Run call() throws Exception {
-                    return buildAndRunTest(testDescription);
+            builders.submit(new Runnable() {
+                public void run() {
+                    try {
+                        String qualifiedName = TestDescriptions.qualifiedName(testDescription);
+                        ExpectedResult expectedResult = ExpectedResult.forRun(expectationDirs, qualifiedName);
+                        TestRun testRun = new TestRun(qualifiedName, testDescription, expectedResult);
+                        buildAndInstall(testRun);
+                        readyToRun.put(testRun);
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
                 }
-            }));
+            });
         }
+        builders.shutdown();
 
-        for (Future<Run> future : futures) {
-            try {
-                System.out.println(future.get());
-                System.out.println();
-            } catch (ExecutionException e) {
-                throw new RuntimeException(e);
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
+        prepareDevice();
+
+        int unsupportedTests = 0;
+
+        while (!builders.isTerminated() || !readyToRun.isEmpty()) {
+            TestRun testRun = readyToRun.take();
+
+            if (testRun.getResult() == Result.UNSUPPORTED) {
+                logger.fine("skipping " + testRun.getQualifiedName());
+                unsupportedTests++;
+                continue;
             }
+
+            if (testRun.isRunnable()) {
+                runTest(testRun);
+            }
+
+            printResult(testRun);
         }
 
-        executor.shutdown();
+        if (unsupportedTests > 0) {
+            logger.info("Skipped " + unsupportedTests + " unsupported tests.");
+        }
     }
 
     /**
@@ -71,21 +119,26 @@
      * tests on a device.
      */
     private void prepareDevice() {
-        System.out.print("Preparing device...");
         adb.mkdir(deviceTemp);
         File testRunnerJar = testToDex.writeTestRunnerJar();
         adb.push(testRunnerJar, deviceTemp);
         deviceTestRunner = new File(deviceTemp, testRunnerJar.getName());
-        System.out.println("done.");
+        if (debugPort != null) {
+            adb.forwardTcp(debugPort, debugPort);
+        }
+        logger.info("Prepared device.");
     }
 
     /**
-     * Creates a dex file for the given test, pushes it out to the device, and
-     * runs it, returning the test's result.
+     * Creates a dex file for the given test and push it out to the device.
+     *
+     * @return true if building and installing completed successfully.
      */
-    private Run buildAndRunTest(TestDescription testDescription)
-            throws IOException {
-        String qualifiedName = TestDescriptions.qualifiedName(testDescription);
+    private void buildAndInstall(TestRun testRun) {
+        TestDescription testDescription = testRun.getTestDescription();
+        String qualifiedName = testRun.getQualifiedName();
+        logger.fine("building " + testRun.getQualifiedName());
+
         File base = new File(deviceTemp, qualifiedName);
         adb.mkdir(base);
 
@@ -93,49 +146,89 @@
         try {
             dex = testToDex.dexify(testDescription);
             if (dex == null) {
-                return new Run(testDescription, Run.Result.SKIPPED, Collections.<String>emptyList());
+                testRun.initResult(Result.UNSUPPORTED, Collections.<String>emptyList());
+                return;
             }
         } catch (CommandFailedException e) {
-            return new Run(testDescription, Run.Result.COMPILE_FAILED, e.getOutputLines());
+            testRun.initResult(Result.COMPILE_FAILED, e.getOutputLines());
+            return;
         } catch (IOException e) {
-            return new Run(testDescription, Run.Result.ERROR, e);
+            testRun.initResult(Result.ERROR, e);
+            return;
         }
 
+        logger.fine("installing " + testRun.getQualifiedName());
         adb.push(testDescription.getDir(), base);
         adb.push(dex, deviceTemp);
-        File deviceDex = new File(deviceTemp, dex.getName());
-
-        return runTest(testDescription, base, deviceDex);
+        testRun.initInstalledFiles(base, new File(deviceTemp, dex.getName()));
     }
 
     /**
      * Runs the specified test on the device.
-     *
-     * @param base the test's base directory, from which local files can be
-     *      read by the test.
-     * @param dex the jar file containing the test code.
-     * @return the result of executing the test.
      */
-    private Run runTest(TestDescription testDescription, File base, File dex) {
-        List<String> output = new Dalvikvm()
-                .classpath(dex, deviceTestRunner)
-                .args("-Duser.dir=" + base)
-                .exec("dalvik.jtreg.TestRunner");
+    private void runTest(TestRun testRun) {
+        if (!testRun.isRunnable()) {
+            throw new IllegalArgumentException();
+        }
+
+        logger.fine("running " + testRun.getQualifiedName());
+        Dalvikvm vm = new Dalvikvm()
+                .classpath(testRun.getDeviceDex(), deviceTestRunner)
+                .args("-Duser.dir=" + testRun.getBase());
+        if (debugPort != null) {
+            vm.args("-Xrunjdwp:transport=dt_socket,address="
+                    + debugPort + ",server=y,suspend=y");
+        }
+        List<String> output = vm.exec("dalvik.jtreg.TestRunner");
 
         if (output.isEmpty()) {
-            return new Run(testDescription, Run.Result.ERROR,
+            testRun.initResult(Result.ERROR,
                     Collections.singletonList("No output returned!"));
         }
 
-        Run.Result result = "SUCCESS".equals(output.get(output.size() - 1))
-                ? Run.Result.SUCCESS
-                : Run.Result.EXEC_FAILED;
-        return new Run(testDescription, result, output.subList(0, output.size() - 1));
+        Result result = "SUCCESS".equals(output.get(output.size() - 1))
+                ? Result.SUCCESS
+                : Result.EXEC_FAILED;
+        testRun.initResult(result, output.subList(0, output.size() - 1));
+    }
+
+    private void printResult(TestRun testRun) {
+        ExpectedResult expected = testRun.getExpectedResult();
+        boolean patternSuccess;
+
+        if (expected.getPattern() != null) {
+            Pattern pattern = Pattern.compile(expected.getPattern(),
+                    Pattern.MULTILINE | Pattern.DOTALL);
+            patternSuccess = pattern.matcher(Strings.join(testRun.getOutputLines(), "\n")).matches();
+        } else {
+            patternSuccess = true;
+        }
+
+        if (expected.getResult() == testRun.getResult() && patternSuccess) {
+            logger.info("OK " + testRun.getQualifiedName() + " (" + testRun.getResult() + ")");
+            return;
+        }
+
+        logger.info("FAIL " + testRun.getQualifiedName() + " (" + testRun.getResult() + ")");
+        logger.info("  \"" + testRun.getTestDescription().getTitle() + "\"");
+
+        if (expected.getResult() != Result.SUCCESS
+                && expected.getResult() != testRun.getResult()) {
+            logger.info("  Expected result: " + expected.getResult());
+        }
+
+        if (!patternSuccess) {
+            logger.info("  Expected output to match \"" + expected.getPattern() + "\"");
+        }
+
+        for (String output : testRun.getOutputLines()) {
+            logger.info("  " + output);
+        }
     }
 
     public static void main(String[] args) throws Exception {
-        if (args.length != 2) {
-            System.out.println("Usage: JTRegRunner <android_jar> <directoryWithTests>");
+        if (args.length < 2) {
+            System.out.println("Usage: JTRegRunner [options]... <android_jar> <directoryWithTests>");
             System.out.println();
             System.out.println("  android_jar: the API jar file to compile against. Usually");
             System.out.println("      this is <SDK>/platforms/android-<X.X>/android.jar where");
@@ -146,19 +239,66 @@
             System.out.println("      typically this is 'platform_v6/jdk/test' if 'platform_v6'");
             System.out.println("      contains the sources of a platform implementation.");
             System.out.println();
+            System.out.println("OPTIONS");
+            System.out.println();
+            System.out.println("  --debug <port>: enable Java debugging on the specified port.");
+            System.out.println("      This port must be free both on the device and on the local");
+            System.out.println("      system.");
+            System.out.println();
+            System.out.println("  --expectations <directory>: use the specified directory when");
+            System.out.println("      looking for test expectations. The directory should include");
+            System.out.println("      <test>.expected files describing expected results.");
+            System.out.println();
+            System.out.println("  --verbose: turn on verbose output");
+            System.out.println();
             return;
         }
 
-        File sdkJar = new File(args[0]);
+        prepareLogging();
+
+        File sdkJar = new File(args[args.length - 2]);
         if (!sdkJar.exists()) {
-            throw new RuntimeException("No such file: " + sdkJar);
+            throw new RuntimeException("Could not find SDK jar: " + sdkJar);
         }
 
-        File directoryToScan = new File(args[1]);
-        if (!directoryToScan.exists()) {
-            throw new RuntimeException("No such directory: " + directoryToScan);
+        File directoryToScan = new File(args[args.length - 1]);
+        if (!directoryToScan.isDirectory()) {
+            throw new RuntimeException("Invalid test directory: " + directoryToScan);
         }
 
-        new JtregRunner(sdkJar, directoryToScan).buildAndRunAllTests();
+        JtregRunner jtregRunner = new JtregRunner(sdkJar, directoryToScan);
+
+        for (int i = 0; i < args.length - 2; i++) {
+            if ("--debug".equals(args[i])) {
+                jtregRunner.debugPort = Integer.valueOf(args[++i]);
+
+            } else if ("--verbose".equals(args[i])) {
+                Logger.getLogger("dalvik.jtreg").setLevel(Level.FINE);
+
+            } else if ("--expectations".equals(args[i])) {
+                File expectationDir = new File(args[++i]);
+                if (!expectationDir.isDirectory()) {
+                    throw new RuntimeException("Invalid expectation directory: " + directoryToScan);
+                }
+                jtregRunner.expectationDirs.add(expectationDir);
+
+            } else {
+                throw new RuntimeException("Unrecognized option: " + args[i]);
+            }
+        }
+
+        jtregRunner.buildAndRunAllTests();
+    }
+
+    private static 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.addHandler(handler);
+        logger.setUseParentHandlers(false);
     }
 }
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Result.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Result.java
new file mode 100644
index 0000000..1189e76
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Result.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.jtreg;
+
+/**
+ * A test run result.
+ */
+public enum Result {
+
+    /**
+     * A test that cannot be run by this harness, such as a shell script.
+     */
+    UNSUPPORTED,
+
+    COMPILE_FAILED,
+    EXEC_FAILED,
+    ERROR,
+    SUCCESS
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Run.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Run.java
deleted file mode 100644
index 2755d93..0000000
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Run.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2009 Google Inc. All Rights Reserved.
-
-package dalvik.jtreg;
-
-import com.sun.javatest.TestDescription;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * The outcome of a test run.
- */
-public final class Run {
-
-    private final String qualifiedName;
-    private final String title;
-    private final Result result;
-    private final List<String> outputLines;
-
-    public Run(TestDescription testDescription, Result result, List<String> outputLines) {
-        this.qualifiedName = TestDescriptions.qualifiedName(testDescription);
-        this.title = testDescription.getTitle();
-        this.result = result;
-        this.outputLines = outputLines;
-    }
-
-    public Run(TestDescription testDescription, Result result, Exception e) {
-        this.qualifiedName = TestDescriptions.qualifiedName(testDescription);
-        this.title = testDescription.getTitle();
-        this.result = result;
-        this.outputLines = throwableToLines(e);
-    }
-
-    private static List<String> throwableToLines(Throwable t) {
-        StringWriter writer = new StringWriter();
-        PrintWriter out = new PrintWriter(writer);
-        t.printStackTrace(out);
-        return Arrays.asList(writer.toString().split("\\n"));
-    }
-
-    @Override public String toString() {
-        StringBuilder builder = new StringBuilder()
-                .append(qualifiedName).append(" ").append(result)
-                .append("\n  \"").append(title).append("\"");
-
-        for (String output : outputLines) {
-            builder.append("\n  ").append(output);
-        }
-
-        return builder.toString();
-    }
-
-    public enum Result {
-        SKIPPED,
-        COMPILE_FAILED,
-        EXEC_FAILED,
-        ERROR,
-        SUCCESS
-    }
-}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Strings.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Strings.java
new file mode 100644
index 0000000..c06c98a
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Strings.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package dalvik.jtreg;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Utility methods for strings.
+ */
+public class Strings {
+
+    static String join(Object[] objects, String delimiter) {
+        return join(Arrays.asList(objects), delimiter);
+    }
+
+    static String join(Iterable<?> objects, String delimiter) {
+        Iterator<?> i = objects.iterator();
+        if (!i.hasNext()) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder();
+        result.append(i.next());
+        while(i.hasNext()) {
+            result.append(delimiter).append(i.next());
+        }
+        return result.toString();
+    }
+
+    static String[] objectsToStrings(Object[] objects) {
+        String[] result = new String[objects.length];
+        int i = 0;
+        for (Object o : objects) {
+            result[i++] = o.toString();
+        }
+        return result;
+    }
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java
new file mode 100644
index 0000000..aa85528
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.jtreg;
+
+import com.sun.javatest.TestDescription;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A test run and its outcome.
+ */
+public final class TestRun {
+
+    private final TestDescription testDescription;
+    private final String qualifiedName;
+    private final ExpectedResult expectedResult;
+
+    private File base;
+    private File deviceDex;
+
+    private Result result;
+    private List<String> outputLines;
+
+
+    public TestRun(String qualifiedName, TestDescription testDescription,
+            ExpectedResult expectedResult) {
+        this.qualifiedName = qualifiedName;
+        this.testDescription = testDescription;
+        this.expectedResult = expectedResult;
+    }
+
+    public TestDescription getTestDescription() {
+        return testDescription;
+    }
+
+    public String getQualifiedName() {
+        return qualifiedName;
+    }
+
+    /**
+     * Initializes the on-device base directory from which the test program
+     * shall be executed, and the dex file containing that program.
+     */
+    public void initInstalledFiles(File base, File deviceDex) {
+        if (this.base != null) {
+            throw new IllegalStateException();
+        }
+
+        this.base = base;
+        this.deviceDex = deviceDex;
+    }
+
+    /**
+     * Returns true if this test is ready for execution on a device.
+     */
+    public boolean isRunnable() {
+        return base != null && deviceDex != null;
+    }
+
+    /**
+     * Returns the test's base directory, from which local files can be read by
+     * the test.
+     */
+    public File getBase() {
+        return base;
+    }
+
+    /**
+     * Returns the jar file containing the test code.
+     */
+    public File getDeviceDex() {
+        return deviceDex;
+    }
+
+    public void initResult(Result result, Exception e) {
+        initResult(result, throwableToLines(e));
+    }
+
+    public void initResult(Result result, List<String> outputLines) {
+        if (this.result != null) {
+            throw new IllegalStateException();
+        }
+
+        this.result = result;
+        this.outputLines = outputLines;
+    }
+
+    private static List<String> throwableToLines(Throwable t) {
+        StringWriter writer = new StringWriter();
+        PrintWriter out = new PrintWriter(writer);
+        t.printStackTrace(out);
+        return Arrays.asList(writer.toString().split("\\n"));
+    }
+
+    public Result getResult() {
+        return result;
+    }
+
+    public List<String> getOutputLines() {
+        return outputLines;
+    }
+
+    public ExpectedResult getExpectedResult() {
+        return expectedResult;
+    }
+
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java
index 245ec75..6badf34 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java
@@ -30,6 +30,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
+import java.util.logging.Logger;
 import java.util.regex.Pattern;
 
 /**
@@ -47,6 +48,8 @@
     private static final File TEST_RUNNER_JAVA
             = new File(DALVIK_JTREG_HOME + "/java/dalvik/jtreg/TestRunner.java");
 
+    private static final Logger logger = Logger.getLogger(TestToDex.class.getName());
+
     private final Pattern JAVA_TEST_PATTERN = Pattern.compile("\\/(\\w)+\\.java$");
 
     private final File sdkJar;
@@ -115,7 +118,7 @@
      * behind the scenes.
      */
     List<TestDescription> findTests(File directoryToScan) throws Exception {
-        System.out.print("Scanning " + directoryToScan + "...");
+        logger.info("Scanning " + directoryToScan + " for tests.");
         File workDirectory = new File(temp, "JTwork");
         workDirectory.mkdirs();
 
@@ -134,7 +137,7 @@
             TestResult testResult = (TestResult) i.next();
             result.add(testResult.getDescription());
         }
-        System.out.println("done. Found " + result.size() + " tests.");
+        logger.info("Found " + result.size() + " tests.");
         return result;
     }
 }