Changing JtregRunner to support running tests off device.
diff --git a/libcore/tools/dalvik_jtreg/Android.mk b/libcore/tools/dalvik_jtreg/Android.mk
index 7da7212..2998e0c 100644
--- a/libcore/tools/dalvik_jtreg/Android.mk
+++ b/libcore/tools/dalvik_jtreg/Android.mk
@@ -6,16 +6,19 @@
         java/dalvik/jtreg/Adb.java \
         java/dalvik/jtreg/Command.java \
         java/dalvik/jtreg/CommandFailedException.java \
+        java/dalvik/jtreg/DeviceDalvikVm.java \
         java/dalvik/jtreg/Dx.java \
         java/dalvik/jtreg/ExpectedResult.java \
+        java/dalvik/jtreg/Harness.java \
         java/dalvik/jtreg/Javac.java \
+        java/dalvik/jtreg/JavaVm.java \
         java/dalvik/jtreg/JtregRunner.java \
         java/dalvik/jtreg/Result.java \
         java/dalvik/jtreg/Strings.java \
-        java/dalvik/jtreg/TestRun.java \
         java/dalvik/jtreg/TestDescriptions.java \
+        java/dalvik/jtreg/TestRun.java \
         java/dalvik/jtreg/TestRunner.java \
-        java/dalvik/jtreg/TestToDex.java \
+        java/dalvik/jtreg/Vm.java \
         java/dalvik/jtreg/XmlReportPrinter.java \
 
 LOCAL_MODULE:= dalvik_jtreg
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
index 9682a18..14aaadb 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Command.java
@@ -22,8 +22,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.Callable;
 
 /**
  * An out of process executable.
@@ -48,10 +48,18 @@
         this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
     }
 
+    public List<String> getArgs() {
+        return Collections.unmodifiableList(args);
+    }
+
     static String path(Object... objects) {
         return Strings.join(objects, ":");
     }
 
+    static String path(Iterable<?> objects) {
+        return Strings.join(objects, ":");
+    }
+
     public synchronized void start() throws IOException {
         if (isStarted()) {
             throw new IllegalStateException("Already started!");
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/DeviceDalvikVm.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/DeviceDalvikVm.java
new file mode 100644
index 0000000..b49b912
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/DeviceDalvikVm.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util.UUID;
+import java.util.logging.Logger;
+
+/**
+ * Execute tests on a Dalvik VM using an Android device or emulator.
+ */
+final class DeviceDalvikVm extends Vm {
+
+    private static final Logger logger = Logger.getLogger(JtregRunner.class.getName());
+    private final File deviceTemp = new File("/data/jtreg" + UUID.randomUUID());
+
+    private final Adb adb = new Adb();
+    private final File testTemp;
+
+    DeviceDalvikVm(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp) {
+        super(debugPort, timeoutSeconds, sdkJar, localTemp);
+        this.testTemp = new File(deviceTemp, "/tests.tmp");
+    }
+
+    @Override public void prepare() {
+        adb.mkdir(deviceTemp);
+        adb.mkdir(testTemp);
+        if (debugPort != null) {
+            adb.forwardTcp(debugPort, debugPort);
+        }
+        super.prepare();
+    }
+
+    @Override protected File postCompile(File classesDirectory, String name) {
+        logger.fine("dex and push " + name);
+
+        // make the local dex
+        File localDex = new File(localTemp, name + ".jar");
+        new Dx().dex(localDex.toString(), classesDirectory);
+
+        // post the local dex to the device
+        File deviceDex = new File(deviceTemp, localDex.getName());
+        adb.push(localDex, deviceDex);
+
+        return deviceDex;
+    }
+
+    @Override public void shutdown() {
+        super.shutdown();
+        adb.rm(deviceTemp);
+    }
+
+    @Override public void buildAndInstall(TestRun testRun) {
+        super.buildAndInstall(testRun);
+
+        File base = new File(deviceTemp, testRun.getQualifiedName());
+        adb.mkdir(base);
+        adb.push(testRun.getTestDescription().getDir(), base);
+        testRun.setUserDir(base);
+    }
+
+    @Override protected VmCommandBuilder newVmCommandBuilder() {
+        return new VmCommandBuilder()
+                .vmCommand("adb", "shell", "dalvikvm")
+                .vmArgs("-Duser.name=root")
+                .vmArgs("-Duser.language=en")
+                .vmArgs("-Duser.region=US")
+                .vmArgs("-Djavax.net.ssl.trustStore=/system/etc/security/cacerts.bks")
+                .temp(testTemp);
+    }
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Harness.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Harness.java
new file mode 100644
index 0000000..7898232
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Harness.java
@@ -0,0 +1,188 @@
+/*
+ * 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.util.LinkedHashSet;
+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 jtreg tests.
+ */
+public final class Harness {
+
+    private final File localTemp;
+    private File sdkJar;
+    private Integer debugPort;
+    private long timeoutSeconds;
+    private Set<File> expectationDirs = new LinkedHashSet<File>();
+    private File xmlReportsDirectory;
+    private String javaHome;
+    private File directoryToScan;
+
+    private Harness() {
+        localTemp = new File("/tmp/" + UUID.randomUUID());
+        timeoutSeconds = 10 * 60; // default is ten minutes
+        sdkJar = new File("/home/dalvik-prebuild/android-sdk-linux/platforms/android-2.0/android.jar");
+        expectationDirs.add(new File("dalvik/libcore/tools/dalvik_jtreg/expectations"));
+    }
+
+    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("dalvik.jtreg");
+        logger.addHandler(handler);
+        logger.setUseParentHandlers(false);
+    }
+
+    private boolean parseArgs(String[] args) throws Exception {
+        if (args.length == 0) {
+            return false;
+        }
+
+        int i = 0;
+        for (; i < args.length - 1; i++) {
+            if ("--debug".equals(args[i])) {
+                debugPort = Integer.valueOf(args[++i]);
+
+            } else if ("--expectations".equals(args[i])) {
+                File expectationDir = new File(args[++i]);
+                if (!expectationDir.isDirectory()) {
+                    System.out.println("Invalid expectation directory: " + expectationDir);
+                    return false;
+                }
+                expectationDirs.add(expectationDir);
+
+            } else if ("--javaHome".equals(args[i])) {
+                javaHome = args[++i];
+                if (!new File(javaHome, "/bin/java").exists()) {
+                    System.out.println("Invalid java home: " + javaHome);
+                    return false;
+                }
+
+            } else if ("--timeout-seconds".equals(args[i])) {
+                timeoutSeconds = Long.valueOf(args[++i]);
+
+            } else if ("--sdk".equals(args[i])) {
+                sdkJar = new File(args[++i]);
+                if (!sdkJar.exists()) {
+                    System.out.println("Could not find SDK jar: " + sdkJar);
+                    return false;
+                }
+
+            } else if ("--verbose".equals(args[i])) {
+                Logger.getLogger("dalvik.jtreg").setLevel(Level.FINE);
+
+            } else if ("--xml-reports-directory".equals(args[i])) {
+                xmlReportsDirectory = new File(args[++i]);
+                if (!xmlReportsDirectory.isDirectory()) {
+                    System.out.println("Invalid XML reports directory: " + xmlReportsDirectory);
+                    return false;
+                }
+
+            } else {
+                System.out.println("Unrecognized option: " + args[i]);
+                return false;
+            }
+        }
+
+        if (i > args.length - 1) {
+            System.out.println("Missing required test directory option");
+            return false;
+        }
+
+        directoryToScan = new File(args[i]);
+        if (!directoryToScan.isDirectory()) {
+            System.out.println("Invalid test directory: " + directoryToScan);
+            return false;
+        }
+
+        return true;
+    }
+
+    private void printUsage() {
+        System.out.println("Usage: JTRegRunner [options]... <tests directory>");
+        System.out.println();
+        System.out.println("  <tests directory>: a directory to scan for test cases;");
+        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("      Default is: " + expectationDirs);
+        System.out.println();
+        System.out.println("  --javaHome <java_home>: execute the tests on the local workstation");
+        System.out.println("      using the specified java home directory. This does not impact");
+        System.out.println("      which javac gets used. When unset, tests are run on a device");
+        System.out.println("      using adb.");
+        System.out.println();
+        System.out.println("  --sdk <android jar>: the API jar file to compile against.");
+        System.out.println("      Usually this is <SDK>/platforms/android-<X.X>/android.jar");
+        System.out.println("      where <SDK> is the path to an Android SDK path and <X.X> is");
+        System.out.println("      a release version like 1.5.");
+        System.out.println("      Default is: " + sdkJar);
+        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);
+        System.out.println();
+        System.out.println("  --xml-reports-directory <path>: directory to emit JUnit-style");
+        System.out.println("      XML test results.");
+        System.out.println();
+        System.out.println("  --verbose: turn on verbose output");
+        System.out.println();
+    }
+
+    private void run() throws Exception {
+        Vm vm = javaHome != null
+                ? new JavaVm(debugPort, timeoutSeconds, sdkJar, localTemp, javaHome)
+                : new DeviceDalvikVm(debugPort, timeoutSeconds, sdkJar, localTemp);
+        JtregRunner jtregRunner = new JtregRunner(localTemp, directoryToScan,
+                vm, expectationDirs, xmlReportsDirectory);
+        jtregRunner.buildAndRunAllTests();
+        vm.shutdown();
+    }
+
+    public static void main(String[] args) throws Exception {
+        Harness harness = new Harness();
+        if (!harness.parseArgs(args)) {
+            harness.printUsage();
+            return;
+        }
+        harness.prepareLogging();
+        harness.parseArgs(args);
+        harness.run();
+    }
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JavaVm.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JavaVm.java
new file mode 100644
index 0000000..d9bad33
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JavaVm.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * A Java virtual machine like Harmony or the RI.
+ */
+final class JavaVm extends Vm {
+
+    private final String javaHome;
+
+    JavaVm(Integer debugPort, long timeoutSeconds, File sdkJar,
+            File localTemp, String javaHome) {
+        super(debugPort, timeoutSeconds, sdkJar, localTemp);
+        this.javaHome = javaHome;
+    }
+
+    @Override protected VmCommandBuilder newVmCommandBuilder() {
+        return new VmCommandBuilder()
+                .vmCommand(javaHome + "/bin/java");
+    }
+}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
index c6f2700..751757d 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/JtregRunner.java
@@ -17,55 +17,45 @@
 package dalvik.jtreg;
 
 import com.sun.javatest.TestDescription;
+import com.sun.javatest.TestResult;
+import com.sun.javatest.TestResultTable;
+import com.sun.javatest.TestSuite;
+import com.sun.javatest.WorkDirectory;
+import com.sun.javatest.regtest.RegressionTestSuite;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-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 {
+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());
-    private final File testTemp = new File(deviceTemp, "/tests.tmp");
-
-    private final Adb adb = new Adb();
+    private final File localTemp;
     private final File directoryToScan;
-    private final TestToDex testToDex;
-    private final ExecutorService outputReaders = Executors.newFixedThreadPool(1);
+    private final Set<File> expectationDirs;
+    private final Vm vm;
+    private final File xmlReportsDirectory;
 
-    private Integer debugPort;
-    private Set<File> expectationDirs = new LinkedHashSet<File>();
-    private long timeoutSeconds = 10 * 60; // default is ten minutes
-    private File xmlReportsDirectory;
-
-    private File deviceTestRunner;
-
-    public JtregRunner(File sdkJar, File directoryToScan) {
+    public JtregRunner(File localTemp, File directoryToScan, Vm vm,
+            Set<File> expectationDirs, File xmlReportsDirectory) {
+        this.localTemp = localTemp;
         this.directoryToScan = directoryToScan;
-        this.testToDex = new TestToDex(sdkJar, localTemp);
+        this.expectationDirs = expectationDirs;
+        this.vm = vm;
+        this.xmlReportsDirectory = xmlReportsDirectory;
     }
 
     /**
@@ -74,7 +64,7 @@
     public void buildAndRunAllTests() throws Exception {
         localTemp.mkdirs();
 
-        List<TestDescription> tests = testToDex.findTests(directoryToScan);
+        List<TestDescription> tests = findTests(directoryToScan);
         final BlockingQueue<TestRun> readyToRun = new ArrayBlockingQueue<TestRun>(4);
 
         // build and install tests in a background thread. Using lots of
@@ -103,7 +93,7 @@
         }
         builders.shutdown();
 
-        prepareDevice();
+        vm.prepare();
 
         int unsupportedTests = 0;
 
@@ -119,7 +109,7 @@
             }
 
             if (testRun.isRunnable()) {
-                runTest(testRun);
+                vm.runTest(testRun);
             }
 
             printResult(testRun);
@@ -137,109 +127,34 @@
     }
 
     /**
-     * Initializes the temporary directories and test harness necessary to run
-     * tests on a device.
+     * Returns the tests in {@code directoryToScan}.
      */
-    private void prepareDevice() {
-        adb.mkdir(deviceTemp);
-        adb.mkdir(testTemp);
-        File testRunnerJar = testToDex.writeTestRunnerJar();
-        adb.push(testRunnerJar, deviceTemp);
-        deviceTestRunner = new File(deviceTemp, testRunnerJar.getName());
-        if (debugPort != null) {
-            adb.forwardTcp(debugPort, debugPort);
+    List<TestDescription> findTests(File directoryToScan) throws Exception {
+        logger.info("Scanning " + directoryToScan + " for tests.");
+        File workDirectory = new File(localTemp, "JTwork");
+        workDirectory.mkdirs();
+
+        /*
+         * This code is capable of extracting test descriptions using jtreg 4.0
+         * and its bundled copy of jtharness. As a command line tool, jtreg's
+         * API wasn't intended for this style of use. As a consequence, this
+         * code is fragile and may be incompatible with newer versions of jtreg.
+         */
+        TestSuite testSuite = new RegressionTestSuite(directoryToScan);
+        WorkDirectory wd = WorkDirectory.convert(workDirectory, testSuite);
+        TestResultTable resultTable = wd.getTestResultTable();
+
+        List<TestDescription> result = new ArrayList<TestDescription>();
+        for (Iterator i = resultTable.getIterator(); i.hasNext(); ) {
+            TestResult testResult = (TestResult) i.next();
+            result.add(testResult.getDescription());
         }
-        logger.info("Prepared device.");
+        logger.info("Found " + result.size() + " tests.");
+        return 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 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);
-
-        File dex;
-        try {
-            dex = testToDex.dexify(testDescription);
-            if (dex == null) {
-                testRun.setResult(Result.UNSUPPORTED, Collections.<String>emptyList());
-                return;
-            }
-        } catch (CommandFailedException e) {
-            testRun.setResult(Result.COMPILE_FAILED, e.getOutputLines());
-            return;
-        } catch (IOException e) {
-            testRun.setResult(Result.ERROR, e);
-            return;
-        }
-
-        logger.fine("installing " + testRun.getQualifiedName());
-        adb.push(testDescription.getDir(), base);
-        adb.push(dex, deviceTemp);
-        testRun.setInstalledFiles(base, new File(deviceTemp, dex.getName()));
-    }
-
-    /**
-     * Runs the specified test on the device.
-     */
-    private void runTest(TestRun testRun) {
-        if (!testRun.isRunnable()) {
-            throw new IllegalArgumentException();
-        }
-
-        Command.Builder builder = new Command.Builder();
-        builder.args("adb", "shell", "dalvikvm");
-        builder.args("-classpath", Command.path(testRun.getDeviceDex(), deviceTestRunner));
-        builder.args("-Duser.dir=" + testRun.getBase());
-        builder.args("-Duser.name=root");
-        builder.args("-Duser.language=en");
-        builder.args("-Duser.region=US");
-        builder.args("-Djavax.net.ssl.trustStore=/system/etc/security/cacerts.bks");
-        builder.args("-Djava.io.tmpdir=" + testTemp);
-        if (debugPort != null) {
-            builder.args("-Xrunjdwp:transport=dt_socket,address="
-                    + debugPort + ",server=y,suspend=y");
-        }
-        builder.args("dalvik.jtreg.TestRunner");
-        final Command command = builder.build();
-
-        try {
-            command.start();
-
-            // run on a different thread to allow a timeout
-            List<String> output = outputReaders.submit(new Callable<List<String>>() {
-                public List<String> call() throws Exception {
-                    return command.gatherOutput();
-                }
-            }).get(timeoutSeconds, TimeUnit.SECONDS);
-
-            if (output.isEmpty()) {
-                testRun.setResult(Result.ERROR,
-                        Collections.singletonList("No output returned!"));
-                return;
-            }
-
-            Result result = "SUCCESS".equals(output.get(output.size() - 1))
-                    ? Result.SUCCESS
-                    : Result.EXEC_FAILED;
-            testRun.setResult(result, output.subList(0, output.size() - 1));
-        } catch (TimeoutException e) {
-            testRun.setResult(Result.EXEC_TIMEOUT, e);
-        } catch (Exception e) {
-            testRun.setResult(Result.ERROR,
-                    Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
-        } finally {
-            if (command.isStarted()) {
-                command.getProcess().destroy(); // to release the output reader
-            }
-        }
+        vm.buildAndInstall(testRun);
     }
 
     private void printResult(TestRun testRun) {
@@ -275,103 +190,4 @@
             logger.info("  " + output);
         }
     }
-
-    private void shutdown() {
-        adb.rm(deviceTemp);
-        outputReaders.shutdown();
-    }
-
-    public static void main(String[] args) throws Exception {
-        if (args.length < 2) {
-            System.out.println("Usage: JTRegRunner [options]... <android jar> <tests directory>");
-            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");
-            System.out.println("      <SDK> is the path to an Android SDK path and <X.X> is a");
-            System.out.println("      release version like 1.5.");
-            System.out.println();
-            System.out.println("  <tests directory>: a directory to scan for test cases;");
-            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("  --timeout-seconds <seconds>: maximum execution time of each");
-            System.out.println("      test before the runner aborts it.");
-            System.out.println();
-            System.out.println("  --xml-reports-directory <path>: directory to emit JUnit-style");
-            System.out.println("      XML test results.");
-            System.out.println();
-            System.out.println("  --verbose: turn on verbose output");
-            System.out.println();
-            return;
-        }
-
-        prepareLogging();
-
-        File sdkJar = new File(args[args.length - 2]);
-        if (!sdkJar.exists()) {
-            throw new RuntimeException("Could not find SDK jar: " + sdkJar);
-        }
-
-        File directoryToScan = new File(args[args.length - 1]);
-        if (!directoryToScan.isDirectory()) {
-            throw new RuntimeException("Invalid test directory: " + directoryToScan);
-        }
-
-        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 ("--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 if ("--timeout-seconds".equals(args[i])) {
-                jtregRunner.timeoutSeconds = Long.valueOf(args[++i]);
-
-            } else if ("--verbose".equals(args[i])) {
-                Logger.getLogger("dalvik.jtreg").setLevel(Level.FINE);
-
-            } else if ("--xml-reports-directory".equals(args[i])) {
-                jtregRunner.xmlReportsDirectory = new File(args[++i]);
-                if (!jtregRunner.xmlReportsDirectory.isDirectory()) {
-                    throw new RuntimeException("Invalid XML reports directory: "
-                            + jtregRunner.xmlReportsDirectory);
-                }
-
-            } else {
-                throw new RuntimeException("Unrecognized option: " + args[i]);
-            }
-        }
-
-        jtregRunner.buildAndRunAllTests();
-        jtregRunner.shutdown();
-    }
-
-    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 logger = Logger.getLogger("dalvik.jtreg");
-        logger.addHandler(handler);
-        logger.setUseParentHandlers(false);
-    }
 }
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java
index feb919a..e49967e 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRun.java
@@ -25,7 +25,14 @@
 import java.util.List;
 
 /**
- * A test run and its outcome.
+ * A test run and its outcome. This class tracks the complete lifecycle of a
+ * single test run:
+ * <ol>
+ *   <li>the test identity (qualified name)
+ *   <li>the test source code (test description)
+ *   <li>the code to execute (user dir, test classes)
+ *   <li>the result of execution (result, output lines)
+ * </ol>
  */
 public final class TestRun {
 
@@ -33,8 +40,8 @@
     private final String qualifiedName;
     private final ExpectedResult expectedResult;
 
-    private File base;
-    private File deviceDex;
+    private File userDir;
+    private File testClasses;
 
     private Result result;
     private List<String> outputLines;
@@ -56,38 +63,33 @@
     }
 
     /**
-     * Initializes the on-device base directory from which the test program
-     * shall be executed, and the dex file containing that program.
+     * Initializes the directory from which local files can be read by the test.
      */
-    public void setInstalledFiles(File base, File deviceDex) {
-        if (this.base != null) {
-            throw new IllegalStateException();
-        }
+    public void setUserDir(File base) {
+        this.userDir = base;
+    }
 
-        this.base = base;
-        this.deviceDex = deviceDex;
+    public File getUserDir() {
+        return userDir;
     }
 
     /**
-     * Returns true if this test is ready for execution on a device.
+     * Initializes the path to the jar file or directory containing test
+     * classes.
+     */
+    public void setTestClasses(File classes) {
+        this.testClasses = classes;
+    }
+
+    public File getTestClasses() {
+        return testClasses;
+    }
+
+    /**
+     * Returns true if this test is ready for execution.
      */
     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;
+        return userDir != null && testClasses != null;
     }
 
     public void setResult(Result result, Throwable e) {
@@ -121,5 +123,4 @@
     public ExpectedResult getExpectedResult() {
         return expectedResult;
     }
-
 }
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRunner.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRunner.java
index f1b5577..389813f 100644
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRunner.java
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestRunner.java
@@ -23,7 +23,7 @@
 import java.util.Properties;
 
 /**
- * Runs a jtreg test that was prepared with {@link TestToDex}.
+ * Runs a jtreg test.
  */
 public final class TestRunner {
 
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java
deleted file mode 100644
index 6badf34..0000000
--- a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/TestToDex.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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 com.sun.javatest.TestResult;
-import com.sun.javatest.TestResultTable;
-import com.sun.javatest.TestSuite;
-import com.sun.javatest.WorkDirectory;
-import com.sun.javatest.regtest.RegressionTestSuite;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Properties;
-import java.util.logging.Logger;
-import java.util.regex.Pattern;
-
-/**
- * Scans a directory of jtreg tests and creates Dalvik-friendly {@code .jar}
- * files for them. These tests can be executed by {@link TestRunner}. Because of
- * the heavy use of the default package by jtreg tests, it is not generally
- * possible to run multiple tests in the same dalvik VM.
- */
-final class TestToDex {
-
-    private static final String DALVIK_JTREG_HOME
-            = "dalvik/libcore/tools/dalvik_jtreg";
-    private static final File JTREG_JAR
-            = new File(DALVIK_JTREG_HOME + "/lib/jtreg.jar");
-    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;
-    private final File temp;
-
-    TestToDex(File sdkJar, File temp) {
-        this.sdkJar = sdkJar;
-        this.temp = temp;
-    }
-
-    /**
-     * Creates a testrunner jar that can execute the packaged tests.
-     */
-    File writeTestRunnerJar() {
-        File base = new File(temp, "testrunner");
-        base.mkdirs();
-
-        new Javac()
-                .destination(base)
-                .compile(TEST_RUNNER_JAVA);
-
-        File output = new File(temp, "testrunner.jar");
-        new Dx().dex(output.toString(), base);
-        return output;
-    }
-
-    /**
-     * Writes a Dalvik-friendly {@code .jar} for the described test.
-     *
-     * @return the path of the constructed {@code .jar}, or {@code null} if the
-     *      test cannot be converted to Dex (presumably because it is not of
-     *      the right type).
-     * @throws CommandFailedException if javac fails
-     */
-    File dexify(TestDescription testDescription) throws IOException {
-        String qualifiedName = TestDescriptions.qualifiedName(testDescription);
-
-        if (!JAVA_TEST_PATTERN.matcher(testDescription.getFile().toString()).find()) {
-            return null;
-        }
-
-        File jarContents = new File(temp, qualifiedName);
-        jarContents.mkdirs();
-
-        // write a test descriptor
-        Properties properties = TestDescriptions.toProperties(testDescription);
-        FileOutputStream propertiesOut = new FileOutputStream(
-                new File(jarContents, TestRunner.TEST_PROPERTIES_FILE));
-        properties.store(propertiesOut, "generated by " + getClass().getName());
-        propertiesOut.close();
-
-        new Javac()
-                .bootClasspath(sdkJar)
-                .classpath(testDescription.getDir(), JTREG_JAR)
-                .sourcepath(testDescription.getDir())
-                .destination(jarContents)
-                .compile(testDescription.getFile());
-
-        File output = new File(temp, qualifiedName + ".jar");
-        new Dx().dex(output.toString(), jarContents);
-        return output;
-    }
-
-    /**
-     * Scans {@code directoryToScan} for test cases, using JTHarness + jtreg
-     * behind the scenes.
-     */
-    List<TestDescription> findTests(File directoryToScan) throws Exception {
-        logger.info("Scanning " + directoryToScan + " for tests.");
-        File workDirectory = new File(temp, "JTwork");
-        workDirectory.mkdirs();
-
-        /*
-         * This code is capable of extracting test descriptions using jtreg 4.0
-         * and its bundled copy of jtharness. As a command line tool, jtreg's
-         * API wasn't intended for this style of use. As a consequence, this
-         * code is fragile and may be incompatible with newer versions of jtreg.
-         */
-        TestSuite testSuite = new RegressionTestSuite(directoryToScan);
-        WorkDirectory wd = WorkDirectory.convert(workDirectory, testSuite);
-        TestResultTable resultTable = wd.getTestResultTable();
-
-        List<TestDescription> result = new ArrayList<TestDescription>();
-        for (Iterator i = resultTable.getIterator(); i.hasNext(); ) {
-            TestResult testResult = (TestResult) i.next();
-            result.add(testResult.getDescription());
-        }
-        logger.info("Found " + result.size() + " tests.");
-        return result;
-    }
-}
diff --git a/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Vm.java b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Vm.java
new file mode 100644
index 0000000..6c2262e
--- /dev/null
+++ b/libcore/tools/dalvik_jtreg/java/dalvik/jtreg/Vm.java
@@ -0,0 +1,290 @@
+/*
+ * 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.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * A Java-like virtual machine for compiling and running tests.
+ */
+public abstract class Vm {
+
+    static final String DALVIK_JTREG_HOME = "dalvik/libcore/tools/dalvik_jtreg";
+    static final File TEST_RUNNER_JAVA = new File(
+            DALVIK_JTREG_HOME + "/java/dalvik/jtreg/TestRunner.java");
+    private final Pattern JAVA_TEST_PATTERN = Pattern.compile("\\/(\\w)+\\.java$");
+    private static final File JTREG_JAR
+            = new File(DALVIK_JTREG_HOME + "/lib/jtreg.jar");
+
+    private static final Logger logger = Logger.getLogger(Vm.class.getName());
+
+    protected final ExecutorService outputReaders = Executors.newFixedThreadPool(1);
+
+    protected final Integer debugPort;
+    protected final long timeoutSeconds;
+    protected final File sdkJar;
+    protected final File localTemp;
+
+    /** The path to the test runner's compiled classes (directory or jar). */
+    private File testRunnerClasses;
+
+    Vm(Integer debugPort, long timeoutSeconds, File sdkJar, File localTemp) {
+        this.debugPort = debugPort;
+        this.timeoutSeconds = timeoutSeconds;
+        this.sdkJar = sdkJar;
+        this.localTemp = localTemp;
+    }
+
+    /**
+     * Initializes the temporary directories and test harness necessary to run
+     * tests.
+     */
+    public void prepare() {
+        testRunnerClasses = compileTestRunner();
+    }
+
+    private File compileTestRunner() {
+        logger.fine("build testrunner");
+
+        File base = new File(localTemp, "testrunner");
+        base.mkdirs();
+        new Javac()
+                .destination(base)
+                .compile(TEST_RUNNER_JAVA);
+        return postCompile(base, "testrunner");
+    }
+
+    /**
+     * Cleans up after all test runs have completed.
+     */
+    public void shutdown() {
+        outputReaders.shutdown();
+    }
+
+    /**
+     * Compiles classes for the given test and makes them ready for execution.
+     * If the test could not be compiled successfully, it will be updated with
+     * the appropriate test result.
+     */
+    public void buildAndInstall(TestRun testRun) {
+        TestDescription testDescription = testRun.getTestDescription();
+        logger.fine("build " + testRun.getQualifiedName());
+
+        File testClasses;
+        try {
+            testClasses = compileTest(testDescription);
+            if (testClasses == null) {
+                testRun.setResult(Result.UNSUPPORTED, Collections.<String>emptyList());
+                return;
+            }
+        } catch (CommandFailedException e) {
+            testRun.setResult(Result.COMPILE_FAILED, e.getOutputLines());
+            return;
+        } catch (IOException e) {
+            testRun.setResult(Result.ERROR, e);
+            return;
+        }
+        testRun.setTestClasses(testClasses);
+        testRun.setUserDir(testDescription.getDir());
+    }
+
+    /**
+     * Compiles the classes for the described test.
+     *
+     * @return the path to the compiled classes (directory or jar), or {@code
+     *      null} if the test could not be compiled.
+     * @throws CommandFailedException if javac fails
+     */
+    private File compileTest(TestDescription testDescription) throws IOException {
+        String qualifiedName = TestDescriptions.qualifiedName(testDescription);
+
+        if (!JAVA_TEST_PATTERN.matcher(testDescription.getFile().toString()).find()) {
+            return null;
+        }
+
+        File base = new File(localTemp, qualifiedName);
+        base.mkdirs();
+
+        // write a test descriptor
+        Properties properties = TestDescriptions.toProperties(testDescription);
+        FileOutputStream propertiesOut = new FileOutputStream(
+                new File(base, TestRunner.TEST_PROPERTIES_FILE));
+        properties.store(propertiesOut, "generated by " + getClass().getName());
+        propertiesOut.close();
+
+        new Javac()
+                .bootClasspath(sdkJar)
+                .classpath(testDescription.getDir(), JTREG_JAR)
+                .sourcepath(testDescription.getDir())
+                .destination(base)
+                .compile(testDescription.getFile());
+        return postCompile(base, qualifiedName);
+    }
+
+    /**
+     * Runs the test, and updates its test result.
+     */
+    public void runTest(TestRun testRun) {
+        if (!testRun.isRunnable()) {
+            throw new IllegalArgumentException();
+        }
+
+        final Command command = newVmCommandBuilder()
+                .classpath(testRun.getTestClasses(), testRunnerClasses)
+                .userDir(testRun.getUserDir())
+                .debugPort(debugPort)
+                .mainClass("dalvik.jtreg.TestRunner")
+                .build();
+
+        logger.fine("executing " + command.getArgs());
+
+        try {
+            command.start();
+
+            // run on a different thread to allow a timeout
+            List<String> output = outputReaders.submit(new Callable<List<String>>() {
+                public List<String> call() throws Exception {
+                    return command.gatherOutput();
+                }
+            }).get(timeoutSeconds, TimeUnit.SECONDS);
+
+            if (output.isEmpty()) {
+                testRun.setResult(Result.ERROR,
+                        Collections.singletonList("No output returned!"));
+                return;
+            }
+
+            Result result = "SUCCESS".equals(output.get(output.size() - 1))
+                    ? Result.SUCCESS
+                    : Result.EXEC_FAILED;
+            testRun.setResult(result, output.subList(0, output.size() - 1));
+        } catch (TimeoutException e) {
+            testRun.setResult(Result.EXEC_TIMEOUT,
+                    Collections.singletonList("Exceeded timeout! (" + timeoutSeconds + "s)"));
+        } catch (Exception e) {
+            testRun.setResult(Result.ERROR, e);
+        } finally {
+            if (command.isStarted()) {
+                command.getProcess().destroy(); // to release the output reader
+            }
+        }
+    }
+
+    /**
+     * Returns a VM for test execution.
+     */
+    protected VmCommandBuilder newVmCommandBuilder() {
+        return new VmCommandBuilder();
+    }
+
+    /**
+     * Hook method called after each compilation.
+     *
+     * @param classesDirectory the compiled classes
+     * @param name the name of this compilation unit. Usually a qualified test
+     *        name like java.lang.Math.PowTests.
+     * @return the new result file.
+     */
+    protected File postCompile(File classesDirectory, String name) {
+        return classesDirectory;
+    }
+
+    /**
+     * Builds a virtual machine command.
+     */
+    public static class VmCommandBuilder {
+        private File temp;
+        private List<File> classpath = new ArrayList<File>();
+        private File userDir;
+        private Integer debugPort;
+        private String mainClass;
+        private List<String> vmCommand = Collections.singletonList("java");
+        private List<String> vmArgs = Collections.emptyList();
+
+        public VmCommandBuilder vmCommand(String... vmCommand) {
+            this.vmCommand = Arrays.asList(vmCommand.clone());
+            return this;
+        }
+
+        public VmCommandBuilder temp(File temp) {
+            this.temp = temp;
+            return this;
+        }
+
+        public VmCommandBuilder classpath(File... elements) {
+            this.classpath.addAll(Arrays.asList(elements));
+            return this;
+        }
+
+        public VmCommandBuilder userDir(File userDir) {
+            this.userDir = userDir;
+            return this;
+        }
+
+        public VmCommandBuilder debugPort(Integer debugPort) {
+            this.debugPort = debugPort;
+            return this;
+        }
+
+        public VmCommandBuilder mainClass(String mainClass) {
+            this.mainClass = mainClass;
+            return this;
+        }
+
+        public VmCommandBuilder vmArgs(String... vmArgs) {
+            this.vmArgs = Arrays.asList(vmArgs.clone());
+            return this;
+        }
+
+        public Command build() {
+            Command.Builder builder = new Command.Builder();
+            builder.args(vmCommand);
+            builder.args("-classpath", Command.path(classpath));
+            builder.args("-Duser.dir=" + userDir);
+
+            if (temp != null) {
+                builder.args("-Djava.io.tmpdir=" + temp);
+            }
+
+            if (debugPort != null) {
+                builder.args("-Xrunjdwp:transport=dt_socket,address="
+                        + debugPort + ",server=y,suspend=y");
+            }
+
+            builder.args(vmArgs);
+            builder.args(mainClass);
+
+            return builder.build();
+        }
+    }
+}