handle more completely junit4 suites

git-svn-id: https://svn.apache.org/repos/asf/ant/antlibs/antunit/trunk@745628 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java b/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java
index 337fe7b..8ec59bf 100644
--- a/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java
+++ b/src/main/org/apache/ant/antunit/junit3/AntUnitSuite.java
@@ -23,7 +23,6 @@
 import java.io.File;
 import java.io.PrintStream;
 import java.util.Collections;
-import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
 
@@ -32,6 +31,7 @@
 import junit.framework.TestResult;
 import junit.framework.TestSuite;
 
+import org.apache.ant.antunit.AntUnitExecutionNotifier;
 import org.apache.ant.antunit.AntUnitScriptRunner;
 import org.apache.ant.antunit.ProjectFactory;
 import org.apache.tools.ant.BuildException;
@@ -117,7 +117,9 @@
             initializationReportingTest.run(testResult);
         } else {
             List testTartgets = antScriptRunner.getTestTartgets();
-            runInContainer(testTartgets, testResult);
+            JUnitNotificationAdapter notifier = new JUnitNotificationAdapter(
+                    testResult, tests());
+            runInContainer(testTartgets, notifier);
         }
     }
 
@@ -142,7 +144,9 @@
         } else {
             String targetName = ((AntUnitTestCase) test).getTarget();
             List singleTargetList = Collections.singletonList(targetName);
-            runInContainer(singleTargetList, result);
+            JUnitNotificationAdapter notifier = new JUnitNotificationAdapter(
+                    result, tests());
+            runInContainer(singleTargetList, notifier);
         }
     }
 
@@ -150,18 +154,14 @@
      * 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)
+     * interactive).<br/>
      * 
      * @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.
+     * @param notifier
+     *            The AntUnit notifier that will receive execution notifications
      */
-    private void runInContainer(List targetList, TestResult result) {
-        JUnitNotificationAdapter notifier = new JUnitNotificationAdapter(
-                result, tests());
+    public void runInContainer(List targetList, AntUnitExecutionNotifier notifier) {        
         PrintStream savedErr = System.err;
         PrintStream savedOut = System.out;
         try {
diff --git a/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java b/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java
index 5f7e597..4d974e0 100644
--- a/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java
+++ b/src/main/org/apache/ant/antunit/junit4/AntUnitSuiteRunner.java
@@ -20,35 +20,62 @@
 
 package org.apache.ant.antunit.junit4;
 
+import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
+import org.apache.ant.antunit.AntUnitExecutionNotifier;
+import org.apache.ant.antunit.AssertionFailedException;
 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;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.Filterable;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.manipulation.Sortable;
+import org.junit.runner.manipulation.Sorter;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
 
 /**
  * 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
+ * run junit3 test. However, the test may be faster with this Runner (with the
+ * default junit4 adapter, the suiteSetUp and suiteTearDown will be executed
+ * around every test target). Also, more features are available when this runner
+ * is used (filtering & sorting)
  */
-public class AntUnitSuiteRunner extends CompositeRunner {
+public class AntUnitSuiteRunner extends Runner implements Filterable, Sortable {
 
+    private final AntUnitSuite junit3Suite;
+    private final Map/*<String, Description>*/ targetDescriptions = new HashMap();
+    private final List/*<String>*/ targetsOrder = new LinkedList();
+    
     private AntUnitSuiteRunner(AntUnitSuite suite, Class junitTestClass) {
-        super(suite.getName());
+        junit3Suite = suite;
         Enumeration tests = suite.tests();
         while (tests.hasMoreElements()) {
             //TODO Handle the the case of FileNotFound. 
             //In that case the suite contains an error Test and we have
-            //a ClassCastException instead of a nice & clear error            
+            //a ClassCastException instead of a nice & clear error
+            //TODO Handle the possibility for the user to define suite of AntUnit scripts
             AntUnitTestCase tc = (AntUnitTestCase) tests.nextElement();
-            add(new AntUnitTestCaseRunner(tc, junitTestClass));
+            Description tc_desc = Description.createTestDescription(junitTestClass, tc.getName());
+            targetDescriptions.put(tc.getTarget(), tc_desc);
+            targetsOrder.add(tc.getTarget());
         }
     }
 
@@ -63,16 +90,93 @@
             if (!Modifier.isStatic(suiteMethod.getModifiers())) {
                 throw new InitializationError("suite method must be static");
             }
-            return (AntUnitSuite) suiteMethod.invoke(null, new Object[0]);
+            Object suite = suiteMethod.invoke(null, new Object[0]);
+            if (suite == null) {
+                throw new InitializationError("suite method can not return null");
+            }
+            if (!(suite instanceof AntUnitSuite)) {
+                throw new InitializationError("suite method must return an AntUnitSuite");
+            }
+            return (AntUnitSuite) suite;
         } 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 });
         }
     }
 
+    /**
+     * @Overwrite Filterable implementation
+     */
+    public void filter(Filter filter) throws NoTestsRemainException {
+        for (Iterator iter= targetDescriptions.entrySet().iterator(); iter.hasNext();) {
+            Map.Entry mapEntry = (Entry) iter.next(); 
+            if (!filter.shouldRun((Description) mapEntry.getValue()))
+                iter.remove();
+                targetsOrder.remove(mapEntry.getKey());
+        }
+    }
+
+    /**
+     * @Overwrite Sortable implementation
+     */
+    public void sort(final Sorter sorter) {
+        Collections.sort(targetsOrder, new Comparator/*<String>*/() {
+            public int compare(Object target1, Object target2) {
+                Description d2 = (Description)targetDescriptions.get(target2);
+                Description d1 = (Description)targetDescriptions.get(target1);
+                return sorter.compare(d1, d2);
+            }
+        });
+        /*for (Runner each : fRunners)
+            sorter.apply(each);
+        */
+    }
+
+    /**
+     * @Overwrite Runner implementation
+     */
+    public Description getDescription() {
+        Description r = Description.createSuiteDescription(
+                junit3Suite.getName(), new Annotation[0]);
+        
+        Collection childDesc = targetDescriptions.values();
+        for (Iterator iterator = childDesc.iterator(); iterator.hasNext();) {
+            Description desc = (Description) iterator.next();
+            r.addChild(desc);
+        }
+        return r;
+    }
+
+    /**
+     * @Overwrite Runner implementation
+     */
+    public void run(final RunNotifier junitNotifier) {
+        LinkedList targetList = new LinkedList(targetDescriptions.keySet());
+        
+        AntUnitExecutionNotifier antUnitNotifier = new AntUnitExecutionNotifier() {            
+            public void fireStartTest(String targetName) {
+                junitNotifier.fireTestStarted(getDescription(targetName));
+            }
+            public void fireEndTest(String targetName) {
+                junitNotifier.fireTestFinished(getDescription(targetName));                
+            }
+            public void fireError(String targetName, Throwable t) {
+                Failure failure = new Failure(getDescription(targetName), t);
+                junitNotifier.fireTestFailure(failure);
+            }
+            public void fireFail(String targetName, AssertionFailedException ae) {
+                Failure failure = new Failure(getDescription(targetName), ae);
+                junitNotifier.fireTestFailure(failure);
+            }            
+            private Description getDescription(String targetName) {
+                return (Description) targetDescriptions.get(targetName);
+            }
+        };
+        
+        junit3Suite.runInContainer(targetList, antUnitNotifier);
+    }
+
 }
diff --git a/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java b/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java
deleted file mode 100644
index a494190..0000000
--- a/src/main/org/apache/ant/antunit/junit4/AntUnitTestCaseRunner.java
+++ /dev/null
@@ -1,77 +0,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.
- *
- */
-
-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/junit4/AntUnitSuiteTest.java b/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteRunnerTest.java
similarity index 66%
rename from src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteTest.java
rename to src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteRunnerTest.java
index 9be7f35..f827e21 100644
--- a/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteTest.java
+++ b/src/tests/junit/org/apache/ant/antunit/junit4/AntUnitSuiteRunnerTest.java
@@ -20,22 +20,45 @@
 package org.apache.ant.antunit.junit4;
 
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
 import java.util.ArrayList;
 
 import junit.framework.TestCase;
+import junit.framework.TestSuite;
 
 import org.apache.ant.antunit.junit3.AntUnitSuite;
-import org.junit.Ignore;
+import org.apache.tools.ant.util.FileUtils;
 import org.junit.internal.runners.InitializationError;
 import org.junit.runner.Description;
 import org.junit.runner.notification.RunNotifier;
 
-public class AntUnitSuiteTest extends TestCase {
+public class AntUnitSuiteRunnerTest extends TestCase {
 
     private boolean mockExecutionOK = false;
     private String mockExcutionError = "";
 
     /**
+     * Validates the execution sequence.
+     */
+    public void testRunFullSuite() throws FileNotFoundException, IOException,
+            InitializationError {
+        AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                JUnit4AntUnitRunnable.class);
+
+        runner.run(new RunNotifier());
+        File outFile = new File("target/test_output/junit_out.xml");
+
+        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));
+    }
+
+    
+    /**
      * 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
@@ -108,6 +131,31 @@
         }
     }
 
+    public void testInvalidSuiteReturnTypeError() {
+        try {
+            AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                    JUnit4AntUnitRunnableWithInvalidSuiteReturnType.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("AntUnitSuite"));
+        }
+    }
+
+    public void testInvalidSuiteReturnNull() {
+        try {
+            AntUnitSuiteRunner runner = new AntUnitSuiteRunner(
+                    JUnit4AntUnitRunnableWithInvalidSuiteReturningNull.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("null"));
+        }
+    }
+
+    
     public static class JUnit4AntUnitRunnable {
         public static AntUnitSuite suite() {
             File f = new File("src/etc/testcases/antunit/junit.xml");
@@ -126,4 +174,17 @@
     public static class JUnit4AntUnitRunnableWithoutSuiteMethod {
     }
 
+    public static class JUnit4AntUnitRunnableWithInvalidSuiteReturnType {
+        public static TestSuite suite() {
+            return new TestSuite("We don't support returning generic TestSuite." +
+                    "  The Runner can not handle that");
+        }
+    }
+    
+    public static class JUnit4AntUnitRunnableWithInvalidSuiteReturningNull {
+        public static TestSuite suite() {
+            return null;
+        }
+    }
+
 }