Add Junit3 and Junit4 adapters allowing to launch AntUnit script from JUnit runner (I still have some weakness to fix in the junit4 runner)

git-svn-id: https://svn.apache.org/repos/asf/ant/antlibs/antunit/trunk@743906 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/build.xml b/build.xml
index 55dc355..d05b528 100644
--- a/build.xml
+++ b/build.xml
@@ -25,5 +25,8 @@
   <!-- don't fork junit; regexp classes not available -->
   <property name="junit.fork" value="false" />
 
+  <property name="javac.test-source" value="1.5"/>
+  <property name="javac.test-target" value="1.5"/>
+
   <import file="common/build.xml"/>
 </project>
diff --git a/changes.xml b/changes.xml
index 4cb282d..6d43760 100644
--- a/changes.xml
+++ b/changes.xml
@@ -38,6 +38,9 @@
   </properties>
 
   <release version="1.2" date="not-released">
+    <action type="add">
+      Add Junit3 and Junit4 adapters allowing to launch AntUnit script from JUnit runner
+    </action>
     <action type="update">
       expectfailure report the original build exception chained when failing
     </action>
diff --git a/contributors.xml b/contributors.xml
index aec08d7..4d2993b 100644
--- a/contributors.xml
+++ b/contributors.xml
@@ -62,4 +62,8 @@
     <first>Steve</first>
     <last>Loughran</last>
   </name>
+  <name>
+    <first>Gilles</first>
+    <last>Scokart</last>
+  </name>
 </contributors>
diff --git a/src/etc/testcases/antunit/junit.xml b/src/etc/testcases/antunit/junit.xml
new file mode 100644
index 0000000..957c2ab
--- /dev/null
+++ b/src/etc/testcases/antunit/junit.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+
+<project name="junit" default="all" xmlns:au="antlib:org.apache.ant.antunit"
+ basedir="../../../../">
+
+  <target name="all">
+    <fail message="These are not standalone tests." />
+  </target>
+
+  <property name="outputdir" location="target/test_output"/>
+  <property name="outputfile" location="${outputdir}/junit_out.xml"/>
+  
+  <target name="suiteSetUp">
+    <mkdir dir="${outputdir}"/>
+    <delete file="${outputfile}"/>
+    <echo file="${outputfile}" append="true" message="suiteSetUp-" />
+  </target>
+  
+  <target name="suiteTearDown">
+    <echo file="${outputfile}" append="true" message="suiteTearDown" />
+  </target>
+  
+  <target name="setUp">
+    <echo file="${outputfile}" append="true" message="setUp-" />
+  </target>
+
+  <target name="tearDown">
+    <echo file="${outputfile}" append="true" message="tearDown-" />
+  </target>
+  
+
+  <target name="test1">
+      <echo file="${outputfile}" append="true" message="test1-" />
+  </target>  
+
+  <target name="test2">
+      <echo file="${outputfile}" append="true" message="test2-" />
+  </target>  
+
+
+ </project>
\ No newline at end of file
diff --git a/src/main/org/apache/ant/antunit/ProjectFactory.java b/src/main/org/apache/ant/antunit/ProjectFactory.java
index a056c76..ebf7a5f 100644
--- a/src/main/org/apache/ant/antunit/ProjectFactory.java
+++ b/src/main/org/apache/ant/antunit/ProjectFactory.java
@@ -4,7 +4,7 @@
 
 /** 
  * Provides project instances for AntUnit execution.<br/>  
- * The aproach to creates a project depends on the context.  When invoked from an 
+ * The approach to creates a project depends on the context.  When invoked from an 
  * ant project, some elements might be intialized from the parent project.  When
  * executed in a junit runner, a brand new project must be initialized.<br/>
  * The AntScriptRunner will usually creates multiple project in order to provide test isolation. 
diff --git a/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java b/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java
new file mode 100644
index 0000000..9b7f13c
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit3;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import junit.framework.Test;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import org.apache.ant.antunit.AntUnitScriptRunner;
+import org.apache.ant.antunit.ProjectFactory;
+import org.apache.tools.ant.DefaultLogger;
+import org.apache.tools.ant.MagicNames;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.ProjectHelper;
+
+/**
+ * A JUnit 3 TestSuite that group a suite of AntUnit targets coming from an ant
+ * script.
+ */
+public class AntUnitSuite extends TestSuite {
+
+    private final AntUnitScriptRunner antScriptRunner;
+    private final MultiProjectDemuxOutputStream stderr;
+    private final MultiProjectDemuxOutputStream stdout;
+
+    /**
+     * Create a JUnit TestSuite that when executed will run the given ant
+     * script.<br/> 
+     * Note that it is the responsibility of the caller to give the correct
+     * File reference. Namely, if the File is a relative file, it will
+     * be resolve relatively to the execution directory (which might be
+     * different different from the project root directory).
+     * 
+     * @param scriptFile
+     *            AntUnit script file
+     * @param rootClass
+     *            The test class that creates this suite. This is used to give
+     *            a name to the suite so that an IDE can reexecute this suite.
+     */
+    public AntUnitSuite(final File scriptFile, Class rootClass) {
+        this(scriptFile);
+        setName(rootClass.getName());// Allows eclipse to reexecute the test
+    }
+
+    /**
+     * Constructor used by AntUnitTestCase when a single test case is created.
+     * The difference with the public constructor is this version doesn't set
+     * the name.
+     */
+    AntUnitSuite(final File scriptFile) {
+        MyProjectFactory prjFactory = new MyProjectFactory(scriptFile);
+        antScriptRunner = new AntUnitScriptRunner(prjFactory);
+        stdout = new MultiProjectDemuxOutputStream(antScriptRunner, false);
+        stderr = new MultiProjectDemuxOutputStream(antScriptRunner, true);
+        setName(antScriptRunner.getName() + "[" + scriptFile + "]"); 
+        List testTargets = antScriptRunner.getTestTartgets();
+        for (Iterator it = testTargets.iterator(); it.hasNext();) {
+            String target = (String) it.next();
+            AntUnitTestCase tc = new AntUnitTestCase(this, scriptFile, target);
+            addTest(tc);
+        }
+    }
+
+    /**
+     * @Override Run the full AntUnit suite
+     */
+    public void run(TestResult testResult) {
+        List testTartgets = antScriptRunner.getTestTartgets();
+        runInContainer(testTartgets, testResult, tests());
+    }
+
+    /**
+     * @Override Run a single test target of the AntUnit suite. suiteSetUp,
+     *           setUp, tearDown and suiteTearDown are executed around it.
+     */
+    public void runTest(Test test, TestResult result) {
+        String targetName = ((AntUnitTestCase) test).getTarget();
+        List singleTargetList = Collections.singletonList(targetName);
+        Enumeration singleTestList = Collections.enumeration(Collections
+                .singletonList(test));
+        runInContainer(singleTargetList, result, singleTestList);
+    }
+
+    /**
+     * Execute the test suite in a 'container' similar to the ant 'container'.
+     * When ant executes a project it redirect the input and the output. In this
+     * context we will only redirect output (unit test are not supposed to be
+     * interactive)
+     * 
+     * @param targetList
+     *            The list of test target to execute
+     * @param result
+     *            The JUnit3 TestResult receiving result notification
+     * @param tests
+     *            The JUnit3 Test classes instances to use in the notification.
+     */
+    private void runInContainer(List targetList, TestResult result,
+            Enumeration/*<Test>*/tests) {
+        JUnitNotificationAdapter notifier = new JUnitNotificationAdapter(
+                result, tests);
+        PrintStream savedErr = System.err;
+        PrintStream savedOut = System.out;
+        try {
+            System.setOut(new PrintStream(stdout));
+            System.setErr(new PrintStream(stderr));
+            antScriptRunner.runSuite(targetList, notifier);
+        } finally {
+            System.setOut(savedOut);
+            System.setErr(savedErr);
+        }
+    }
+
+    /**
+     * The antscript project factory that creates projects in a junit context.
+     */
+    private static class MyProjectFactory implements ProjectFactory {
+
+        private final File scriptFile;
+        private final PrintStream realStdErr = System.err;
+        private final PrintStream realStdOut = System.out;
+
+        public MyProjectFactory(File scriptFile) {
+            this.scriptFile = scriptFile;
+        }
+
+        public Project createProject() { 
+            ProjectHelper prjHelper = ProjectHelper.getProjectHelper();
+            Project prj = new Project();
+            DefaultLogger logger = new DefaultLogger();
+            logger.setMessageOutputLevel(Project.MSG_INFO);
+            logger.setErrorPrintStream(realStdErr);
+            logger.setOutputPrintStream(realStdOut);
+            prj.addBuildListener(logger);
+            String absolutePath = scriptFile.getAbsolutePath();
+            prj.setUserProperty(MagicNames.ANT_FILE, absolutePath);
+            prj.addReference(ProjectHelper.PROJECTHELPER_REFERENCE, prjHelper);
+            prj.init();
+            prjHelper.parse(prj, scriptFile);
+            return prj;
+        }
+    }
+
+}
diff --git a/src/main/org/apache/ant/antunit/junit3/AntUnitTestCase.java b/src/main/org/apache/ant/antunit/junit3/AntUnitTestCase.java
new file mode 100644
index 0000000..7aa4888
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit3/AntUnitTestCase.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit3;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+
+/**
+ * JUnit TestCase that will executes a single AntUnit target. This class is not
+ * supposed to be used directly. <br/>
+ * It is public only because junit must access it as a public.
+ */
+public class AntUnitTestCase extends TestCase {
+    // We have to extends TestCase, and not implements Test because otherwise 
+    // JUnit4 will derive the Description composing the suite description from 
+    // this className only (AntUnitTestCase), and not from the name.
+    // However, during execution it use the right Description (base on the 
+    // toString)
+
+    /**
+     * AntUnitSuite that contains this AntUnitTestCase. Execution is done via
+     * this suite
+     */
+    private final AntUnitSuite suite;
+
+    /**
+     * The test target
+     */
+    private final String target;
+
+    /**
+     * Prepare an AntUnitTestCase that will be executed alone. This constructor 
+     * is typically used by a junit 3 runner that will reexecute a specific 
+     * test.</br> 
+     * The execution of this test will be embed in a suiteSetUp and 
+     * suiteTearDown.
+     * @param name The name of the AntUnitTestCase, normally obtained from a 
+     * previous execution. 
+     */
+    public AntUnitTestCase(String name) {
+        super(name);
+        TestCaseName nameParser = new TestCaseName(name);
+        target = nameParser.getTarget();
+        suite = new AntUnitSuite(nameParser.getScript());
+        // TODO : check that target is in the list
+    }
+
+    /**
+     * Prepare an AntUnitTestCase that will be executed in a suite. It is the
+     * suite that prepare the antScriptRunner and the JUnitExcutionPlatform. It
+     * is the responsibility of the suite to execute the suiteSetUp and the
+     * suiteTearDown.
+     * 
+     * @param target
+     * @param antScriptRunner
+     * @param executionEnv
+     */
+    public AntUnitTestCase(AntUnitSuite suite, File scriptFile, String target) {
+        // The name can be reused by eclipse when running a single test
+        super(new TestCaseName(scriptFile, target).getName());
+        this.target = target;
+        this.suite = suite;
+    }
+
+    /** Get the AntUnit test target name */
+    public String getTarget() {
+        return target;
+    }
+
+    /** @overwrite */
+    public void run(TestResult result) {
+        suite.runTest(this, result);
+    }
+
+    /**
+     * Handle the serialization and the parsing of the name of a TestCase. The
+     * name of the TestCase contains the filename of the script and the target,
+     * so that the name uniquely identify the TestCase, and a TestCase can be
+     * executed from its name.
+     */
+    static class TestCaseName {
+        private final String name;
+        private final File script;
+        private final String target;
+
+        public TestCaseName(String name) {
+            this.name = name;
+            this.target = name.substring(0, name.indexOf(' '));
+            String filename = name.substring(name.indexOf(' ') + 2, name
+                    .length() - 1);
+            this.script = new File(filename);
+        }
+
+        public TestCaseName(File script, String target) {
+            this.script = script;
+            this.target = target;
+            this.name = target + " [" + script + "]";
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public File getScript() {
+            return script;
+        }
+
+        public String getTarget() {
+            return target;
+        }
+    }
+
+}
diff --git a/src/main/org/apache/ant/antunit/junit3/JunitNotificationAdapter.java b/src/main/org/apache/ant/antunit/junit3/JunitNotificationAdapter.java
new file mode 100644
index 0000000..1c315c1
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit3/JunitNotificationAdapter.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit3;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.Test;
+import junit.framework.TestResult;
+
+import org.apache.ant.antunit.AntUnitExecutionNotifier;
+import org.apache.ant.antunit.AssertionFailedException;
+
+/**
+ * Adapt AntUnitExecutionNotifier events into JUnit3 TestResult events
+ */
+class JUnitNotificationAdapter implements AntUnitExecutionNotifier {
+
+    private final TestResult junitTestResult;
+    private Map testByTarget = new HashMap(); 
+
+    public JUnitNotificationAdapter(TestResult testResult, Enumeration tests) {
+        this.junitTestResult = testResult;
+        while(tests.hasMoreElements()) {
+            AntUnitTestCase test = (AntUnitTestCase) tests.nextElement();
+            testByTarget.put(test.getTarget(), test);
+        }
+    }
+
+    public void fireStartTest(String targetName) {
+        //TODO : if it is null, eclipse stop the unit test (add a unit test)
+        junitTestResult.startTest((Test) testByTarget.get(targetName));
+    }
+    
+    public void fireEndTest(String targetName) {
+        junitTestResult.endTest((Test) testByTarget.get(targetName));
+    }
+
+    public void fireError(String targetName, Throwable t) {
+        junitTestResult.addError((Test) testByTarget.get(targetName), t);
+    }
+
+    public void fireFail(String targetName, AssertionFailedException ae) {
+        //I don't see how to transform the AntUnit assertion exception into 
+        //junit assertion exception (we would loose the stack trace).
+        //So failures will be reported as errors
+        junitTestResult.addError((Test) testByTarget.get(targetName), ae);
+    }
+
+}
diff --git a/src/main/org/apache/ant/antunit/junit3/MultiProjectDemuxOutputStream.java b/src/main/org/apache/ant/antunit/junit3/MultiProjectDemuxOutputStream.java
new file mode 100644
index 0000000..5bb5db2
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit3/MultiProjectDemuxOutputStream.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit3;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.ant.antunit.AntUnitScriptRunner;
+import org.apache.tools.ant.DemuxOutputStream;
+import org.apache.tools.ant.Project;
+
+
+/**
+ * Forward stdout or stderr operation to the current antunit project.
+ */
+class MultiProjectDemuxOutputStream extends OutputStream {
+
+    private final AntUnitScriptRunner scriptRunner;
+    
+    private Project lastProject;
+    private DemuxOutputStream lastDemuxOutputStream = null;
+
+    private final boolean isErrorStream; 
+
+    public MultiProjectDemuxOutputStream(AntUnitScriptRunner scriptRunner, boolean isErrorStream) {
+        this.scriptRunner = scriptRunner;
+        this.isErrorStream = isErrorStream;        
+    }
+    
+
+    private DemuxOutputStream getDemuxOutputStream() {
+        if (lastProject != scriptRunner.getCurrentProject()) {
+            lastProject = scriptRunner.getCurrentProject();
+            lastDemuxOutputStream = new DemuxOutputStream(lastProject,isErrorStream);
+        }
+        return lastDemuxOutputStream;
+    }
+    
+    public void write(int b) throws IOException {
+        getDemuxOutputStream().write(b);
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+        getDemuxOutputStream().write(b, off, len);
+    }
+    
+    public void close() throws IOException {
+        getDemuxOutputStream().close();
+    }
+    
+    public void flush() throws IOException {
+        getDemuxOutputStream().flush();
+    }
+}
diff --git a/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java b/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java
new file mode 100644
index 0000000..a59ed47
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit4;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Enumeration;
+
+import org.apache.ant.antunit.junit3.AntUnitSuite;
+import org.apache.ant.antunit.junit3.AntUnitTestCase;
+import org.junit.internal.runners.CompositeRunner;
+import org.junit.internal.runners.InitializationError;
+
+/**
+ * JUnit4 Runner to put in a RunWith annotation of the AntUnitSuite when using a
+ * JUnit4 runner. Using this runner is not mandatory because junit4 is able to
+ * run junit3 test. However, the test will be faster (TODO make that true :-) )
+ * with this Runner. Also, more features are available when this runner is used
+ * (filtering & sorting) 
+ * TODO Support filtering and sorting
+ */
+public class AntUnitSuiteRunner extends CompositeRunner {
+
+    private AntUnitSuiteRunner(AntUnitSuite suite, Class junitTestClass) {
+        super(suite.getName());
+        Enumeration tests = suite.tests();
+        while (tests.hasMoreElements()) {
+            AntUnitTestCase tc = (AntUnitTestCase) tests.nextElement();
+            add(new AntUnitTestCaseRunner(tc, junitTestClass));
+        }
+    }
+
+    public AntUnitSuiteRunner(Class testCaseClass) throws InitializationError {
+        this(getJUnit3AntSuite(testCaseClass), testCaseClass);
+    }
+
+    private static AntUnitSuite getJUnit3AntSuite(Class testCaseClass)
+            throws InitializationError {
+        try {
+            Method suiteMethod = testCaseClass.getMethod("suite", new Class[0]);
+            if (!Modifier.isStatic(suiteMethod.getModifiers())) {
+                throw new InitializationError("suite method must be static");
+            }
+            return (AntUnitSuite) suiteMethod.invoke(null, new Object[0]);
+        } catch (NoSuchMethodException e) {
+            throw new InitializationError(new Throwable[] { e });
+        } catch (IllegalAccessException e) {
+            throw new InitializationError(new Throwable[] { e });
+        } catch (InvocationTargetException e) {
+            throw new InitializationError(new Throwable[] { e });
+        } catch (ClassCastException e) {
+            throw new InitializationError(new Throwable[] { e });
+        }
+    }
+
+}
diff --git a/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java b/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java
new file mode 100644
index 0000000..a494190
--- /dev/null
+++ b/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit4;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestListener;
+import junit.framework.TestResult;
+
+import org.apache.ant.antunit.junit3.AntUnitTestCase;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+
+
+class AntUnitTestCaseRunner extends Runner {
+
+    private final AntUnitTestCase fTest;
+    private final Class junitTestClass;
+
+    public AntUnitTestCaseRunner(AntUnitTestCase testCase, Class junitTestClass) {
+        this.fTest = testCase;
+        this.junitTestClass = junitTestClass;
+    }
+
+    public void run(final RunNotifier notifier) {
+        final Description description = getDescription();
+        TestListener testListener = new TestListener() {
+            // TODO implement directly the mapping from AntUnitExecutionNotifier
+            // to junit4 RunNotifier
+            public void endTest(Test test) {
+                notifier.fireTestFinished(description);
+            }
+
+            public void startTest(Test test) {
+                notifier.fireTestStarted(description);
+            }
+
+            public void addError(Test test, Throwable t) {
+                Failure failure = new Failure(description, t);
+                notifier.fireTestFailure(failure);
+            }
+
+            public void addFailure(Test test, AssertionFailedError t) {
+                addError(test, t);
+            }
+        };
+        TestResult result = new TestResult();
+        result.addListener(testListener);
+        fTest.run(result);
+    }
+
+    public Description getDescription() {
+        return Description.createTestDescription(junitTestClass, fTest
+                .getName());
+    }
+
+}
diff --git a/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitSuiteTest.java b/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitSuiteTest.java
new file mode 100644
index 0000000..020ce1b
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitSuiteTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ant.antunit.junit3;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Enumeration;
+
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+
+import org.apache.tools.ant.util.FileUtils;
+
+public class AntUnitSuiteTest extends TestCase {
+
+    AntUnitSuite suite = new AntUnitSuite(new File(
+            "src/etc/testcases/antunit/junit.xml"));
+    File outFile = new File("target/test_output/junit_out.xml");
+
+    public void testRunSuiteSetUp() throws FileNotFoundException, IOException {
+
+        TestResult testResult = new TestResult();
+        suite.run(testResult);
+        assertTrue(testResult.wasSuccessful());
+
+        String output = FileUtils.readFully(new FileReader(outFile));
+        String EXPECT1 = "suiteSetUp-setUp-test1-tearDown-setUp-test2-tearDown-suiteTearDown";
+        String EXPECT2 = "suiteSetUp-setUp-test2-tearDown-setUp-test1-tearDown-suiteTearDown";
+        assertTrue("unexted output : " + output, EXPECT1.equals(output)
+                || EXPECT2.equals(output));
+    }
+
+    public void testSuiteName() {
+        assertTrue("Expected non empty suite name", suite.getName().trim()
+                .length() > 0);
+    }
+
+    public void testChildNames() {
+        assertTrue("Expected more test, received " + suite.testCount(), 
+                suite.testCount() >= 1);
+
+        Enumeration/*<Test>*/tests = suite.tests();
+        StringBuffer testTargets = new StringBuffer();
+        while (tests.hasMoreElements()) {
+            String nextName = tests.nextElement().toString();
+            testTargets.append(" ").append(nextName).append(" ,");
+        }
+
+        assertTrue("test1 not found in child : " + testTargets, testTargets
+                .toString().contains(" test1 "));
+    }
+
+    public void testSingleTestRunSuiteSetUp() throws Exception {
+        AntUnitTestCase test1 = (AntUnitTestCase) suite.testAt(0);
+        if (test1.getTarget().equals("test2")) {
+            test1 = (AntUnitTestCase) suite.testAt(1);
+        }
+        TestResult testResult = new TestResult();
+        suite.runTest(test1, testResult);
+        assertTrue(testResult.wasSuccessful());
+
+        String output = FileUtils.readFully(new FileReader(outFile));
+        assertTrue("unexted output : " + output,
+                "suiteSetUp-setUp-test1-tearDown-suiteTearDown".equals(output));
+    }
+
+}
diff --git a/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitTestCaseTest.java b/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitTestCaseTest.java
new file mode 100644
index 0000000..7637f42
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/antunit/junit3/AntUnitTestCaseTest.java
@@ -0,0 +1,71 @@
+package org.apache.ant.antunit.junit3;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+import org.apache.ant.antunit.junit3.AntUnitTestCase;
+import org.apache.tools.ant.util.FileUtils;
+
+import junit.framework.Assert;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+
+public class AntUnitTestCaseTest extends TestCase {
+
+    File f = new File("src/etc/testcases/antunit/junit.xml");
+    String test1Name = new AntUnitTestCase.TestCaseName(f, "test1").getName();
+
+    File outFile = new File("target/test_output/junit_out.xml");
+
+    public void testNameParsing() {
+        AntUnitTestCase.TestCaseName nameObj = new AntUnitTestCase.TestCaseName(
+                test1Name);
+        assertEquals(f, nameObj.getScript());
+        assertEquals("test1", nameObj.getTarget());
+    }
+
+    public void testRunSuiteSetUp() throws FileNotFoundException, IOException {
+        // When eclipse has to run a specific testCase (user click Run on it),
+        // an AntUnitTestCase(name) is created, and the run should execute the
+        // suiteSetup/SuiteTearDown
+        AntUnitTestCase antUnitTestCase = new AntUnitTestCase(test1Name);
+
+        TestResult testResult = new TestResult();
+        antUnitTestCase.run(testResult);
+        assertTrue(testResult.wasSuccessful());
+
+        String output = FileUtils.readFully(new FileReader(outFile));
+        assertEquals("suiteSetUp-setUp-test1-tearDown-suiteTearDown", output);
+    }
+
+    private Test startedTest = null;
+    private Test endedTest = null;
+
+    public void testTestIdentityInNotification() {
+        // When eclipse has to run a specific testCase (user click Run on it),
+        // an AntUnitTestCase(name) is created, and this instance must be used
+        // in the notification (otherwise the test appears twice, once normal 
+        // but never executed, and once with "Unrooted Tests" parent.
+        TestResult testResultMock = new TestResult() {
+            public void startTest(Test test) {
+                // Note that putting an assertion here to fail fatser doesn't
+                // work because
+                // exceptions are catched by the runner
+                startedTest = test;
+            }
+
+            public void endTest(Test test) {
+                endedTest = test;
+            }
+        };
+
+        AntUnitTestCase antUnitTestCase = new AntUnitTestCase(test1Name);
+        antUnitTestCase.run(testResultMock);
+
+        Assert.assertSame(antUnitTestCase, startedTest);
+        Assert.assertSame(antUnitTestCase, endedTest);
+    }
+}
diff --git a/src/tests/junit/org/apache/ant/antunit/junit3/EatYourOwnDogFoodTest.java b/src/tests/junit/org/apache/ant/antunit/junit3/EatYourOwnDogFoodTest.java
new file mode 100644
index 0000000..a9e9bab
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/antunit/junit3/EatYourOwnDogFoodTest.java
@@ -0,0 +1,22 @@
+package org.apache.ant.antunit.junit3;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import org.apache.ant.antunit.junit4.AntUnitSuiteRunner;
+import org.junit.runner.RunWith;
+
+/**
+ * A unit test using the junit3 and junit4 adapter.
+ */
+@RunWith(AntUnitSuiteRunner.class)
+public class EatYourOwnDogFoodTest extends TestCase {
+
+    public static TestSuite suite() {
+        File script = new File("src/etc/testcases/antunit/java-io.xml");
+        return new AntUnitSuite(script, EatYourOwnDogFoodTest.class);
+    }
+
+}
diff --git a/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteTest.java b/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteTest.java
new file mode 100644
index 0000000..7bbf1b3
--- /dev/null
+++ b/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteTest.java
@@ -0,0 +1,110 @@
+package org.apache.ant.antunit.junit4;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+import org.apache.ant.antunit.junit3.AntUnitSuite;
+import org.junit.Ignore;
+import org.junit.internal.runners.InitializationError;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+
+public class AntUnitSuiteTest extends TestCase {
+
+    private boolean mockExecutionOK = false;
+    private String mockExcutionError = "";
+
+    /**
+     * When a test is executed, the description used in the notification must be
+     * equals to the description declared, otherwise the runner is confused (for
+     * example in eclipse you have all the tests listed twice, but reported only
+     * once as executed.
+     * 
+     * @throws InitializationError
+     */
+    public void testDescriptionsReportedInNotifier() throws InitializationError {
+        final AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                JUnit4AntUnitRunnable.class);
+        final ArrayList tDescs = runner.getDescription().getChildren();
+
+        final int TEST_STARTED = 1, TEST_FINISHED = 2;
+        RunNotifier notifierMock = new RunNotifier() {
+            Description curTest = null;
+
+            public void fireTestStarted(Description description) {
+                if (curTest != null) {
+                    mockExcutionError += "Unexpected fireTestStarted("
+                            + description.getDisplayName() + "\n";
+                }
+                if (!tDescs.contains(description)) {
+                    mockExcutionError += "Unexpected fireTestStarted("
+                            + description.getDisplayName() + ")\n";
+                }
+                curTest = description;
+            }
+
+            @Override
+            public void fireTestFinished(Description description) {
+                if (curTest == null) {
+                    mockExcutionError += "Unexpected fireTestFinished("
+                            + description.getDisplayName() + "\n";
+                }
+                if (!curTest.equals(description)) {
+                    mockExcutionError += "Unexpected fireTestFinished("
+                            + description.getDisplayName() + "); expect "
+                            + curTest.getDisplayName() + "\n";
+                }
+                curTest = null;
+                mockExecutionOK = true;
+            }
+        };
+
+        runner.run(notifierMock);
+        assertTrue(mockExcutionError, mockExcutionError.isEmpty());
+        assertTrue(mockExecutionOK);
+    }
+
+    public void testMissingSuiteMethodInitializationError() {
+        try {
+            AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                    JUnit4AntUnitRunnableWithoutSuiteMethod.class);
+            fail("InitializationError expected");
+        } catch (InitializationError e) {
+            String msg = e.getCauses().get(0).getMessage();
+            assertTrue("Unexpected error : " + msg, msg.contains("suite"));
+        }
+    }
+
+    public void testNonStaticSuiteMethodInitializationError() {
+        try {
+            AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                    JUnit4AntUnitRunnableWithNonStaticSuite.class);
+            fail("InitializationError expected");
+        } catch (InitializationError e) {
+            String msg = e.getCauses().get(0).getMessage();
+            assertTrue("Unexpected error : " + msg, msg.contains("suite"));
+            assertTrue("Unexpected error : " + msg, msg.contains("static"));
+        }
+    }
+
+    public static class JUnit4AntUnitRunnable {
+        public static AntUnitSuite suite() {
+            File f = new File("src/etc/testcases/antunit/junit.xml");
+            return new AntUnitSuite(f, JUnit4AntUnitRunnable.class);
+        }
+    }
+
+    public static class JUnit4AntUnitRunnableWithNonStaticSuite {
+        public AntUnitSuite suite() {
+            File f = new File("src/etc/testcases/antunit/junit.xml");
+            return new AntUnitSuite(f,
+                    JUnit4AntUnitRunnableWithNonStaticSuite.class);
+        }
+    }
+
+    public static class JUnit4AntUnitRunnableWithoutSuiteMethod {
+    }
+
+}