Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ordering, Orderable and @OrderWith #1130

Merged
merged 29 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
44b64df
Add Ordering, Orderable and @OrderWith.
kcooney Mar 28, 2015
e68c015
Remove unreachable code
kcooney Apr 25, 2015
4770fff
Handle @RunWith in RunnerBuilder
kcooney Apr 30, 2015
a19a3ab
Change Sorter to no longer extend Ordering.
kcooney Apr 30, 2015
82e019f
Revert "Change Sorter to no longer extend Ordering."
kcooney May 1, 2015
f19f035
Replace Ordering.order(List) with Ordering.orderDescription(Description)
kcooney May 1, 2015
8390bc7
Rename GenericOrdering to GeneralOrdering
kcooney Jan 7, 2017
6e38776
Merge branch 'master' into ordering
kcooney Jan 7, 2017
1ba37d2
Add Ordering.Context so Orderings can use the Description to get
kcooney Jan 7, 2017
62ca6a0
Rename parameters in applyOrdering() and Sorter.apply() from "runner"…
kcooney Jan 7, 2017
753842d
Check ordering correctness in Ordering.
kcooney Jan 7, 2017
ea71fa4
Pass Ordering.Context in constructor when reflectively creating insta…
kcooney Jan 7, 2017
121744f
Remove unnecessary call to unmodifableCollection()
kcooney Jan 7, 2017
9d71b2f
Remove use of ReflectiveOperationException
kcooney Jan 8, 2017
5a7186b
Merge branch 'master' into ordering
kcooney May 15, 2017
2f9d21a
Fix javadoc for orderWith()
kcooney May 15, 2017
c6de86d
Minor formatting fixes
kcooney May 15, 2017
5a3f954
Add Ordering.Factory.
kcooney May 18, 2017
f2f6131
Merge branch 'master' into ordering
kcooney May 18, 2017
4ce52c1
Add missing @since Javadoc for Ordering methods
kcooney May 18, 2017
d8a1ee6
Merge branch 'master' into ordering
kcooney May 26, 2017
bfbad94
Minor formatting fix.
kcooney Jun 27, 2017
9fb4772
Merge branch 'master' into ordering
kcooney Aug 7, 2017
78ee8c6
Rename ComparsionBasedOrdering to ComparatorBasedOrdering; fix Javadoc
kcooney Jun 1, 2018
9d1e2aa
Rename GeneralOrdering to Orderer and make it no longer implement Ord…
kcooney Jun 1, 2018
550654a
Add OrderingRequest, to ensure orderWith() orders once
kcooney Jun 2, 2018
b2ce86a
Introduce MemoizingRequest
kcooney Jun 2, 2018
ca3e040
Sort tests in AllManipulationTests
kcooney Jun 2, 2018
3e6d464
Removed Comparators.reverse(); it's broken and the JDK provides a bet…
kcooney Jun 2, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/main/java/junit/framework/JUnit4TestAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.GenericOrdering;
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;

public class JUnit4TestAdapter implements Test, Filterable, Sortable, Describable {
public class JUnit4TestAdapter implements Test, Filterable, Orderable, Describable {
private final Class<?> fNewTestClass;

private final Runner fRunner;
Expand Down Expand Up @@ -83,4 +85,8 @@ public void filter(Filter filter) throws NoTestsRemainException {
public void sort(Sorter sorter) {
sorter.apply(fRunner);
}

public void order(GenericOrdering ordering) throws InvalidOrderingException {
ordering.apply(fRunner);
}
}
17 changes: 14 additions & 3 deletions src/main/java/org/junit/internal/runners/JUnit38ClassRunner.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.GenericOrdering;
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;
Expand Down Expand Up @@ -170,6 +174,13 @@ public void sort(Sorter sorter) {
}
}

public void order(GenericOrdering ordering) throws InvalidOrderingException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this take regular Ordering?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Sorter extends Ordering, but we want to make sure that anyone attempting to do a sort of a runner goes through Ordering.apply(Object). Originally I didn't have Sorter extend Ordering, and there the code was more complicated and had more duplication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the duplication? I can see that one would want to be able to do both @OrderWith(MyOrdering.class) and @OrderWith(MySorter.class), and that's difficult unless one of the classes extends the other. But it looks as though Ordering is essentially a marker interface at this point: most of the code that actually does anything is assuming that all Orderings are either Sorters or GenericOrderings. If you like the RunRule suggestion below, this becomes more of an asset than a liability, but if not, maybe there's some further shuffling that could happen here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Not editing earlier comment in case you're currently responding, but "Why the duplication?" is too strong. More to the point: "I see the case of the target type of OrderWith. Are there other places?")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should chat over VC, but I'll try to answer

If Sorter doesn't extend Ordering then we have to write extra code to allow creation of an Ordering from a Sorter or Comparator (since Ordering is a very generic concept, it seems reasonable that one could have an Ordering that performs a sort; to do otherwise would require people to choose an API based on the current implementation).

Also, as I said earlier, if we allow order() to take in an Ordering and that ordering does a sort, then we'll get worse performance than we would if the caller was smart enough to "know" that they should instead call sort() (really they shouldn't call either, and instead apply the Ordering or Sorter to the Runner).

Note that I also made Orderable extend Sortable. We could change that if we think it's an extra burden to people that write Orderable runners to have to also make them Sortable. If you want to get a hint as to the changes I would need to make if Orderable did not extend Sortable, look at the second commit in this pull (the "unreachable code" would be reachable).

if (getTest() instanceof Orderable) {
Orderable adapter = (Orderable) getTest();
adapter.order(ordering);
}
}

private void setTest(Test test) {
this.test = test;
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/junit/runner/OrderWith.java
Original file line number Diff line number Diff line change
@@ -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 <code>&#064;OrderWith</code> or extends a class annotated
* with <code>&#064;OrderWith</code>, 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple musings here (which I may not finish all at once)

  1. I'd be strongly tempted to make this an annotated static field instead/in-addition. Why? Well, consider the case (which I've written several times in the past) where you try to run the tests from the fastest to the slowest, based on a record of previous runtimes kept in a file. It probably makes sense to allow the location of the file to be specified by the test writer. I think that with the current pull request, this would be 100% impossible. One COULD imagine a different spec of this annotation where what's returned is not an Ordering, but a functional interface that takes a Description and returns an Ordering, so that one could write a Fastest Ordering that looks for a @TestTimesFile annotation on the Description to know what file to use, BUT...

It might be so much easier to instead just say:

@OrderWith public static Ordering ordering = new FastestOrdering("/tmp/test-times.txt");

or some such.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dsaff I don't see how what you are proposing would be 100% impossible. You could write your own ordering that reads the file the first time apply() is called. If the location of the file should be specified by the test writer, then your Ordering could use a custom annotation.

I'd hate to add more broiler-plate because of a possibly rare use case. Although I've personally never been bothered by adding a method with an annotation that returns something, other people have objected loudly to this (for example, with Rules).

The problem with passing a Description when you create an ordering is that an ordering applies to all children. Luckily, you can get a Description from a Runner so a custom Ordering do a different algorithm based on the Description inside of Ordering.apply(). I could be convinced that Ordering.order() should also take in the parent Description.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my apply() method would cast the Object to a Runner, call getDescription, and then look for the custom annotation in the Description? On balance, I'm not a fan of asking end-extenders to cast things, but could see doing it if there are benefits gained.

I can't personally call this use case "possibly rare", since it's occurred in the majority of test-case sorting scenarios I've written (and since test re-ordering was an essential part of my thesis, I may be in the sorry position of having personally seen a healthy percentage of all test-ordering schemes our species have attempted...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dsaff would you be fine with allowing a static method annotated with OrderWith as an additional way of specifying an ordering, and document whether the static method overrides the annotation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dsaff alternatively, as I suggested, we could add the parent Description to the parameters passed to Ordering.order(), which should handle your use case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the long delay, but I hope that we can release JUnit 4.13 in 2017 and I'd like to include this pull.

I updated the code to pass a context object that allows you to access the top-level description
of the class annotated with @OrderWith. This will handle David's use-case of " run the tests from the fastest to the slowest, based on a record of previous runtimes kept in a file". The filename could
either be specified as an annotation on the class annotated with @OrderWith or it could be
derived from the class name of the class annotated with @OrderWith

/**
* @return a class that extends {@link Ordering} (must have a public no-argument constructor)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Sorry, deleted and re-instated the comment on a different line once I realized what a nightmare having two discussions on the same line of code would be. Alternatively, we could pop the whole conversation out to the general comment thread]

  1. Changes like this (which affect how any class processed by JUnit is run, regardless of the chosen runner) have been rare, on purpose. There's a good argument to be made in favor of this one, but as long as we've got the hood open, I wonder if something more general would make sense.

For example, there's not currently a FilterWith annotation, but once we accept this change, that seems to be a natural next square in the matrix to fill in, which would be another top-level change. Also related would be the common request to attach a custom runlistener to a class, ListenWith

Just thinking out loud, what if we made a top-level annotation (RunRule? Modify?) which would handle any of these changes? Essentially:

public interface RunRule {
public Runner apply(Runner runner, Description classBeingRun);
}

and then:

public class MyTest {
@UseRunRule public static RunRule ordering = new UseOrdering(Ordering.random());
@UseRunRule public static RunRule listening = new AddListener(new MyListener());
// etc...
}

Note that:

a) This builds on the static field suggestion in (1), but could be orthogonal to it.
b) I'm not thrilled with the top-of-my-head naming here; especially, RunRule makes this sound tied to Rule, for better or for worse.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we would want end users to have to understand that filtering and sorting are applied via this new rule mechanism. They are orthogonal, and I think we should provide the simplest possible API to enable them.

I don't see why one couldn't implement @FilterWith today, without any changes outside of ParentRunner. Anyone extending ParentRunner would get that "for free", and filtering would work for children in a suite as long as the children implement Filterable. It sounds like what you are proposing would require a new general purpose mix-in interface, and users would have the confusing problem of having a Runner that's Filterable but can't be filtered via a RunRule

What you are proposing sounds like a larger redesign of runners, and one we should consider as part of the runner refactoring being proposed by JUnitLambda.

*/
Class<? extends Ordering> value();
}
54 changes: 48 additions & 6 deletions src/main/java/org/junit/runner/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.junit.internal.requests.SortingRequest;
import org.junit.internal.runners.ErrorReportingRunner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.InvalidOrderingException;
import org.junit.runner.manipulation.Ordering;
import org.junit.runners.model.InitializationError;

/**
Expand Down Expand Up @@ -148,15 +150,15 @@ public Request filterWith(final Description desiredDescription) {
* For example, here is code to run a test suite in alphabetical order:
* <pre>
* private static Comparator&lt;Description&gt; forward() {
* return new Comparator&lt;Description&gt;() {
* public int compare(Description o1, Description o2) {
* return o1.getDisplayName().compareTo(o2.getDisplayName());
* }
* };
* return new Comparator&lt;Description&gt;() {
* 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()));
* }
* </pre>
*
Expand All @@ -166,4 +168,44 @@ public Request filterWith(final Description desiredDescription) {
public Request sortWith(Comparator<Description> comparator) {
return new SortingRequest(this, comparator);
}

/**
* Returns a Request whose Tests can be run in a certain order, defined by
* <code>ordering</code>
* <p>
* For example, here is code to run a test suite in reverse order:
* <pre>
* private static Ordering reverse() {
* return new Ordering() {
* public List&lt;Description&gt; order(Collection&lt;Description&gt; siblings) {
* List&lt;Description&gt; ordered = new ArrayList&lt;&gt;(siblings);
* Collections.reverse(ordered);
* return ordered;
* }
* }
* }
*
* public static main() {
* new JUnitCore().run(Request.aClass(AllTests.class).orderWith(reverse()));
* }
* </pre>
*
* @return a Request with ordered Tests
* @since 4.13
*/
public Request orderWith(final Ordering ordering) {
final Request delegate = this;
return new Request() {
@Override
public Runner getRunner() {
try {
Runner runner = delegate.getRunner();
ordering.apply(runner);
return runner;
} catch (InvalidOrderingException e) {
return new ErrorReportingRunner(ordering.getClass(), e);
}
}
};
}
}
36 changes: 36 additions & 0 deletions src/main/java/org/junit/runner/manipulation/GenericOrdering.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.junit.runner.manipulation;

import java.util.Collection;
import java.util.List;

import org.junit.runner.Description;

/**
* An {@link Ordering} that is not a {@link Sorter}.
*
* @since 4.13
*/
public final class GenericOrdering extends Ordering {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name and comment here is a bit confusing. I might call it DelegatingOrdering?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dsaff I would rather not call it DelegatingOrdering. It is specifically an Ordering that is known not to be a Sorter, and some of the core code depends on that. I'm open to other names and improved doc, of course.

private final Ordering delegate;

GenericOrdering(Ordering delegate) {
this.delegate = delegate;
}

@Override
public List<Description> order(Collection<Description> siblings) {
return delegate.order(siblings);
}

@Override
public void apply(Object runner) throws InvalidOrderingException {
/*
* We overwrite apply() to avoid having a GenericOrdering wrap another
* GenericOrdering.
*/
if (runner instanceof Orderable) {
Orderable orderable = (Orderable) runner;
orderable.order(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/junit/runner/manipulation/Orderable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.junit.runner.manipulation;

/**
* Interface for runners that allow ordering of tests.
*
* <p>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 <code>ordering</code>
*
* @throws InvalidOrderingException if ordering does something invalid (like remove or add children)
*/
void order(GenericOrdering ordering) throws InvalidOrderingException;
}
78 changes: 78 additions & 0 deletions src/main/java/org/junit/runner/manipulation/Ordering.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.junit.runner.manipulation;

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;

/**
* Reorders tests. An {@code Ordering} can reverse the order of tests, sort the
* order or even shuffle the order.
*
* <p>In general you will not need to use a <code>Ordering</code> directly.
* Instead, use {@link org.junit.runner.Request#orderWith(Ordering)}.
*
* @since 4.13
*/
public abstract class Ordering {

/**
* 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
public List<Description> order(Collection<Description> siblings) {
List<Description> shuffled = new ArrayList<Description>(siblings);
Collections.shuffle(shuffled, random);
return shuffled;
}
};
}

/**
* Creates an {@link Ordering} from the given class. The class must have a public no-argument constructor.
*
* @throws InvalidOrderingException if the instance could not be created
*/
public static Ordering definedBy(Class<? extends Ordering> orderingClass)
throws InvalidOrderingException {
try {
return orderingClass.newInstance();
} catch (InstantiationException e) {
throw new InvalidOrderingException("Could not create ordering", e);
} catch (IllegalAccessException e) {
throw new InvalidOrderingException("Could not create ordering", e);
}
}

/**
* Order the tests in <code>runner</code> using this ordering.
*
* @throws InvalidOrderingException if ordering does something invalid (like remove or add children)
*/
public void apply(Object runner) throws InvalidOrderingException {
/*
* If the runner is Sortable but not Orderable and this Ordering is a
* Sorter, then the Sorter subclass overrides apply() to apply the sort.
*
* Note that GenericOrdering also overrides apply() to avoid having a
* GenericOrdering wrap another GenericOrdering.
*/
if (runner instanceof Orderable) {
Orderable orderable = (Orderable) runner;
orderable.order(new GenericOrdering(this));
}
}

/**
* Orders the given descriptions (all of which have the same parent).
*
* @param siblings unmodifiable collection of descriptions to order
*/
public abstract List<Description> order(Collection<Description> siblings);
}
2 changes: 1 addition & 1 deletion src/main/java/org/junit/runner/manipulation/Sortable.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public interface Sortable {
*
* @param sorter the {@link Sorter} to use for sorting the tests
*/
public void sort(Sorter sorter);
void sort(Sorter sorter);

}
27 changes: 22 additions & 5 deletions src/main/java/org/junit/runner/manipulation/Sorter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
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;

Expand All @@ -10,7 +14,7 @@
*
* @since 4.0
*/
public class Sorter implements Comparator<Description> {
public class Sorter extends Ordering implements Comparator<Description> {
/**
* NULL is a <code>Sorter</code> that leaves elements in an undefined order
*/
Expand All @@ -33,16 +37,29 @@ public Sorter(Comparator<Description> comparator) {
}

/**
* Sorts the test in <code>runner</code> using <code>comparator</code>
* Sorts the test in <code>runner</code> using <code>comparator</code>.
*/
public void apply(Object object) {
if (object instanceof Sortable) {
Sortable sortable = (Sortable) object;
@Override
public void apply(Object runner) {
/*
* 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 (runner instanceof Sortable) {
Sortable sortable = (Sortable) runner;
sortable.sort(this);
}
}

public int compare(Description o1, Description o2) {
return comparator.compare(o1, o2);
}

@Override
public final List<Description> order(Collection<Description> siblings) {
List<Description> sorted = new ArrayList<Description>(siblings);
Collections.sort(sorted, this);
return sorted;
}
}
Loading