Index: trunk/src/test/groovy/org/gradle/api/plugins/JavaPluginConventionTest.groovy
===================================================================
--- trunk/src/test/groovy/org/gradle/api/plugins/JavaPluginConventionTest.groovy	(revision 711)
+++ trunk/src/test/groovy/org/gradle/api/plugins/JavaPluginConventionTest.groovy	Wed Jul 23 20:17:58 EST 2008
@@ -60,6 +60,7 @@
         assertEquals('javadoc', convention.javadocDirName)
         assertEquals('test-results', convention.testResultsDirName)
         assertEquals('reports', convention.reportsDirName)
+        assertEquals('tests', convention.testReportDirName)
         assertEquals(['main/java'], convention.srcDirNames)
         assertEquals(['test/java'], convention.testSrcDirNames)
         assertEquals(['main/resources'], convention.resourceDirNames)
@@ -95,9 +96,9 @@
         assertEquals(new File(project.buildDir, convention.javadocDirName), convention.javadocDir)
         assertEquals(new File(project.buildDir, convention.testResultsDirName), convention.testResultsDir)
         assertEquals(new File(project.buildDir, convention.reportsDirName), convention.reportsDir)
+        assertEquals(new File(convention.reportsDir, convention.testReportDirName), convention.testReportDir)
     }
 
-
     @Test public void testMkdir() {
         String expectedDirName = 'somedir'
         File dir = convention.mkdir(expectedDirName)
Index: trunk/build.gradle
===================================================================
--- trunk/build.gradle	(revision 711)
+++ trunk/build.gradle	Thu Jul 24 10:05:39 EST 2008
@@ -79,6 +79,7 @@
             ":ivy:2.0.0.rc1_20080716132100_r677238@jar"
     runtime ":ant-launcher:1.7.0",
             ":ant-nodeps:1.7.0",
+            ":ant-trax:1.7.0",
             ":junit:4.4"
 
     def FIRST_LEVEL_JMOCK = [':hamcrest-core:1.1', ':hamcrest-library:1.1', ':jmock-junit4:2.4.0']
@@ -364,7 +365,7 @@
  * We can't use the Ant unzip task, as this task does not preserve the *nix file system permissions of the Gradle zip.
  */
 void asternixInstall() {
-    ant.exec(dir: gradle_installDir, executable: "unzip") {
+    ant.exec(executable: "unzip") {
         arg(value: '-q')
         arg(value: '-d')
         arg(value: gradle_installDir)
@@ -383,6 +384,8 @@
     }
 }
 
+createTask('developerBuild', dependsOn: ['clean', 'integTest'])
+
 createTask('release', dependsOn: ['uploadDists', 'uploadUserguide', 'uploadJavadoc']) {
     svn.release()
 }
Index: trunk/src/main/groovy/org/gradle/api/tasks/testing/Test.groovy
===================================================================
--- trunk/src/main/groovy/org/gradle/api/tasks/testing/Test.groovy	(revision 711)
+++ trunk/src/main/groovy/org/gradle/api/tasks/testing/Test.groovy	Thu Jul 24 08:56:32 EST 2008
@@ -21,7 +21,6 @@
 import org.gradle.api.InvalidUserDataException
 import org.gradle.api.Task
 import org.gradle.api.internal.ConventionTask
-import org.gradle.api.internal.project.DefaultProject
 import org.gradle.api.tasks.compile.ClasspathConverter
 import org.gradle.api.tasks.util.ExistingDirsFilter
 import org.slf4j.Logger
@@ -46,6 +45,11 @@
     File testResultsDir = null
 
     /**
+     * The directory to generate the test report into.
+     */
+    File testReportDir = null
+
+    /**
      * This task delegates to Ants Junit task for the execution of the tests. This options contain most of the options
      * of Ants Junit task.
      */
@@ -97,7 +101,7 @@
         List classpath = classpathConverter.createFileClasspath(
                 project.rootDir, [getTestClassesDir()] + getUnmanagedClasspath()) + getDependencyManager().resolveTask(name)
 
-        antJunit.execute(getTestClassesDir(), classpath, getTestResultsDir(), includes, excludes, options, project.ant)
+        antJunit.execute(getTestClassesDir(), classpath, getTestResultsDir(), getTestReportDir(), includes, excludes, options, project.ant)
         if (stopAtFailuresOrErrors && project.ant.project.getProperty(AntJunit.FAILURES_OR_ERRORS_PROPERTY)) {
             throw new GradleException("There were failing tests!")
         }
@@ -137,6 +141,10 @@
         this.testResultsDir = testResultsDir;
     }
 
+    public File getTestReportDir() {
+        return conv(testReportDir, "testReportDir");
+    }
+
     public JunitOptions getOptions() {
         return options;
     }
Index: trunk/src/test/groovy/org/gradle/api/tasks/testing/TestTest.groovy
===================================================================
--- trunk/src/test/groovy/org/gradle/api/tasks/testing/TestTest.groovy	(revision 711)
+++ trunk/src/test/groovy/org/gradle/api/tasks/testing/TestTest.groovy	Thu Jul 24 10:01:03 EST 2008
@@ -20,7 +20,6 @@
 import org.gradle.api.DependencyManager
 import org.gradle.api.GradleException
 import org.gradle.api.InvalidUserDataException
-import org.gradle.api.Task
 import org.gradle.api.tasks.AbstractConventionTaskTest
 import org.gradle.api.tasks.AbstractTaskTest
 import org.gradle.api.tasks.StopActionException
@@ -28,7 +27,8 @@
 import org.gradle.api.tasks.util.ExistingDirsFilter
 import static org.junit.Assert.*
 import org.junit.Before
-import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.internal.AbstractTask
+import org.hamcrest.Matchers;
 
 /**
  * @author Hans Dockter
@@ -40,6 +40,7 @@
 
     static final File TEST_TEST_CLASSES_DIR = '/testClassesDir' as File
     static final File TEST_TEST_RESULTS_DIR = '/resultDir' as File
+    static final File TEST_TEST_REPORT_DIR = '/report/tests' as File
     static final File TEST_ROOT_DIR = '/ROOTDir' as File
 
     static final List TEST_DEPENDENCY_MANAGER_CLASSPATH = ['jar1' as File]
@@ -62,7 +63,7 @@
 
     AbstractTask getTask() {test}
 
-    @Test public void testCompile() {
+    @org.junit.Test public void testCompile() {
         assertNotNull(test.antJunit)
         assertNotNull(test.options)
         assertNotNull(test.existingDirsFilter)
@@ -70,21 +71,24 @@
         assertNull(test.testClassesDir)
         assertNull(test.dependencyManager)
         assertNull(test.testResultsDir)
+        assertNull(test.testReportDir)
         assertEquals([], test.includes)
         assertEquals([], test.excludes)
         assert test.stopAtFailuresOrErrors
     }
 
-    @Test public void testExecute() {
+    @org.junit.Test public void testExecute() {
         setUpMocks(test)
 
         setExistingDirsFilter()
 
-        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir, List includes,
-                                             List excludes, JunitOptions junitOptions, AntBuilder ant ->
+        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir,
+                                             File testReportDir, List includes, List excludes,
+                                             JunitOptions junitOptions, AntBuilder ant ->
             assertEquals(TEST_TEST_CLASSES_DIR, testClassesDir)
             assertEquals([TEST_TEST_CLASSES_DIR] + TEST_CONVERTED_UNMANAGED_CLASSPATH + TEST_DEPENDENCY_MANAGER_CLASSPATH, classpath)
             assertEquals(TEST_TEST_RESULTS_DIR, testResultsDir)
+            assertEquals(TEST_TEST_REPORT_DIR, testReportDir)
             assertEquals(test.options, junitOptions)
             assert includes.is(test.includes)
             assert excludes.is(test.excludes)
@@ -96,11 +100,12 @@
         }
     }
 
-    @Test public void testExecuteWithTestFailuresAndStopAtFailures() {
+    @org.junit.Test public void testExecuteWithTestFailuresAndStopAtFailures() {
         setUpMocks(test)
         setExistingDirsFilter()
-        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir, List includes,
-                                             List excludes, JunitOptions junitOptions, AntBuilder ant ->
+        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir,
+                                             File testReportDir, List includes, List excludes,
+                                             JunitOptions junitOptions, AntBuilder ant ->
             ant.project.setProperty(AntJunit.FAILURES_OR_ERRORS_PROPERTY, 'somevalue')
         }
         antJUnitMocker.use(test.antJunit) {
@@ -110,12 +115,13 @@
         }
     }
 
-    @Test public void testExecuteWithTestFailuresAndContinueWithFailures() {
+    @org.junit.Test public void testExecuteWithTestFailuresAndContinueWithFailures() {
         setUpMocks(test)
         test.stopAtFailuresOrErrors = false
         setExistingDirsFilter()
-        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir, List includes,
-                                             List excludes, JunitOptions junitOptions, AntBuilder ant ->
+        antJUnitMocker.demand.execute(1..1) {File testClassesDir, List classpath, File testResultsDir,
+                                             File testReportDir, List includes, List excludes,
+                                             JunitOptions junitOptions, AntBuilder ant ->
             ant.project.setProperty(AntJunit.FAILURES_OR_ERRORS_PROPERTY, 'somevalue')
         }
 
@@ -124,7 +130,7 @@
         }
     }
 
-    @Test public void testExecuteWithUnspecifiedCompiledTestsDir() {
+    @org.junit.Test public void testExecuteWithUnspecifiedCompiledTestsDir() {
         setUpMocks(test)
         test.testClassesDir = null
         shouldFailWithCause(InvalidUserDataException) {
@@ -132,7 +138,7 @@
         }
     }
 
-    @Test public void testExecuteWithUnspecifiedTestResultsDir() {
+    @org.junit.Test public void testExecuteWithUnspecifiedTestResultsDir() {
         setUpMocks(test)
         test.testResultsDir = null
         shouldFailWithCause(InvalidUserDataException) {
@@ -140,7 +146,7 @@
         }
     }
 
-    @Test public void testExecuteWithNonExistingCompiledTestsDir() {
+    @org.junit.Test public void testExecuteWithNonExistingCompiledTestsDir() {
         setUpMocks(test)
         test.unmanagedClasspath = null
 
@@ -150,8 +156,9 @@
         }] as ExistingDirsFilter
 
         antJUnitMocker.demand.execute(0..0) {File compiledTestClassesDir, List classpath, File testResultsDir,
-                                             List includes, List excludes, JunitOptions junitOptions, List jvmArgs,
-                                             Map systemProperties, AntBuilder ant ->}
+                                             File testReportDir, List includes, List excludes,
+                                             JunitOptions junitOptions, List jvmArgs, Map systemProperties,
+                                             AntBuilder ant -> }
 
         antJUnitMocker.use(test.antJunit) {
             test.execute()
@@ -161,6 +168,7 @@
     private void setUpMocks(Test test) {
         test.testClassesDir = TEST_TEST_CLASSES_DIR
         test.testResultsDir = TEST_TEST_RESULTS_DIR
+        test.testReportDir = TEST_TEST_REPORT_DIR
         test.unmanagedClasspath = TEST_UNMANAGED_CLASSPATH
 
         test.dependencyManager = [resolveTask: {String taskName ->
@@ -175,11 +183,11 @@
         }] as ClasspathConverter
     }
 
-    @Test public void testIncludes() {
+    @org.junit.Test public void testIncludes() {
         checkIncludesExcludes('include')
     }
 
-    @Test public void testExcludes() {
+    @org.junit.Test public void testExcludes() {
         checkIncludesExcludes('exclude')
     }
 
@@ -190,7 +198,7 @@
         assertEquals([TEST_PATTERN_1, TEST_PATTERN_2, TEST_PATTERN_3], test."${name}s")
     }
 
-    @Test public void testUnmanagedClasspath() {
+    @org.junit.Test public void testUnmanagedClasspath() {
         List list1 = ['a', new Object()]
         assert test.unmanagedClasspath(list1 as Object[]).is(test)
         assertEquals(list1, test.unmanagedClasspath)
@@ -205,4 +213,13 @@
         }] as ExistingDirsFilter
     }
 
+    private shouldFailWithCause(Class exceptionClass, Closure closure) {
+        try {
+            closure.call()
+            fail()
+        } catch (Throwable t) {
+            assertThat(exceptionClass.isInstance(t), Matchers.equalTo(true))
-}
+        }
+    }
+
+}
Index: trunk/src/main/groovy/org/gradle/api/plugins/DefaultConventionsToPropertiesMapping.groovy
===================================================================
--- trunk/src/main/groovy/org/gradle/api/plugins/DefaultConventionsToPropertiesMapping.groovy	(revision 711)
+++ trunk/src/main/groovy/org/gradle/api/plugins/DefaultConventionsToPropertiesMapping.groovy	Wed Jul 23 20:21:12 EST 2008
@@ -56,6 +56,7 @@
     public final static Map TEST = [
             testClassesDir: {it.plugins.java.testClassesDir},
             testResultsDir: {it.plugins.java.testResultsDir},
+            testReportDir: {it.plugins.java.testReportDir},
 //            // Order of dirs is important because of classpath!
 //            unmanagedClasspath: {[it.plugins.java.classesDir]},
             dependencyManager: {it.project.dependencies}
Index: trunk/src/main/groovy/org/gradle/api/tasks/testing/AntJunit.groovy
===================================================================
--- trunk/src/main/groovy/org/gradle/api/tasks/testing/AntJunit.groovy	(revision 711)
+++ trunk/src/main/groovy/org/gradle/api/tasks/testing/AntJunit.groovy	Thu Jul 24 09:13:50 EST 2008
@@ -33,7 +33,7 @@
 
     static final String FAILURES_OR_ERRORS_PROPERTY = 'org.gradle.api.tasks.testing.failuresOrErrors'
 
-    void execute(File compiledTestsClassesDir, List classPath, File testResultsDir, List includes, List excludes, JunitOptions junitOptions, AntBuilder ant) {
+    void execute(File compiledTestsClassesDir, List classPath, File testResultsDir, File testReportDir, List includes, List excludes, JunitOptions junitOptions, AntBuilder ant) {
         ant.mkdir(dir: testResultsDir.absolutePath)
         createAntClassPath(ant, classPath + GradleUtil.antJunitJarFiles)
         Map otherArgs = [
@@ -66,7 +66,11 @@
                 path(refid: CLASSPATH_ID)
             }
         }
+        ant.junitreport(todir: testResultsDir.absolutePath) {
+            fileset(dir: testResultsDir.absolutePath, includes: '*.xml')
+            report(todir: testReportDir.absolutePath)
-    }
+        }
+    }
 
     private void createAntClassPath(AntBuilder ant, List classpath) {
         ant.path(id: CLASSPATH_ID) {
Index: trunk/src/main/groovy/org/gradle/api/plugins/JavaPluginConvention.groovy
===================================================================
--- trunk/src/main/groovy/org/gradle/api/plugins/JavaPluginConvention.groovy	(revision 711)
+++ trunk/src/main/groovy/org/gradle/api/plugins/JavaPluginConvention.groovy	Wed Jul 23 20:19:03 EST 2008
@@ -47,6 +47,7 @@
     String testResultsDirName
     String reportsDirName
     String webAppDirName
+    String testReportDirName
     List srcDirNames = []
     List resourceDirNames = []
     List testSrcDirNames = []
@@ -76,6 +77,7 @@
         javadocDirName = 'javadoc'
         reportsDirName = 'reports'
         testResultsDirName = 'test-results'
+        testReportDirName = 'tests'
         srcDirNames << 'main/java'
         resourceDirNames << 'main/resources'
         testSrcDirNames << 'test/java'
@@ -144,6 +146,10 @@
         new File(project.buildDir, testResultsDirName)
     }
 
+    File getTestReportDir() {
+        new File(reportsDir, testReportDirName)
+    }
+
     File getReportsDir() {
         new File(project.buildDir, reportsDirName)
     }
