diff --git a/src/main/java/junit/framework/JUnit4TestAdapter.java b/src/main/java/junit/framework/JUnit4TestAdapter.java index 13e478efc3e9..9d32031e4eba 100644 --- a/src/main/java/junit/framework/JUnit4TestAdapter.java +++ b/src/main/java/junit/framework/JUnit4TestAdapter.java @@ -9,8 +9,10 @@ import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; import org.junit.runner.manipulation.NoTestsRemainException; -import org.junit.runner.manipulation.Sortable; +import org.junit.runner.manipulation.Orderable; import org.junit.runner.manipulation.Sorter; /** @@ -23,7 +25,7 @@ public static Test suite() { } */ -public class JUnit4TestAdapter implements Test, Filterable, Sortable, Describable { +public class JUnit4TestAdapter implements Test, Filterable, Orderable, Describable { private final Class fNewTestClass; private final Runner fRunner; @@ -93,4 +95,13 @@ public void filter(Filter filter) throws NoTestsRemainException { public void sort(Sorter sorter) { sorter.apply(fRunner); } + + /** + * {@inheritDoc} + * + * @since 4.13 + */ + public void order(Orderer orderer) throws InvalidOrderingException { + orderer.apply(fRunner); + } } \ No newline at end of file diff --git a/src/main/java/org/junit/internal/requests/ClassRequest.java b/src/main/java/org/junit/internal/requests/ClassRequest.java index acc9c90aebe9..d60e36062d81 100644 --- a/src/main/java/org/junit/internal/requests/ClassRequest.java +++ b/src/main/java/org/junit/internal/requests/ClassRequest.java @@ -1,17 +1,11 @@ package org.junit.internal.requests; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; import org.junit.internal.builders.SuiteMethodBuilder; -import org.junit.runner.Request; import org.junit.runner.Runner; import org.junit.runners.model.RunnerBuilder; -public class ClassRequest extends Request { - private final Lock runnerLock = new ReentrantLock(); - +public class ClassRequest extends MemoizingRequest { /* * We have to use the f prefix, because IntelliJ's JUnit4IdeaTestRunner uses * reflection to access this field. See @@ -19,7 +13,6 @@ public class ClassRequest extends Request { */ private final Class fTestClass; private final boolean canUseSuiteMethod; - private volatile Runner runner; public ClassRequest(Class testClass, boolean canUseSuiteMethod) { this.fTestClass = testClass; @@ -31,18 +24,8 @@ public ClassRequest(Class testClass) { } @Override - public Runner getRunner() { - if (runner == null) { - runnerLock.lock(); - try { - if (runner == null) { - runner = new CustomAllDefaultPossibilitiesBuilder().safeRunnerForClass(fTestClass); - } - } finally { - runnerLock.unlock(); - } - } - return runner; + protected Runner createRunner() { + return new CustomAllDefaultPossibilitiesBuilder().safeRunnerForClass(fTestClass); } private class CustomAllDefaultPossibilitiesBuilder extends AllDefaultPossibilitiesBuilder { diff --git a/src/main/java/org/junit/internal/requests/MemoizingRequest.java b/src/main/java/org/junit/internal/requests/MemoizingRequest.java new file mode 100644 index 000000000000..191c23022536 --- /dev/null +++ b/src/main/java/org/junit/internal/requests/MemoizingRequest.java @@ -0,0 +1,30 @@ +package org.junit.internal.requests; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.runner.Request; +import org.junit.runner.Runner; + +abstract class MemoizingRequest extends Request { + private final Lock runnerLock = new ReentrantLock(); + private volatile Runner runner; + + @Override + public final Runner getRunner() { + if (runner == null) { + runnerLock.lock(); + try { + if (runner == null) { + runner = createRunner(); + } + } finally { + runnerLock.unlock(); + } + } + return runner; + } + + /** Creates the {@link Runner} to return from {@link #getRunner()}. Called at most once. */ + protected abstract Runner createRunner(); +} diff --git a/src/main/java/org/junit/internal/requests/OrderingRequest.java b/src/main/java/org/junit/internal/requests/OrderingRequest.java new file mode 100644 index 000000000000..441e595a3680 --- /dev/null +++ b/src/main/java/org/junit/internal/requests/OrderingRequest.java @@ -0,0 +1,29 @@ +package org.junit.internal.requests; + +import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.runner.Request; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.Ordering; + +/** @since 4.13 */ +public class OrderingRequest extends MemoizingRequest { + private final Request request; + private final Ordering ordering; + + public OrderingRequest(Request request, Ordering ordering) { + this.request = request; + this.ordering = ordering; + } + + @Override + protected Runner createRunner() { + Runner runner = request.getRunner(); + try { + ordering.apply(runner); + } catch (InvalidOrderingException e) { + return new ErrorReportingRunner(ordering.getClass(), e); + } + return runner; + } +} diff --git a/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java b/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java index 631fcf2e5904..0d51541adcf8 100644 --- a/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java +++ b/src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java @@ -1,5 +1,8 @@ package org.junit.internal.runners; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + import junit.extensions.TestDecorator; import junit.framework.AssertionFailedError; import junit.framework.Test; @@ -12,15 +15,16 @@ import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runner.manipulation.Orderable; import org.junit.runner.manipulation.Sortable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -public class JUnit38ClassRunner extends Runner implements Filterable, Sortable { +public class JUnit38ClassRunner extends Runner implements Filterable, Orderable { private static final class OldTestClassAdaptingListener implements TestListener { private final RunNotifier notifier; @@ -170,6 +174,18 @@ public void sort(Sorter sorter) { } } + /** + * {@inheritDoc} + * + * @since 4.13 + */ + public void order(Orderer orderer) throws InvalidOrderingException { + if (getTest() instanceof Orderable) { + Orderable adapter = (Orderable) getTest(); + adapter.order(orderer); + } + } + private void setTest(Test test) { this.test = test; } diff --git a/src/main/java/org/junit/runner/OrderWith.java b/src/main/java/org/junit/runner/OrderWith.java new file mode 100644 index 000000000000..8e30110edd14 --- /dev/null +++ b/src/main/java/org/junit/runner/OrderWith.java @@ -0,0 +1,26 @@ +package org.junit.runner; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.runner.manipulation.Ordering; + +/** + * When a test class is annotated with @OrderWith or extends a class annotated + * with @OrderWith, JUnit will order the tests in the test class (and child + * test classes, if any) using the ordering defined by the {@link Ordering} class. + * + * @since 4.13 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface OrderWith { + /** + * Gets a class that extends {@link Ordering}. The class must have a public no-arg constructor. + */ + Class value(); +} diff --git a/src/main/java/org/junit/runner/Request.java b/src/main/java/org/junit/runner/Request.java index 264489217f74..7b9a99003b47 100644 --- a/src/main/java/org/junit/runner/Request.java +++ b/src/main/java/org/junit/runner/Request.java @@ -5,9 +5,11 @@ import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; import org.junit.internal.requests.ClassRequest; import org.junit.internal.requests.FilterRequest; +import org.junit.internal.requests.OrderingRequest; import org.junit.internal.requests.SortingRequest; import org.junit.internal.runners.ErrorReportingRunner; import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.Ordering; import org.junit.runners.model.InitializationError; /** @@ -151,15 +153,15 @@ public Request filterWith(Description desiredDescription) { * For example, here is code to run a test suite in alphabetical order: *
      * private static Comparator<Description> forward() {
-     * return new Comparator<Description>() {
-     * public int compare(Description o1, Description o2) {
-     * return o1.getDisplayName().compareTo(o2.getDisplayName());
-     * }
-     * };
+     *   return new Comparator<Description>() {
+     *     public int compare(Description o1, Description o2) {
+     *       return o1.getDisplayName().compareTo(o2.getDisplayName());
+     *     }
+     *   };
      * }
      *
      * public static main() {
-     * new JUnitCore().run(Request.aClass(AllTests.class).sortWith(forward()));
+     *   new JUnitCore().run(Request.aClass(AllTests.class).sortWith(forward()));
      * }
      * 
* @@ -169,4 +171,32 @@ public Request filterWith(Description desiredDescription) { public Request sortWith(Comparator comparator) { return new SortingRequest(this, comparator); } + + /** + * Returns a Request whose Tests can be run in a certain order, defined by + * ordering + *

+ * For example, here is code to run a test suite in reverse order: + *

+     * private static Ordering reverse() {
+     *   return new Ordering() {
+     *     public List<Description> orderItems(Collection<Description> descriptions) {
+     *       List<Description> ordered = new ArrayList<>(descriptions);
+     *       Collections.reverse(ordered);
+     *       return ordered;
+     *     }
+     *   }
+     * }
+     *     
+     * public static main() {
+     *   new JUnitCore().run(Request.aClass(AllTests.class).orderWith(reverse()));
+     * }
+     * 
+ * + * @return a Request with ordered Tests + * @since 4.13 + */ + public Request orderWith(Ordering ordering) { + return new OrderingRequest(this, ordering); + } } diff --git a/src/main/java/org/junit/runner/manipulation/Alphanumeric.java b/src/main/java/org/junit/runner/manipulation/Alphanumeric.java new file mode 100644 index 000000000000..8388d21eb804 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/Alphanumeric.java @@ -0,0 +1,27 @@ +package org.junit.runner.manipulation; + +import java.util.Comparator; + +import org.junit.runner.Description; + +/** + * A sorter that orders tests alphanumerically by test name. + * + * @since 4.13 + */ +public final class Alphanumeric extends Sorter implements Ordering.Factory { + + public Alphanumeric() { + super(COMPARATOR); + } + + public Ordering create(Context context) { + return this; + } + + private static final Comparator COMPARATOR = new Comparator() { + public int compare(Description o1, Description o2) { + return o1.getDisplayName().compareTo(o2.getDisplayName()); + } + }; +} diff --git a/src/main/java/org/junit/runner/manipulation/InvalidOrderingException.java b/src/main/java/org/junit/runner/manipulation/InvalidOrderingException.java new file mode 100644 index 000000000000..d9d60f778b62 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/InvalidOrderingException.java @@ -0,0 +1,21 @@ +package org.junit.runner.manipulation; + +/** + * Thrown when an ordering does something invalid (like remove or add children) + * + * @since 4.13 + */ +public class InvalidOrderingException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidOrderingException() { + } + + public InvalidOrderingException(String message) { + super(message); + } + + public InvalidOrderingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/junit/runner/manipulation/Orderable.java b/src/main/java/org/junit/runner/manipulation/Orderable.java new file mode 100644 index 000000000000..9a12a3bc1ef2 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/Orderable.java @@ -0,0 +1,21 @@ +package org.junit.runner.manipulation; + +/** + * Interface for runners that allow ordering of tests. + * + *

Beware of using this interface to cope with order dependencies between tests. + * Tests that are isolated from each other are less expensive to maintain and + * can be run individually. + * + * @since 4.13 + */ +public interface Orderable extends Sortable { + + /** + * Orders the tests using orderer + * + * @throws InvalidOrderingException if orderer does something invalid (like remove or add + * children) + */ + void order(Orderer orderer) throws InvalidOrderingException; +} diff --git a/src/main/java/org/junit/runner/manipulation/Orderer.java b/src/main/java/org/junit/runner/manipulation/Orderer.java new file mode 100644 index 000000000000..eb1305437076 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/Orderer.java @@ -0,0 +1,62 @@ +package org.junit.runner.manipulation; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.runner.Description; + +/** + * Orders tests. + * + * @since 4.13 + */ +public final class Orderer { + private final Ordering ordering; + + Orderer(Ordering delegate) { + this.ordering = delegate; + } + + /** + * Orders the descriptions. + * + * @return descriptions in order + */ + public List order(Collection descriptions) + throws InvalidOrderingException { + List inOrder = ordering.orderItems( + Collections.unmodifiableCollection(descriptions)); + if (!ordering.validateOrderingIsCorrect()) { + return inOrder; + } + + Set uniqueDescriptions = new HashSet(descriptions); + if (!uniqueDescriptions.containsAll(inOrder)) { + throw new InvalidOrderingException("Ordering added items"); + } + Set resultAsSet = new HashSet(inOrder); + if (resultAsSet.size() != inOrder.size()) { + throw new InvalidOrderingException("Ordering duplicated items"); + } else if (!resultAsSet.containsAll(uniqueDescriptions)) { + throw new InvalidOrderingException("Ordering removed items"); + } + + return inOrder; + } + + /** + * Order the tests in target. + * + * @throws InvalidOrderingException if ordering does something invalid (like remove or add + * children) + */ + public void apply(Object target) throws InvalidOrderingException { + if (target instanceof Orderable) { + Orderable orderable = (Orderable) target; + orderable.order(this); + } + } +} diff --git a/src/main/java/org/junit/runner/manipulation/Ordering.java b/src/main/java/org/junit/runner/manipulation/Ordering.java new file mode 100644 index 000000000000..0d0ce93780e6 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/Ordering.java @@ -0,0 +1,172 @@ +package org.junit.runner.manipulation; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import org.junit.runner.Description; +import org.junit.runner.OrderWith; + +/** + * Reorders tests. An {@code Ordering} can reverse the order of tests, sort the + * order or even shuffle the order. + * + *

In general you will not need to use a Ordering directly. + * Instead, use {@link org.junit.runner.Request#orderWith(Ordering)}. + * + * @since 4.13 + */ +public abstract class Ordering { + private static final String CONSTRUCTOR_ERROR_FORMAT + = "Ordering class %s should have a public constructor with signature " + + "%s(Ordering.Context context)"; + + /** + * Creates an {@link Ordering} that shuffles the items using the given + * {@link Random} instance. + */ + public static Ordering shuffledBy(final Random random) { + return new Ordering() { + @Override + boolean validateOrderingIsCorrect() { + return false; + } + + @Override + protected List orderItems(Collection descriptions) { + List shuffled = new ArrayList(descriptions); + Collections.shuffle(shuffled, random); + return shuffled; + } + }; + } + + /** + * Creates an {@link Ordering} from the given factory class. The class must have a public no-arg + * constructor. + * + * @param factoryClass class to use to create the ordering + * @param annotatedTestClass test class that is annotated with {@link OrderWith}. + * @throws InvalidOrderingException if the instance could not be created + */ + public static Ordering definedBy( + Class factoryClass, Description annotatedTestClass) + throws InvalidOrderingException { + if (factoryClass == null) { + throw new NullPointerException("factoryClass cannot be null"); + } + if (annotatedTestClass == null) { + throw new NullPointerException("annotatedTestClass cannot be null"); + } + + Ordering.Factory factory; + try { + Constructor constructor = factoryClass.getConstructor(); + factory = constructor.newInstance(); + } catch (NoSuchMethodException e) { + throw new InvalidOrderingException(String.format( + CONSTRUCTOR_ERROR_FORMAT, + getClassName(factoryClass), + factoryClass.getSimpleName())); + } catch (Exception e) { + throw new InvalidOrderingException( + "Could not create ordering for " + annotatedTestClass, e); + } + return definedBy(factory, annotatedTestClass); + } + + /** + * Creates an {@link Ordering} from the given factory. + * + * @param factory factory to use to create the ordering + * @param annotatedTestClass test class that is annotated with {@link OrderWith}. + * @throws InvalidOrderingException if the instance could not be created + */ + public static Ordering definedBy( + Ordering.Factory factory, Description annotatedTestClass) + throws InvalidOrderingException { + if (factory == null) { + throw new NullPointerException("factory cannot be null"); + } + if (annotatedTestClass == null) { + throw new NullPointerException("annotatedTestClass cannot be null"); + } + + return factory.create(new Ordering.Context(annotatedTestClass)); + } + + private static String getClassName(Class clazz) { + String name = clazz.getCanonicalName(); + if (name == null) { + return clazz.getName(); + } + return name; + } + + /** + * Order the tests in target using this ordering. + * + * @throws InvalidOrderingException if ordering does something invalid (like remove or add + * children) + */ + public void apply(Object target) throws InvalidOrderingException { + /* + * Note that some subclasses of Ordering override apply(). The Sorter + * subclass of Ordering overrides apply() to apply the sort (this is + * done because sorting is more efficient than ordering). + */ + if (target instanceof Orderable) { + Orderable orderable = (Orderable) target; + orderable.order(new Orderer(this)); + } + } + + /** + * Returns {@code true} if this ordering could produce invalid results (i.e. + * if it could add or remove values). + */ + boolean validateOrderingIsCorrect() { + return true; + } + + /** + * Implemented by sub-classes to order the descriptions. + * + * @return descriptions in order + */ + protected abstract List orderItems(Collection descriptions); + + /** Context about the ordering being applied. */ + public static class Context { + private final Description description; + + /** + * Gets the description for the top-level target being ordered. + */ + public Description getTarget() { + return description; + } + + private Context(Description description) { + this.description = description; + } + } + + /** + * Factory for creating {@link Ordering} instances. + * + *

For a factory to be used with {@code @OrderWith} it needs to have a public no-arg + * constructor. + */ + public interface Factory { + /** + * Creates an Ordering instance using the given context. Implementations + * of this method that do not need to use the context can return the + * same instance every time. + */ + Ordering create(Context context); + } +} diff --git a/src/main/java/org/junit/runner/manipulation/Sorter.java b/src/main/java/org/junit/runner/manipulation/Sorter.java index 20192d0c96e8..4b5274c31012 100644 --- a/src/main/java/org/junit/runner/manipulation/Sorter.java +++ b/src/main/java/org/junit/runner/manipulation/Sorter.java @@ -1,16 +1,21 @@ package org.junit.runner.manipulation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Comparator; +import java.util.List; import org.junit.runner.Description; /** * A Sorter orders tests. In general you will not need - * to use a Sorter directly. Instead, use {@link org.junit.runner.Request#sortWith(Comparator)}. + * to use a Sorter directly. Instead, use + * {@link org.junit.runner.Request#sortWith(Comparator)}. * * @since 4.0 */ -public class Sorter implements Comparator { +public class Sorter extends Ordering implements Comparator { /** * NULL is a Sorter that leaves elements in an undefined order */ @@ -27,17 +32,26 @@ public int compare(Description o1, Description o2) { * to sort tests * * @param comparator the {@link Comparator} to use when sorting tests + * @since 4.0 */ public Sorter(Comparator comparator) { this.comparator = comparator; } /** - * Sorts the test in runner using comparator + * Sorts the tests in target using comparator. + * + * @since 4.0 */ - public void apply(Object object) { - if (object instanceof Sortable) { - Sortable sortable = (Sortable) object; + @Override + public void apply(Object target) { + /* + * Note that all runners that are Orderable are also Sortable (because + * Orderable extends Sortable). Sorting is more efficient than ordering, + * so we override the parent behavior so we sort instead. + */ + if (target instanceof Sortable) { + Sortable sortable = (Sortable) target; sortable.sort(this); } } @@ -45,4 +59,32 @@ public void apply(Object object) { public int compare(Description o1, Description o2) { return comparator.compare(o1, o2); } + + /** + * {@inheritDoc} + * + * @since 4.13 + */ + @Override + protected final List orderItems(Collection descriptions) { + /* + * In practice, we will never get here--Sorters do their work in the + * compare() method--but the Liskov substitution principle demands that + * we obey the general contract of Orderable. Luckily, it's trivial to + * implement. + */ + List sorted = new ArrayList(descriptions); + Collections.sort(sorted, this); // Note: it would be incorrect to pass in "comparator" + return sorted; + } + + /** + * {@inheritDoc} + * + * @since 4.13 + */ + @Override + boolean validateOrderingIsCorrect() { + return false; + } } diff --git a/src/main/java/org/junit/runners/ParentRunner.java b/src/main/java/org/junit/runners/ParentRunner.java index 4949c242e5b2..1f69e0567a99 100644 --- a/src/main/java/org/junit/runners/ParentRunner.java +++ b/src/main/java/org/junit/runners/ParentRunner.java @@ -8,11 +8,12 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -31,8 +32,10 @@ import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; import org.junit.runner.manipulation.NoTestsRemainException; -import org.junit.runner.manipulation.Sortable; +import org.junit.runner.manipulation.Orderable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; @@ -61,7 +64,7 @@ * @since 4.5 */ public abstract class ParentRunner extends Runner implements Filterable, - Sortable { + Orderable { private static final List VALIDATORS = Arrays.asList( new AnnotationsValidator()); @@ -69,7 +72,7 @@ public abstract class ParentRunner extends Runner implements Filterable, private final TestClass testClass; // Guarded by childrenLock - private volatile Collection filteredChildren = null; + private volatile List filteredChildren = null; private volatile RunnerScheduler scheduler = new RunnerScheduler() { public void schedule(Runnable childStatement) { @@ -422,7 +425,7 @@ public void filter(Filter filter) throws NoTestsRemainException { iter.remove(); } } - filteredChildren = Collections.unmodifiableCollection(children); + filteredChildren = Collections.unmodifiableList(children); if (filteredChildren.isEmpty()) { throw new NoTestsRemainException(); } @@ -439,7 +442,43 @@ public void sort(Sorter sorter) { } List sortedChildren = new ArrayList(getFilteredChildren()); Collections.sort(sortedChildren, comparator(sorter)); - filteredChildren = Collections.unmodifiableCollection(sortedChildren); + filteredChildren = Collections.unmodifiableList(sortedChildren); + } finally { + childrenLock.unlock(); + } + } + + /** + * Implementation of {@link Orderable#order(Orderer)}. + * + * @since 4.13 + */ + public void order(Orderer orderer) throws InvalidOrderingException { + childrenLock.lock(); + try { + List children = getFilteredChildren(); + // In theory, we could have duplicate Descriptions. De-dup them before ordering, + // and add them back at the end. + Map> childMap = new LinkedHashMap>( + children.size()); + for (T child : children) { + Description description = describeChild(child); + List childrenWithDescription = childMap.get(description); + if (childrenWithDescription == null) { + childrenWithDescription = new ArrayList(1); + childMap.put(description, childrenWithDescription); + } + childrenWithDescription.add(child); + orderer.apply(child); + } + + List inOrder = orderer.order(childMap.keySet()); + + children = new ArrayList(children.size()); + for (Description description : inOrder) { + children.addAll(childMap.get(description)); + } + filteredChildren = Collections.unmodifiableList(children); } finally { childrenLock.unlock(); } @@ -457,12 +496,13 @@ private void validate() throws InitializationError { } } - private Collection getFilteredChildren() { + private List getFilteredChildren() { if (filteredChildren == null) { childrenLock.lock(); try { if (filteredChildren == null) { - filteredChildren = Collections.unmodifiableCollection(getChildren()); + filteredChildren = Collections.unmodifiableList( + new ArrayList(getChildren())); } } finally { childrenLock.unlock(); diff --git a/src/main/java/org/junit/runners/model/RunnerBuilder.java b/src/main/java/org/junit/runners/model/RunnerBuilder.java index bc6f85f04813..ba7c9e24d6ba 100644 --- a/src/main/java/org/junit/runners/model/RunnerBuilder.java +++ b/src/main/java/org/junit/runners/model/RunnerBuilder.java @@ -6,7 +6,11 @@ import java.util.Set; import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.runner.Description; +import org.junit.runner.OrderWith; import org.junit.runner.Runner; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.Ordering; /** * A RunnerBuilder is a strategy for constructing runners for classes. @@ -63,12 +67,25 @@ public abstract class RunnerBuilder { */ public Runner safeRunnerForClass(Class testClass) { try { - return runnerForClass(testClass); + Runner runner = runnerForClass(testClass); + if (runner != null) { + configureRunner(runner); + } + return runner; } catch (Throwable e) { return new ErrorReportingRunner(testClass, e); } } + private void configureRunner(Runner runner) throws InvalidOrderingException { + Description description = runner.getDescription(); + OrderWith orderWith = description.getAnnotation(OrderWith.class); + if (orderWith != null) { + Ordering ordering = Ordering.definedBy(orderWith.value(), description); + ordering.apply(runner); + } + } + Class addParent(Class parent) throws InitializationError { if (!parents.add(parent)) { throw new InitializationError(String.format("class '%s' (possibly indirectly) contains itself as a SuiteClass", parent.getName())); diff --git a/src/test/java/org/junit/tests/manipulation/AllManipulationTests.java b/src/test/java/org/junit/tests/manipulation/AllManipulationTests.java index d7532152b490..5989e1be7e51 100644 --- a/src/test/java/org/junit/tests/manipulation/AllManipulationTests.java +++ b/src/test/java/org/junit/tests/manipulation/AllManipulationTests.java @@ -8,6 +8,8 @@ @SuiteClasses({ FilterableTest.class, FilterTest.class, + OrderableTest.class, + OrderWithTest.class, SingleMethodTest.class, SortableTest.class }) diff --git a/src/test/java/org/junit/tests/manipulation/AlphanumericOrdering.java b/src/test/java/org/junit/tests/manipulation/AlphanumericOrdering.java new file mode 100644 index 000000000000..ff1a513e9119 --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/AlphanumericOrdering.java @@ -0,0 +1,15 @@ +package org.junit.tests.manipulation; + +import org.junit.runner.manipulation.Ordering; + +/** + * An ordering that orders tests alphanumerically by test name. + */ +public final class AlphanumericOrdering implements Ordering.Factory { + public static final ComparatorBasedOrdering INSTANCE = new ComparatorBasedOrdering( + Comparators.alphanumeric()); + + public Ordering create(Ordering.Context context) { + return INSTANCE; + } +} diff --git a/src/test/java/org/junit/tests/manipulation/ComparatorBasedOrdering.java b/src/test/java/org/junit/tests/manipulation/ComparatorBasedOrdering.java new file mode 100644 index 000000000000..6eff4a94dda9 --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/ComparatorBasedOrdering.java @@ -0,0 +1,28 @@ +package org.junit.tests.manipulation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.runner.Description; +import org.junit.runner.manipulation.Ordering; + +/** + * An ordering that internally uses a {@link Comparator}. + */ +class ComparatorBasedOrdering extends Ordering { + private final Comparator comparator; + + protected ComparatorBasedOrdering(Comparator comparator) { + this.comparator = comparator; + } + + @Override + protected List orderItems(Collection descriptions) { + List ordered = new ArrayList(descriptions); + Collections.sort(ordered, comparator); + return ordered; + } +} diff --git a/src/test/java/org/junit/tests/manipulation/Comparators.java b/src/test/java/org/junit/tests/manipulation/Comparators.java new file mode 100644 index 000000000000..4bba3238a470 --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/Comparators.java @@ -0,0 +1,19 @@ +package org.junit.tests.manipulation; + +import java.util.Comparator; + +import org.junit.runner.Description; +import org.junit.runner.manipulation.Alphanumeric; + +/** + * Factory and utility metods for creating {@link Comparator} instances for tests. + */ +class Comparators { + private static final Comparator ALPHANUMERIC = new Alphanumeric(); + + private Comparators() {} + + public static Comparator alphanumeric() { + return ALPHANUMERIC; + } +} diff --git a/src/test/java/org/junit/tests/manipulation/OrderWithTest.java b/src/test/java/org/junit/tests/manipulation/OrderWithTest.java new file mode 100644 index 000000000000..bfefeb35022b --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/OrderWithTest.java @@ -0,0 +1,268 @@ +package org.junit.tests.manipulation; + +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.OrderWith; +import org.junit.runner.Request; +import org.junit.runner.RunWith; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Alphanumeric; +import org.junit.runner.notification.RunNotifier; + +@RunWith(Enclosed.class) +public class OrderWithTest { + + public static class TestClassRunnerIsOrderableViaOrderWith { + private static String log = ""; + + public static class Unordered { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + } + + @OrderWith(AlphanumericOrdering.class) + public static class OrderedAlphanumerically extends Unordered { + } + + @OrderWith(ReverseAlphanumericOrdering.class) + public static class OrderedReverseAlphanumerically extends Unordered { + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void orderingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(OrderedAlphanumerically.class); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void orderingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(OrderedReverseAlphanumerically.class); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + + @RunWith(Enclosed.class) + public static class UnorderedSuite { + public static class A { + @Test + public void a() { + log += "Aa"; + } + + @Test + public void b() { + log += "Ab"; + } + + @Test + public void c() { + log += "Ac"; + } + } + + public static class B { + @Test + public void a() { + log += "Ba"; + } + + @Test + public void b() { + log += "Bb"; + } + + @Test + public void c() { + log += "Bc"; + } + } + } + + @OrderWith(AlphanumericOrdering.class) + public static class SuiteOrderedAlphanumerically extends UnorderedSuite { + } + + @OrderWith(ReverseAlphanumericOrdering.class) + public static class SuiteOrderedReverseAlphanumerically extends UnorderedSuite { + } + + @Test + public void orderingForwardWorksOnSuite() { + Request forward = Request.aClass(SuiteOrderedAlphanumerically.class); + + new JUnitCore().run(forward); + assertEquals("AaAbAcBaBbBc", log); + } + + @Test + public void orderingBackwardWorksOnSuite() { + Request backward = Request.aClass(SuiteOrderedReverseAlphanumerically.class); + + new JUnitCore().run(backward); + assertEquals("BcBbBaAcAbAa", log); + } + } + + public static class TestClassRunnerIsSortableViaOrderWith { + private static String log = ""; + + public static class Unordered { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + } + + @Before + public void resetLog() { + log = ""; + } + + @OrderWith(Alphanumeric.class) + public static class SortedAlphanumerically extends Unordered { + } + + @OrderWith(ReverseAlphanumericSorter.class) + public static class SortedReverseAlphanumerically extends Unordered { + } + + @Test + public void sortingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(SortedAlphanumerically.class); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void sortingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(SortedReverseAlphanumerically.class); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + } + + public static class TestClassRunnerIsOrderableWithSuiteMethod { + private static String log = ""; + + public static class Unordered { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + } + + @OrderWith(AlphanumericOrdering.class) + public static class OrderedAlphanumerically extends Unordered { + + public static junit.framework.Test suite() { + return new JUnit4TestAdapter(OrderedAlphanumerically.class); + } + } + + @OrderWith(ReverseAlphanumericOrdering.class) + public static class OrderedReverseAlphanumerically extends Unordered { + + public static junit.framework.Test suite() { + return new JUnit4TestAdapter(OrderedReverseAlphanumerically.class); + } + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void orderingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(OrderedAlphanumerically.class); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void orderingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(OrderedReverseAlphanumerically.class); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + } + + public static class UnOrderableRunnersAreHandledWithoutCrashing { + public static class UnOrderableRunner extends Runner { + public UnOrderableRunner(Class klass) { + } + + @Override + public Description getDescription() { + return Description.EMPTY; + } + + @Override + public void run(RunNotifier notifier) { + } + } + + @RunWith(UnOrderableRunner.class) + public static class UnOrderable { + @Test + public void a() { + } + } + + @Test + public void unOrderablesAreHandledWithoutCrashing() { + Request unordered = Request.aClass(UnOrderable.class).orderWith( + AlphanumericOrdering.INSTANCE); + new JUnitCore().run(unordered); + } + } +} diff --git a/src/test/java/org/junit/tests/manipulation/OrderableTest.java b/src/test/java/org/junit/tests/manipulation/OrderableTest.java new file mode 100644 index 000000000000..3a787dcdd535 --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/OrderableTest.java @@ -0,0 +1,272 @@ +package org.junit.tests.manipulation; + +import static org.junit.Assert.assertEquals; +import junit.framework.JUnit4TestAdapter; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; +import org.junit.runner.RunWith; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.Orderable; +import org.junit.runner.manipulation.Sorter; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; + +@RunWith(Enclosed.class) +public class OrderableTest { + + public static class TestClassRunnerIsOrderable { + private static String log = ""; + + public static class OrderMe { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void orderingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(OrderMe.class).orderWith( + AlphanumericOrdering.INSTANCE); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void orderingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(OrderMe.class).orderWith( + new ReverseAlphanumericOrdering()); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + + @RunWith(Enclosed.class) + public static class Enclosing { + public static class A { + @Test + public void a() { + log += "Aa"; + } + + @Test + public void b() { + log += "Ab"; + } + + @Test + public void c() { + log += "Ac"; + } + } + + public static class B { + @Test + public void a() { + log += "Ba"; + } + + @Test + public void b() { + log += "Bb"; + } + + @Test + public void c() { + log += "Bc"; + } + } + } + + @Test + public void orderingForwardWorksOnSuite() { + Request forward = Request.aClass(Enclosing.class).orderWith( + AlphanumericOrdering.INSTANCE); + + new JUnitCore().run(forward); + assertEquals("AaAbAcBaBbBc", log); + } + + @Test + public void orderingBackwardWorksOnSuite() { + Request backward = Request.aClass(Enclosing.class).orderWith( + new ReverseAlphanumericOrdering()); + + new JUnitCore().run(backward); + assertEquals("BcBbBaAcAbAa", log); + } + } + + public static class TestOrderableClassRunnerIsSortable { + private static String log = ""; + + /** + * A Runner that implements {@link Orderable}. + */ + public static class OrderableRunner extends Runner implements Orderable { + private final BlockJUnit4ClassRunner delegate; + + public OrderableRunner(Class klass) throws Throwable { + delegate = new BlockJUnit4ClassRunner(klass); + } + + @Override + public void run(RunNotifier notifier) { + delegate.run(notifier); + } + + @Override + public Description getDescription() { + return delegate.getDescription(); + } + + public void order(Orderer orderer) throws InvalidOrderingException { + delegate.order(orderer); + } + + public void sort(Sorter sorter) { + delegate.sort(sorter); + } + } + + @RunWith(OrderableRunner.class) + public static class OrderMe { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void orderingorwardWorksOnTestClassRunner() { + Request forward = Request.aClass(OrderMe.class).orderWith( + AlphanumericOrdering.INSTANCE); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void orderedBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(OrderMe.class).orderWith( + new ReverseAlphanumericOrdering()); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + } + + public static class TestClassRunnerIsOrderableWithSuiteMethod { + private static String log = ""; + + public static class OrderMe { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + + public static junit.framework.Test suite() { + return new JUnit4TestAdapter(OrderMe.class); + } + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void orderingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(OrderMe.class).orderWith(AlphanumericOrdering.INSTANCE); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void orderingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(OrderMe.class).orderWith( + new ReverseAlphanumericOrdering()); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + } + + public static class UnOrderableRunnersAreHandledWithoutCrashing { + public static class UnOrderableRunner extends Runner { + public UnOrderableRunner(Class klass) { + } + + @Override + public Description getDescription() { + return Description.EMPTY; + } + + @Override + public void run(RunNotifier notifier) { + } + } + + @RunWith(UnOrderableRunner.class) + public static class UnOrderable { + @Test + public void a() { + } + } + + @Test + public void unOrderablesAreHandledWithoutCrashing() { + Request unordered = Request.aClass(UnOrderable.class).orderWith( + AlphanumericOrdering.INSTANCE); + new JUnitCore().run(unordered); + } + } +} diff --git a/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericOrdering.java b/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericOrdering.java new file mode 100644 index 000000000000..b93d5c6a9887 --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericOrdering.java @@ -0,0 +1,20 @@ +package org.junit.tests.manipulation; + +import static java.util.Collections.reverseOrder; + +import org.junit.runner.manipulation.Ordering; + +/** + * An ordering that orders tests reverse alphanumerically by test name. + */ +public final class ReverseAlphanumericOrdering extends ComparatorBasedOrdering + implements Ordering.Factory { + + public ReverseAlphanumericOrdering() { + super(reverseOrder(Comparators.alphanumeric())); + } + + public Ordering create(Context context) { + return this; + } +} diff --git a/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericSorter.java b/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericSorter.java new file mode 100644 index 000000000000..415253ebc62d --- /dev/null +++ b/src/test/java/org/junit/tests/manipulation/ReverseAlphanumericSorter.java @@ -0,0 +1,16 @@ +package org.junit.tests.manipulation; + +import static java.util.Collections.reverseOrder; + +import org.junit.runner.manipulation.Ordering; +import org.junit.runner.manipulation.Sorter; + +/** + * A sorter that orders tests reverse alphanumerically by test name. + */ +public final class ReverseAlphanumericSorter implements Ordering.Factory { + + public Ordering create(Ordering.Context context) { + return new Sorter(reverseOrder(Comparators.alphanumeric())); + } +} diff --git a/src/test/java/org/junit/tests/manipulation/SortableTest.java b/src/test/java/org/junit/tests/manipulation/SortableTest.java index 9d10ff95f313..0ccb9683961d 100644 --- a/src/test/java/org/junit/tests/manipulation/SortableTest.java +++ b/src/test/java/org/junit/tests/manipulation/SortableTest.java @@ -1,5 +1,6 @@ package org.junit.tests.manipulation; +import static java.util.Collections.reverseOrder; import static org.junit.Assert.assertEquals; import java.util.Comparator; @@ -13,24 +14,20 @@ import org.junit.runner.Request; import org.junit.runner.RunWith; import org.junit.runner.Runner; +import org.junit.runner.manipulation.Orderable; +import org.junit.runner.manipulation.Sortable; +import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; @RunWith(Enclosed.class) public class SortableTest { private static Comparator forward() { - return new Comparator() { - public int compare(Description o1, Description o2) { - return o1.getDisplayName().compareTo(o2.getDisplayName()); - } - }; + return Comparators.alphanumeric(); } private static Comparator backward() { - return new Comparator() { - public int compare(Description o1, Description o2) { - return o2.getDisplayName().compareTo(o1.getDisplayName()); - } - }; + return reverseOrder(Comparators.alphanumeric()); } public static class TestClassRunnerIsSortable { @@ -202,4 +199,76 @@ public void unsortablesAreHandledWithoutCrashing() { new JUnitCore().run(unsorted); } } + + public static class TestOnlySortableClassRunnerIsSortable { + private static String log = ""; + + /** + * A Runner that implements {@link Sortable} but not {@link Orderable}. + */ + public static class SortableRunner extends Runner implements Sortable { + private final BlockJUnit4ClassRunner delegate; + + public SortableRunner(Class klass) throws Throwable { + delegate = new BlockJUnit4ClassRunner(klass); + } + + @Override + public void run(RunNotifier notifier) { + delegate.run(notifier); + } + + @Override + public Description getDescription() { + return delegate.getDescription(); + } + + public void sort(Sorter sorter) { + delegate.sort(sorter); + } + } + + @RunWith(SortableRunner.class) + public static class SortMe { + @Test + public void a() { + log += "a"; + } + + @Test + public void b() { + log += "b"; + } + + @Test + public void c() { + log += "c"; + } + + public static junit.framework.Test suite() { + return new JUnit4TestAdapter(SortMe.class); + } + } + + @Before + public void resetLog() { + log = ""; + } + + @Test + public void sortingForwardWorksOnTestClassRunner() { + Request forward = Request.aClass(SortMe.class).sortWith(forward()); + + new JUnitCore().run(forward); + assertEquals("abc", log); + } + + @Test + public void sortingBackwardWorksOnTestClassRunner() { + Request backward = Request.aClass(SortMe.class).sortWith(backward()); + + new JUnitCore().run(backward); + assertEquals("cba", log); + } + } }