Teaching DalvikRunner to run arbitrary classes with main() methods.

This came up for the XML test suite, which isn't JUnit but a bunch
of main methods (that ask you to verify their output independently; ugh)

Also setting up the current working directory of the forked process.
This only works for local VMs; setting the working directory for
device VMs causes the "adb shell" call to crash.
diff --git a/libcore/tools/runner/Android.mk b/libcore/tools/runner/Android.mk
index 1400351..ee5c4f1 100644
--- a/libcore/tools/runner/Android.mk
+++ b/libcore/tools/runner/Android.mk
@@ -15,6 +15,7 @@
         java/dalvik/runner/CaliperFinder.java \
         java/dalvik/runner/CaliperRunner.java \
         java/dalvik/runner/Classpath.java \
+        java/dalvik/runner/CodeFinder.java \
         java/dalvik/runner/Command.java \
         java/dalvik/runner/CommandFailedException.java \
         java/dalvik/runner/DalvikRunner.java \
@@ -28,10 +29,12 @@
         java/dalvik/runner/Javac.java \
         java/dalvik/runner/JtregFinder.java \
         java/dalvik/runner/JtregRunner.java \
+        java/dalvik/runner/MainFinder.java \
+        java/dalvik/runner/MainRunner.java \
+        java/dalvik/runner/NamingPatternCodeFinder.java \
         java/dalvik/runner/Result.java \
         java/dalvik/runner/Strings.java \
         java/dalvik/runner/TestRun.java \
-        java/dalvik/runner/TestFinder.java \
         java/dalvik/runner/TestRunner.java \
         java/dalvik/runner/Threads.java \
         java/dalvik/runner/Vm.java \
diff --git a/libcore/tools/runner/java/dalvik/runner/CaliperFinder.java b/libcore/tools/runner/java/dalvik/runner/CaliperFinder.java
index e60947d..77d85dc 100644
--- a/libcore/tools/runner/java/dalvik/runner/CaliperFinder.java
+++ b/libcore/tools/runner/java/dalvik/runner/CaliperFinder.java
@@ -22,7 +22,7 @@
  * Create {@link TestRun}s for {@code .java} files with Caliper benchmarks in
  * them.
  */
-class CaliperFinder extends TestFinder {
+class CaliperFinder extends NamingPatternCodeFinder {
 
     @Override protected boolean matches(File file) {
         return file.getName().endsWith("Benchmark.java");
diff --git a/libcore/tools/runner/java/dalvik/runner/CodeFinder.java b/libcore/tools/runner/java/dalvik/runner/CodeFinder.java
new file mode 100644
index 0000000..1d4a95a
--- /dev/null
+++ b/libcore/tools/runner/java/dalvik/runner/CodeFinder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.runner;
+
+import java.io.File;
+import java.util.Set;
+
+/**
+ * A strategy for finding runnable things in a directory.
+ */
+public interface CodeFinder {
+
+    /**
+     * Returns all test runs in the given file or directory. If the returned set
+     * is empty, no executable code of this kind were found.
+     */
+    public Set<TestRun> findTests(File file);
+}
diff --git a/libcore/tools/runner/java/dalvik/runner/Command.java b/libcore/tools/runner/java/dalvik/runner/Command.java
index 553ee24..8904711 100644
--- a/libcore/tools/runner/java/dalvik/runner/Command.java
+++ b/libcore/tools/runner/java/dalvik/runner/Command.java
@@ -17,6 +17,7 @@
 package dalvik.runner;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
@@ -34,6 +35,7 @@
     private final Logger logger = Logger.getLogger(Command.class.getName());
 
     private final List<String> args;
+    private final File workingDirectory;
     private final boolean permitNonZeroExitStatus;
     private Process process;
 
@@ -43,11 +45,13 @@
 
     Command(List<String> args) {
         this.args = new ArrayList<String>(args);
+        this.workingDirectory = null;
         this.permitNonZeroExitStatus = false;
     }
 
     private Command(Builder builder) {
         this.args = new ArrayList<String>(builder.args);
+        this.workingDirectory = builder.workingDirectory;
         this.permitNonZeroExitStatus = builder.permitNonZeroExitStatus;
     }
 
@@ -62,10 +66,14 @@
 
         logger.fine("executing " + Strings.join(args, " "));
 
-        process = new ProcessBuilder()
+        ProcessBuilder processBuilder = new ProcessBuilder()
                 .command(args)
-                .redirectErrorStream(true)
-                .start();
+                .redirectErrorStream(true);
+        if (workingDirectory != null) {
+            processBuilder.directory(workingDirectory);
+        }
+
+        process = processBuilder.start();
     }
 
     public boolean isStarted() {
@@ -118,6 +126,7 @@
 
     static class Builder {
         private final List<String> args = new ArrayList<String>();
+        private File workingDirectory;
         private boolean permitNonZeroExitStatus = false;
 
         public Builder args(String... args) {
@@ -129,6 +138,17 @@
             return this;
         }
 
+        /**
+         * Sets the working directory from which the command will be executed.
+         * This must be a <strong>local</strong> directory; Commands run on
+         * remote devices (ie. via {@code adb shell}) require a local working
+         * directory.
+         */
+        public Builder workingDirectory(File workingDirectory) {
+            this.workingDirectory = workingDirectory;
+            return this;
+        }
+
         public Builder permitNonZeroExitStatus() {
             permitNonZeroExitStatus = true;
             return this;
diff --git a/libcore/tools/runner/java/dalvik/runner/DalvikRunner.java b/libcore/tools/runner/java/dalvik/runner/DalvikRunner.java
index f968b96..5c5075a 100644
--- a/libcore/tools/runner/java/dalvik/runner/DalvikRunner.java
+++ b/libcore/tools/runner/java/dalvik/runner/DalvikRunner.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -174,12 +175,13 @@
                 ? new JavaVm(debugPort, timeoutSeconds, sdkJar, localTemp, javaHome, clean)
                 : new DeviceDalvikVm(debugPort, timeoutSeconds, sdkJar, localTemp,
                         clean, deviceRunnerDir);
-        JtregFinder jtregFinder = new JtregFinder(localTemp);
-        JUnitFinder jUnitFinder = new JUnitFinder();
-        CaliperFinder caliperFinder = new CaliperFinder();
+        List<CodeFinder> codeFinders = Arrays.asList(
+                new JtregFinder(localTemp),
+                new JUnitFinder(),
+                new CaliperFinder(),
+                new MainFinder());
         Driver driver = new Driver(localTemp,
-                vm, expectationFiles, xmlReportsDirectory, jtregFinder,
-                jUnitFinder, caliperFinder);
+                vm, expectationFiles, xmlReportsDirectory, codeFinders);
         driver.loadExpectations();
         driver.buildAndRunAllTests(testFiles);
         vm.shutdown();
diff --git a/libcore/tools/runner/java/dalvik/runner/DeviceDalvikVm.java b/libcore/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
index 87c4605..d95e948 100644
--- a/libcore/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
+++ b/libcore/tools/runner/java/dalvik/runner/DeviceDalvikVm.java
@@ -75,9 +75,7 @@
         }
     }
 
-    @Override public void buildAndInstall(TestRun testRun) {
-        super.buildAndInstall(testRun);
-
+    @Override protected void prepareUserDir(TestRun testRun) {
         File testClassesDirOnDevice = testClassesDirOnDevice(testRun);
         adb.mkdir(testClassesDirOnDevice);
         adb.push(testRun.getTestDirectory(), testClassesDirOnDevice);
@@ -96,7 +94,10 @@
         return new File(runnerDir, testRun.getQualifiedName());
     }
 
-    @Override protected VmCommandBuilder newVmCommandBuilder() {
+    @Override protected VmCommandBuilder newVmCommandBuilder(
+            File workingDirectory) {
+        // ignore the working directory; it's device-local and we can't easily
+        // set the working directory for commands run via adb shell.
         return new VmCommandBuilder()
                 .vmCommand("adb", "shell", "dalvikvm")
                 .vmArgs("-Duser.name=root")
diff --git a/libcore/tools/runner/java/dalvik/runner/Driver.java b/libcore/tools/runner/java/dalvik/runner/Driver.java
index 78ab3dd..7a30ab7 100644
--- a/libcore/tools/runner/java/dalvik/runner/Driver.java
+++ b/libcore/tools/runner/java/dalvik/runner/Driver.java
@@ -41,9 +41,7 @@
 
     private final File localTemp;
     private final Set<File> expectationFiles;
-    private final JtregFinder jtregFinder;
-    private final JUnitFinder junitFinder;
-    private final CaliperFinder caliperFinder;
+    private final List<CodeFinder> codeFinders;
     private final Vm vm;
     private final File xmlReportsDirectory;
     private final Map<String, ExpectedResult> expectedResults = new HashMap<String, ExpectedResult>();
@@ -55,15 +53,12 @@
     private int unsupportedTests = 0;
 
     public Driver(File localTemp, Vm vm, Set<File> expectationFiles,
-            File xmlReportsDirectory, JtregFinder jtregFinder,
-            JUnitFinder junit, CaliperFinder caliperFinder) {
+            File xmlReportsDirectory, List<CodeFinder> codeFinders) {
         this.localTemp = localTemp;
         this.expectationFiles = expectationFiles;
         this.vm = vm;
         this.xmlReportsDirectory = xmlReportsDirectory;
-        this.jtregFinder = jtregFinder;
-        this.junitFinder = junit;
-        this.caliperFinder = caliperFinder;
+        this.codeFinders = codeFinders;
     }
 
     public void loadExpectations() throws IOException {
@@ -86,18 +81,16 @@
         for (File testFile : testFiles) {
             Set<TestRun> testsForFile = Collections.emptySet();
 
-            if (testFile.isDirectory()) {
-                testsForFile = jtregFinder.findTests(testFile);
-                logger.fine("found " + testsForFile.size() + " jtreg tests for " + testFile);
+            for (CodeFinder codeFinder : codeFinders) {
+                testsForFile = codeFinder.findTests(testFile);
+
+                // break as soon as we find any match. We don't need multiple
+                // matches for the same file, since that would run it twice.
+                if (!testsForFile.isEmpty()) {
+                    break;
+                }
             }
-            if (testsForFile.isEmpty()) {
-                testsForFile = junitFinder.findTests(testFile);
-                logger.fine("found " + testsForFile.size() + " JUnit tests for " + testFile);
-            }
-            if (testsForFile.isEmpty()) {
-                testsForFile = caliperFinder.findTests(testFile);
-                logger.fine("found " + testsForFile.size() + " Caliper benchmarks for " + testFile);
-            }
+
             tests.addAll(testsForFile);
         }
 
diff --git a/libcore/tools/runner/java/dalvik/runner/JUnitFinder.java b/libcore/tools/runner/java/dalvik/runner/JUnitFinder.java
index 0d1ad86..b446a39 100644
--- a/libcore/tools/runner/java/dalvik/runner/JUnitFinder.java
+++ b/libcore/tools/runner/java/dalvik/runner/JUnitFinder.java
@@ -21,7 +21,7 @@
 /**
  * Create {@link TestRun}s for {@code .java} files with JUnit tests in them.
  */
-class JUnitFinder extends TestFinder {
+class JUnitFinder extends NamingPatternCodeFinder {
 
     @Override protected boolean matches(File file) {
         return file.getName().endsWith("Test.java");
diff --git a/libcore/tools/runner/java/dalvik/runner/JavaVm.java b/libcore/tools/runner/java/dalvik/runner/JavaVm.java
index 6e44d1f..c1eab3e 100644
--- a/libcore/tools/runner/java/dalvik/runner/JavaVm.java
+++ b/libcore/tools/runner/java/dalvik/runner/JavaVm.java
@@ -19,7 +19,7 @@
 import java.io.File;
 
 /**
- * A Java virtual machine like Harmony or the RI.
+ * A local Java virtual machine like Harmony or the RI.
  */
 final class JavaVm extends Vm {
 
@@ -31,8 +31,10 @@
         this.javaHome = javaHome;
     }
 
-    @Override protected VmCommandBuilder newVmCommandBuilder() {
+    @Override protected VmCommandBuilder newVmCommandBuilder(
+            File workingDirectory) {
         return new VmCommandBuilder()
-                .vmCommand(javaHome + "/bin/java");
+                .vmCommand(javaHome + "/bin/java")
+                .workingDir(workingDirectory);
     }
 }
diff --git a/libcore/tools/runner/java/dalvik/runner/JtregFinder.java b/libcore/tools/runner/java/dalvik/runner/JtregFinder.java
index f02c492..c4e865c 100644
--- a/libcore/tools/runner/java/dalvik/runner/JtregFinder.java
+++ b/libcore/tools/runner/java/dalvik/runner/JtregFinder.java
@@ -24,7 +24,7 @@
 import com.sun.javatest.regtest.RegressionTestSuite;
 
 import java.io.File;
-import java.io.FileNotFoundException;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Set;
@@ -33,7 +33,7 @@
 /**
  * Create {@link TestRun}s for {@code .java} files with jtreg tests in them.
  */
-class JtregFinder {
+class JtregFinder implements CodeFinder {
 
     // TODO: add support for the  @library directive, as seen in
     //   test/com/sun/crypto/provider/Cipher/AES/TestKATForECB_VT.java
@@ -56,37 +56,44 @@
     /**
      * Returns the tests in {@code directoryToScan}.
      */
-    public Set<TestRun> findTests(File directoryToScan)
-            throws TestSuite.Fault, WorkDirectory.InitializationFault,
-            FileNotFoundException, WorkDirectory.WorkDirectoryExistsFault,
-            WorkDirectory.BadDirectoryFault, TestResult.Fault {
-        logger.fine("scanning " + directoryToScan + " for jtreg 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();
-
-        Set<TestRun> result = new LinkedHashSet<TestRun>();
-        for (Iterator i = resultTable.getIterator(); i.hasNext(); ) {
-            TestResult testResult = (TestResult) i.next();
-            TestDescription description = testResult.getDescription();
-            String qualifiedName = qualifiedName(description);
-            String suiteName = suiteName(description);
-            String testName = description.getName();
-            String testClass = description.getName();
-            result.add(new TestRun(description.getDir(), description.getFile(),
-                    testClass, suiteName, testName, qualifiedName,
-                    description.getTitle(), JtregRunner.class));
+    public Set<TestRun> findTests(File directoryToScan) {
+        // for now, jtreg doesn't know how to scan anything but directories
+        if (!directoryToScan.isDirectory()) {
+            return Collections.emptySet();
         }
-        return result;
+
+        try {
+            logger.fine("scanning " + directoryToScan + " for jtreg 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();
+
+            Set<TestRun> result = new LinkedHashSet<TestRun>();
+            for (Iterator i = resultTable.getIterator(); i.hasNext(); ) {
+                TestResult testResult = (TestResult) i.next();
+                TestDescription description = testResult.getDescription();
+                String qualifiedName = qualifiedName(description);
+                String suiteName = suiteName(description);
+                String testName = description.getName();
+                String testClass = description.getName();
+                result.add(new TestRun(description.getDir(), description.getFile(),
+                        testClass, suiteName, testName, qualifiedName,
+                        description.getTitle(), JtregRunner.class));
+            }
+            return result;
+        } catch (Exception jtregFailure) {
+            // jtreg shouldn't fail in practice
+            throw new RuntimeException(jtregFailure);
+        }
     }
 
     /**
diff --git a/libcore/tools/runner/java/dalvik/runner/MainFinder.java b/libcore/tools/runner/java/dalvik/runner/MainFinder.java
new file mode 100644
index 0000000..0ebcb43
--- /dev/null
+++ b/libcore/tools/runner/java/dalvik/runner/MainFinder.java
@@ -0,0 +1,37 @@
+/*
+ * 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.runner;
+
+import java.io.File;
+
+/**
+ * Create {@link TestRun}s for {@code .java} files with main methods in them.
+ */
+class MainFinder extends NamingPatternCodeFinder {
+
+    @Override protected boolean matches(File file) {
+        return file.getName().endsWith(".java");
+    }
+
+    @Override protected String testName(File file) {
+        return "main";
+    }
+
+    @Override protected Class<? extends TestRunner> runnerClass() {
+        return MainRunner.class;
+    }
+}
diff --git a/libcore/tools/runner/java/dalvik/runner/MainRunner.java b/libcore/tools/runner/java/dalvik/runner/MainRunner.java
new file mode 100644
index 0000000..6e993fe
--- /dev/null
+++ b/libcore/tools/runner/java/dalvik/runner/MainRunner.java
@@ -0,0 +1,40 @@
+/*
+ * 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.runner;
+
+import java.lang.reflect.Method;
+
+/**
+ * Runs a Java class with a main method.
+ */
+public final class MainRunner extends TestRunner {
+
+    @Override public boolean test() {
+        try {
+            Method mainMethod = Class.forName(className)
+                    .getDeclaredMethod("main", String[].class);
+            mainMethod.invoke(null, new Object[] { new String[0] });
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return false; // always print main method output
+    }
+
+    public static void main(String[] args) throws Exception {
+        new MainRunner().run();
+    }
+}
diff --git a/libcore/tools/runner/java/dalvik/runner/TestFinder.java b/libcore/tools/runner/java/dalvik/runner/NamingPatternCodeFinder.java
similarity index 78%
rename from libcore/tools/runner/java/dalvik/runner/TestFinder.java
rename to libcore/tools/runner/java/dalvik/runner/NamingPatternCodeFinder.java
index e270679..d0b6459 100644
--- a/libcore/tools/runner/java/dalvik/runner/TestFinder.java
+++ b/libcore/tools/runner/java/dalvik/runner/NamingPatternCodeFinder.java
@@ -24,9 +24,15 @@
 import java.util.regex.Pattern;
 
 /**
- * A pluggable strategy for converting files into test runs.
+ * A code finder that traverses through the directory tree looking for matching
+ * naming patterns.
  */
-abstract class TestFinder {
+abstract class NamingPatternCodeFinder implements CodeFinder {
+
+    private final String PACKAGE_PATTERN = "(?m)^\\s*package\\s+(\\S+)\\s*;";
+
+    private final String TYPE_DECLARATION_PATTERN
+            = "(?m)\\b(?:public|private)\\s+(?:interface|class|enum)\\b";
 
     public Set<TestRun> findTests(File testDirectory) {
         Set<TestRun> result = new LinkedHashSet<TestRun>();
@@ -85,10 +91,16 @@
         // declaration inside the file.
         try {
             String content = Strings.readFile(file);
-            Pattern packagePattern = Pattern.compile("(?m)^\\s*package\\s+(\\S+)\\s*;");
+            Pattern packagePattern = Pattern.compile(PACKAGE_PATTERN);
             Matcher packageMatcher = packagePattern.matcher(content);
             if (!packageMatcher.find()) {
-                throw new IllegalArgumentException("No package in '" + file + "'\n"+content);
+                // if it doesn't have a package, make sure there's at least a
+                // type declaration otherwise we're probably reading the wrong
+                // kind of file.
+                if (Pattern.compile(TYPE_DECLARATION_PATTERN).matcher(content).find()) {
+                    return className;
+                }
+                throw new IllegalArgumentException("Not a .java file: '" + file + "'\n"+content);
             }
             String packageName = packageMatcher.group(1);
             return packageName + "." + className;
diff --git a/libcore/tools/runner/java/dalvik/runner/TestRunner.java b/libcore/tools/runner/java/dalvik/runner/TestRunner.java
index 93478a7..af811d0 100644
--- a/libcore/tools/runner/java/dalvik/runner/TestRunner.java
+++ b/libcore/tools/runner/java/dalvik/runner/TestRunner.java
@@ -21,7 +21,7 @@
 import java.util.Properties;
 
 /**
- * Runs a jtreg test.
+ * Runs a test.
  */
 public abstract class TestRunner {
 
diff --git a/libcore/tools/runner/java/dalvik/runner/Vm.java b/libcore/tools/runner/java/dalvik/runner/Vm.java
index 25b095c..52bf5f6 100644
--- a/libcore/tools/runner/java/dalvik/runner/Vm.java
+++ b/libcore/tools/runner/java/dalvik/runner/Vm.java
@@ -44,6 +44,7 @@
             new File(DALVIK_RUNNER_HOME + "/java/dalvik/runner/CaliperRunner.java"),
             new File(DALVIK_RUNNER_HOME + "/java/dalvik/runner/JUnitRunner.java"),
             new File(DALVIK_RUNNER_HOME + "/java/dalvik/runner/JtregRunner.java"),
+            new File(DALVIK_RUNNER_HOME + "/java/dalvik/runner/MainRunner.java"),
             new File(DALVIK_RUNNER_HOME + "/java/dalvik/runner/TestRunner.java")));
 
     private final Pattern JAVA_TEST_PATTERN = Pattern.compile("\\/(\\w)+\\.java$");
@@ -128,6 +129,26 @@
             return;
         }
         testRun.setTestClasspath(testClasses);
+        prepareUserDir(testRun);
+    }
+
+    /**
+     * Prepares the directory from which the test will be executed. Some tests
+     * expect to read data files from the current working directory; this step
+     * should ensure such files are available.
+     */
+    protected void prepareUserDir(TestRun testRun) {
+        File testUserDir = testUserDir(testRun);
+
+        // if the user dir exists, cp would copy the files to the wrong place
+        if (testUserDir.exists()) {
+            throw new IllegalStateException();
+        }
+
+        testUserDir.getParentFile().mkdirs();
+        new Command("cp", "-r", testRun.getTestDirectory().toString(),
+                testUserDir.toString()).execute();
+        testRun.setUserDir(testUserDir);
     }
 
     /**
@@ -140,6 +161,8 @@
 
             new Command.Builder().args("rm", "-rf", testClassesDir(testRun).getPath())
                     .execute();
+            new Command.Builder().args("rm", "-rf", testUserDir(testRun).getPath())
+                    .execute();
         }
     }
 
@@ -178,6 +201,11 @@
         return new File(localTemp, testRun.getQualifiedName());
     }
 
+    private File testUserDir(TestRun testRun) {
+        File testTemp = new File(localTemp, "userDir");
+        return new File(testTemp, testRun.getQualifiedName());
+    }
+
     /**
      * Returns a properties object for the given test description.
      */
@@ -196,7 +224,7 @@
             throw new IllegalArgumentException();
         }
 
-        final Command command = newVmCommandBuilder()
+        final Command command = newVmCommandBuilder(testRun.getUserDir())
                 .classpath(testRun.getTestClasspath())
                 .classpath(testRunnerClasses)
                 .classpath(getRuntimeSupportClasspath())
@@ -242,9 +270,7 @@
     /**
      * Returns a VM for test execution.
      */
-    protected VmCommandBuilder newVmCommandBuilder() {
-        return new VmCommandBuilder();
-    }
+    protected abstract VmCommandBuilder newVmCommandBuilder(File workingDirectory);
 
     /**
      * Returns the classpath containing JUnit and the dalvik annotations
@@ -274,6 +300,7 @@
     public static class VmCommandBuilder {
         private File temp;
         private Classpath classpath = new Classpath();
+        private File workingDir;
         private File userDir;
         private Integer debugPort;
         private String mainClass;
@@ -295,6 +322,11 @@
             return this;
         }
 
+        public VmCommandBuilder workingDir(File workingDir) {
+            this.workingDir = workingDir;
+            return this;
+        }
+
         public VmCommandBuilder userDir(File userDir) {
             this.userDir = userDir;
             return this;
@@ -320,6 +352,9 @@
             builder.args(vmCommand);
             builder.args("-classpath", classpath.toString());
             builder.args("-Duser.dir=" + userDir);
+            if (workingDir != null) {
+                builder.workingDirectory(workingDir);
+            }
 
             if (temp != null) {
                 builder.args("-Djava.io.tmpdir=" + temp);