-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
@Rule(order=N) #1445
@Rule(order=N) #1445
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution.
I'm usually not a fan of using thread locals in this way, but assuming we can limit who can access the thread-local, the harm seems small (some custom runners won't get ordered rules, but they would have to re implement large parts of BlockJUnit4ClassRunner
for that to happen).
List<T> results = new ArrayList<T>(); | ||
for (FrameworkField each : getAnnotatedFields(annotationClass)) { | ||
try { | ||
Object fieldValue = each.get(test); | ||
if (valueClass.isInstance(fieldValue)) { | ||
valueListener.acceptValue(each, fieldValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to have this depend on the thread-local; it's particularly far from the point where it is set, and this method can be called from other contexts.
When I was making an attempt at adding priorities, I ended up extracting classes to replace getAnnotatedFieldValues()
and getAnnotatedMethodValues()
. See #1448 (in particular, MethodValueCollector (usage) and FieldValueCollector (usage)
I ended up wrapping the rules with adapters that could be asked about the priority of the rule, but I wasn't all that excited about that particular approach. But perhaps you could use classes like those two to populate your RuleContainer
?
If we do this, then perhaps the thread local can be in a package-scope class.
@@ -224,9 +224,7 @@ private static boolean isTestFrameworkMethod(String methodName) { | |||
private static final String[] REFLECTION_METHOD_NAME_PREFIXES = { | |||
"sun.reflect.", | |||
"java.lang.reflect.", | |||
"org.junit.rules.RunRules.<init>(", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to keep RunRules
listed here, in case custom runners use that class.
|
||
private final List<RuleEntry> ruleEntries = new ArrayList<RuleEntry>(); | ||
|
||
public void add(MethodRule methodRule) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit fragile that this method should only be called after the container collected all of it's data (via the AnnotatedValueListener
.
There is a similar issue with between the add()
and sort()
methods, and between the sort()
and apply()
methods.
I'm hoping this problem goes away if you use something similar to my MethodValueCollector (see the other comment). If not, then I think think you either need some kind of a state enum (and throw IllegalStateException
if the methods are called out of order) or extract classes or interfaces.
ruleEntries.add(new TestRuleEntry(testRule, orderValues.get(testRule))); | ||
} | ||
|
||
public void sort() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only called right before the caller calls apply
. I suggest making it private and calling it from apply()
)
this.order = order != null ? order.intValue() : -1; | ||
} | ||
|
||
public int getInterfacePriority() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest just making this method abstract
}; | ||
List<TestRule> testRules; | ||
List<MethodRule> methodRules; | ||
AnnotatedValueListener.CURRENT.set(ruleContainer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would probably want to do something similar in ParentRunner.withClassRules()
@kcooney Thanks for the feedback, I think I've adjusted most of the concerns, could you please give another look? It terms of code flow it should be better and we could possibly discuss how it should be named and in what packages the new classes should go :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow. Much cleaner than my attempt.
I like the package location and class naming. Missing some Javadoc, and a few minor issues, but it looks like we'll get Rule ordering in 4.13!
@@ -255,14 +284,13 @@ public String getName() { | |||
*/ | |||
if (valueClass.isAssignableFrom(each.getReturnType())) { | |||
Object fieldValue = each.invokeExplosively(test); | |||
results.add(valueClass.cast(fieldValue)); | |||
consumer.accept(each, valueClass.cast(fieldValue)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I noticed in my attempt is that we will call MethodRule
methods twice if the return value implements both TestRule
and MethodRule
(one of the values would be thrown away in withRules()
. My MethodValueCollector
class avoided this, because the collector subclass can filter out the values before the method value was called.
I'm not sure how much we should worry about that. Your approach is quite nice.
RuleContainer.CURRENT.set(ruleContainer); | ||
try { | ||
List<TestRule> testRules = getTestRules(target); | ||
List<MethodRule> methodRules = rules(target); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since methodRules
is used once, I suggest inlining it:
for (MethodRule each : rules(target)) {
...
(no idea why MethodRule was fully qualified here)
/** | ||
* @since 4.13 | ||
*/ | ||
public final class RuleContainer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this package-scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only question is how likely someone might want to reuse it? The only part which should stay "hidden" here is the ThreadLocal
field, the rest of the class can be moved into the internal
package.
public void accept(FrameworkMember member, TestRule value) { | ||
ClassRule rule = member.getAnnotation(ClassRule.class); | ||
if (rule != null) { | ||
RuleContainer.recordRuleOrder(value, rule.order()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious here that we are modifying a thread local. I would prefer that recordRuleOrder()
be a non-static method.
You can use ThreadLocal.inititalValue()
to initialize the thread local to a no-op value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, instead of doing all the work to create a no-op version of RuleCollector
, just do the null
check of the thread-local here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used twice - for @Rule
and @ClassRule
, are we OK with the small code duplication?
testRules.add(testRule); | ||
} | ||
|
||
public void addTestRules(List<? extends TestRule> rules) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's either name this addAll()
or rename the add()
methods to addTestRule()
and addMethodRule()
Or just get rid of it. It's trivial to inline, and you don't have addMethodRules()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used twice, so I've renamed the add()
methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still prefer to leave it out. If you keep it, rename it to addAllTestRules
and add a method named addAllMethodRules
* Returns entries in the order how they should be applied, i.e. inner-to-outer. | ||
*/ | ||
private List<RuleEntry> getSortedEntries() { | ||
List<RuleEntry> ruleEntries = new ArrayList<RuleEntry>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: new ArrayList<RuleEntry>(methodRules.size() + testRules.size())
/** | ||
* @since 4.13 | ||
*/ | ||
public interface MemberValueConsumer<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the name and the packaging. Could you add a short description of what it does in the class Javadoc, and add method Javadoc to every new public method that is in a public class?
public void methodRulesOnly() { | ||
container.add(MRule.M1); | ||
container.add(MRule.M2); | ||
assertEquals("[M1, M2]", container.getSortedRules().toString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's clever making the rules an enum and using toString()
.
* | ||
* @since 4.13 | ||
*/ | ||
int order() default -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like priority
best. I can live with order
. I don't like level
@junit-team/junit-committers what are your preferences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with order
.
If we merge this, should we deprecate |
I was also thinking of this. |
I've made some more changes here, particularly ordering of |
@panchenko thanks. I've busy with my primary job, so I might not have time to look into this until the weekend. |
* | ||
* @since 4.13 | ||
*/ | ||
public static Statement applyAll(Statement base, Iterable<TestRule> rules, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you need to create this new method. What's wrong with the RunRules
API?
testRules.add(testRule); | ||
} | ||
|
||
public void addTestRules(List<? extends TestRule> rules) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would still prefer to leave it out. If you keep it, rename it to addAllTestRules
and add a method named addAllMethodRules
RuleContainerHelper.CURRENT.remove(); | ||
} | ||
return ruleContainer.apply(null, getDescription(), null, statement); | ||
return RunRules.applyAll(statement, classRules(), getDescription()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not be applying rules in this method. Instead, we should return a Statement
which applies the rules, so the rules are applied when the final statement is evaluated. That's why RunRules
is a Statement
.
@kcooney currently RunRules applies all the rules in the constructor, so the only difference between
is an additional public void evaluate() throws Throwable {
statement.evaluate();
} As an optimization I've removed this instance and make use of the constructed |
@panchenko ah didn't realize |
@kcooney reverted that change, will suggest it separately :-) |
This LGTM. Thanks! The only open issue in my mind is the naming of the attribute. Order might be confusing, since if you ordered two |
I agree that naming is always an issue. Currently the rules with lower values are executed first, so for some kind of rules it's already intuitively clear, from the test: @Rule(order = 1)
public final TestRule a = new LoggingTestRule(ruleLog, "outer");
@Rule(order = 2)
public final TestRule z = new LoggingTestRule(ruleLog, "inner");
....
assertEquals(" outer.begin inner.begin foo inner.end outer.end", ruleLog.toString());
I would suggest optimizing for the common use case, which I think is about creating some resource depending on another one. That's why |
@@ -463,4 +441,22 @@ private long getTimeout(Test annotation) { | |||
} | |||
return annotation.timeout(); | |||
} | |||
|
|||
private static final ThreadLocal<RuleContainer> CURRENT_RULE_CONTAINER = | |||
new ThreadLocal<RuleContainer>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this have to be a ThreadLocal
? Can't we use an instance variable and pass it to the RuleCollector
constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This complexity comes from different methods returning MethodRule
and TestRule
instances - both are merged and ordered together.
result = ((TestRuleEntry) ruleEntry).rule.apply(result, description); | ||
} else { | ||
result = ((MethodRuleEntry) ruleEntry).rule.apply(result, method, target); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The if
can be replaced my introducing an apply(FrameworkMethod method, Description description, Object target, Statement statement)
method in RuleEntry
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible, however I am not sure that would improve readability. The number of lines would be increased for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if you don't want to change this, then please remove TestRuleEntry
and MethodRuleEntry
and do the instanceof
check on the contained rule class directly.
RuleEntry(T rule, int order) { | ||
this.rule = rule; | ||
this.order = order; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need both constructors (same below)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed this.
@kcooney Are you fine with |
@marcphilipp What's your opinion on deprecating |
FWIW I think we should deprecate RuleChain. |
Changes look great! I'd like to merge this as two commits. The second commit would have the |
@kcooney Sure, I've changed this to 2 commits. |
@panchenko would it be possible to put a few more details in the first commit message? No rush; I won't be spending time on JUnit this weekend. Thanks for all of the work recently! |
@marcphilipp merged. Would you please update the release notes on the wki? |
@kcooney I've added this to release notes. |
Yet another attempt to order
@Rule
s, based on the #1417 idea, issue #1221.No breaking API changes, with a cost of using
ThreadLocal
:-)both MethodRule & TestRule are supported.
Will add javadocs later, when we agree on the approach.