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

@Rule(order=N) #1445

Closed
wants to merge 2 commits into from
Closed

@Rule(order=N) #1445

wants to merge 2 commits into from

Conversation

panchenko
Copy link
Contributor

@panchenko panchenko commented May 3, 2017

Yet another attempt to order @Rules, 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.

Copy link
Member

@kcooney kcooney left a 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);
Copy link
Member

@kcooney kcooney May 5, 2017

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>(",
Copy link
Member

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) {
Copy link
Member

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() {
Copy link
Member

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() {
Copy link
Member

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);
Copy link
Member

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()

@panchenko
Copy link
Contributor Author

@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 :-)

Copy link
Member

@kcooney kcooney left a 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));
Copy link
Member

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);
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Contributor Author

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());
Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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()

Copy link
Contributor Author

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.

Copy link
Member

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>();
Copy link
Member

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> {
Copy link
Member

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());
Copy link
Member

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;
Copy link
Member

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?

Copy link
Member

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.

@kcooney
Copy link
Member

kcooney commented May 6, 2017

If we merge this, should we deprecate RuleChain?

@panchenko
Copy link
Contributor Author

If we merge this, should we deprecate RuleChain?

I was also thinking of this.

@panchenko
Copy link
Contributor Author

I've made some more changes here, particularly ordering of @ClassRule happens in a more direct way, no ThreadLocal is needed there, so it's moved into BlockJUnit4ClassRunner.

@kcooney
Copy link
Member

kcooney commented May 9, 2017

@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,
Copy link
Member

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) {
Copy link
Member

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());
Copy link
Member

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.

@panchenko
Copy link
Contributor Author

panchenko commented May 11, 2017

@kcooney currently RunRules applies all the rules in the constructor, so the only difference between

  • new RunRules(statement, rules)
  • RunRules.applyAll(statement, rules)

is an additional RunRules instance with the evaluatemethod like this

public void evaluate() throws Throwable {
    statement.evaluate();
}

As an optimization I've removed this instance and make use of the constructed statement directly. I agree this change is not strictly necessary, and at the same time I can't understand why an extra object is needed here :-)

@kcooney
Copy link
Member

kcooney commented May 12, 2017

@panchenko ah didn't realize RunRules did the work in the constructor. Still, I prefer not to change it because 1) the change is unrelated to this pull 2) we should avoid depreciating things unless there is a clear reason for the current users to change, 3) the overhead of creating an object is low and 4) maybe we should apply the rules at statement execution time.

@panchenko
Copy link
Contributor Author

panchenko commented May 12, 2017

@kcooney reverted that change, will suggest it separately :-)
"4)" would be a behavior change, I don't think we should do it.

@kcooney
Copy link
Member

kcooney commented May 12, 2017

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 ExternalResource rules, I would expect the before() method of the rule with the lower order to happen first, but it happens the other way around. But for Verifier rules, I think I would expect the opposite. I think priority makes it clear that the attribute affects which one is applied first, but forces the user to read the Javadoc to get clarity (though we might want to provide an example of what it means to be "inner", since if you use rules but don't write them you might not understand)

@panchenko
Copy link
Contributor Author

panchenko commented May 13, 2017

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());

priority might cause more confusion, e.g. in Windows higher value means higher priority, so one might assume it would be executed the other way around. Reading documentation is a good thing, but sometimes a question to GitHub/StackOverflow/forum is posted before doing that :-)

I would suggest optimizing for the common use case, which I think is about creating some resource depending on another one. That's why order and sorting this way.

@@ -463,4 +441,22 @@ private long getTimeout(Test annotation) {
}
return annotation.timeout();
}

private static final ThreadLocal<RuleContainer> CURRENT_RULE_CONTAINER =
new ThreadLocal<RuleContainer>();
Copy link
Member

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?

Copy link
Contributor Author

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);
}
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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;
}
Copy link
Member

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)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed this.

@marcphilipp
Copy link
Member

@kcooney Are you fine with order now? PTAL!

@panchenko
Copy link
Contributor Author

@marcphilipp What's your opinion on deprecating RuleChain?

@marcphilipp
Copy link
Member

FWIW I think we should deprecate RuleChain.

@kcooney
Copy link
Member

kcooney commented May 25, 2017

Changes look great!

I'd like to merge this as two commits. The second commit would have the RuleChain changes and the first would have anything else. @panchenko are you comfortable enough with git to do this? If not, I can do it, but that would mess up the history in your repo.

@panchenko
Copy link
Contributor Author

@kcooney Sure, I've changed this to 2 commits.

@kcooney
Copy link
Member

kcooney commented May 26, 2017

@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!

@kcooney
Copy link
Member

kcooney commented Aug 7, 2017

@marcphilipp merged. Would you please update the release notes on the wki?

@panchenko
Copy link
Contributor Author

@kcooney I've added this to release notes.

@panchenko panchenko deleted the Rule_order branch August 26, 2017 04:41
sebasjm pushed a commit to sebasjm/junit4 that referenced this pull request Mar 11, 2018
aristotle0x01 pushed a commit to aristotle0x01/junit4 that referenced this pull request Jun 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants