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

Bug: ParentRunner lost test Class from a separate class loader #1252

Merged
merged 3 commits into from
Sep 17, 2016

Conversation

Gordiychuk
Copy link
Contributor

When junit.jar located in one ClassLoader but runing tests in another ParentRunner can lost information about run class. For example if use @ClassRule and request test class(org.junit.runner.Description#getTestClass) we can get null, because ParentRunner instead of set Class as is to Description tranform it to class name string, as result org.junit.runner.Description#getTestClass execute Class.forName and can't find test class.

Spring-test fail with exception if we try use SpringClassRule

java.lang.NullPointerException
 at org.springframework.test.context.junit4.rules.SpringClassRule.validateSpringMethodRuleConfiguration(SpringClassRule.java:186)
 at org.springframework.test.context.junit4.rules.SpringClassRule.apply(SpringClassRule.java:134)
 at org.junit.rules.RunRules.applyAll(RunRules.java:26)
 at org.junit.rules.RunRules.<init>(RunRules.java:15)
 at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:245)
 at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:137)

As solution, now ParentRunner create Descriptor with explicit specify Class, name and uses annotations.

@Gordiychuk
Copy link
Contributor Author

Any updates?

@marcphilipp
Copy link
Member

Sorry, @Gordiychuk, but I did not get to doing a thorough review, yet.

getRunnerAnnotations());
Description description =
Description.createSuiteDescription(
getTestClass().getJavaClass(),
Copy link
Member

Choose a reason for hiding this comment

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

It looks like getJavaClass() can in theory return null. Just to be safe, I suggest you check for that here, and if it is null use getName() like we did before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kcooney is it work? If I set like test class null, method getName() return "null" how it will process next? But even if class can be null, code will work as they work before, because in that casen we initialize org.junit.runner.Description#fTestClass by null and in then if they null will try resolve by name

org.junit.runner.Description#getTestClass

public Class<?> getTestClass() {
        if (fTestClass != null) {
            return fTestClass;
        }
        String name = getClassName();
        if (name == null) {
            return null;
        }
        try {
            fTestClass = Class.forName(name, false, getClass().getClassLoader());
            return fTestClass;
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

Copy link
Member

Choose a reason for hiding this comment

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

But in this case Description wouldn't have a class name either.

Copy link
Member

Choose a reason for hiding this comment

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

I see what you mean.

I think it's confusing to add a creational method to Description that takes in a class and a name, since the name parameter will only be used if the class parameter is null. I would prefer we do this:

Class<?> clazz = getTestClass().getJavaClass();
Description description;
if (clazz == null) {
  description = Description.createSuiteDescription(
          getName(), 
          getRunnerAnnotations());
} else {
  description = Description.createSuiteDescription(
          getTestClass().getJavaClass(),
          getRunnerAnnotations());
}

Even then, this would be a change in behavior for subclasses of ParentRunner that override getName()

Edit: we could do this:

Class<?> clazz = getTestClass().getJavaClass();
Description description;
// if subclass overrides `getName()` then we should use it
// to maintain backwards compatibility with JUnit 4.12
if (clazz == null || !clazz.getName().equals(getName())) {
  description = Description.createSuiteDescription(
          getName(), 
          getRunnerAnnotations());
} else {
  description = Description.createSuiteDescription(
          getTestClass().getJavaClass(),
          getRunnerAnnotations());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fix it in new commit. But i think this kind of backward compatibility very confusing, Because firs, in ParentRunner we specify some class, and after that override it by getName() О_о

    @Test
    public void testBackwardCompatibilityWithOverrideGetName() throws Exception {
        final Class<TestWithClassRule> originalTestClass = TestWithClassRule.class;
        final Class<?> waitClass = ParentRunnerClassLoaderTest.class;

        ParentRunner<FrameworkMethod> subParentRunner = new BlockJUnit4ClassRunner(originalTestClass) {
            @Override
            protected String getName() {
                return waitClass.getName();
            }
        };

        Description description = subParentRunner.getDescription();
        Class<?> result = description.getTestClass();

        assertEquals("Subclass of ParentRunner can override getName method and specify another test class for run, " +
                "we should  maintain backwards compatibility with JUnit 4.12",
                waitClass, result
        );
    }

Copy link
Member

Choose a reason for hiding this comment

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

@Gordiychuk I have seen ParentRunnner used in cases where there is no Java class (for example, a JavaScript test). There are probably other cases where geName() is overridden (tests in JVM-based languages perhaps).

Unfortunately, the Runner APIs have many extension points so are hard to change without breaking someone. I think what you have here is safe.

@kcooney
Copy link
Member

kcooney commented Mar 23, 2016

LGTM

@marcphilipp
Copy link
Member

marcphilipp commented Apr 16, 2016

@kcooney I think the new factory method should have an additional displayName parameter. Then we could get rid of the if, couldn't we?

Edit: So this would be

Description.createSuiteDescription(getTestClass().getJavaClass(), getName(), getRunnerAnnotations());

@kcooney
Copy link
Member

kcooney commented Apr 16, 2016

@marcphilipp why would a suite description need both a class and a display name?

@marcphilipp
Copy link
Member

marcphilipp commented Apr 17, 2016

ParentRunner.getName() is designed to be overridden to return a name different from the name of the test class. Custom runners may do so and there is even an example in our own codebase: BlockJUnit4ClassRunnerWithParameters overrides getName() and stores its testClass.

Therefore, I think it would make sense to create suite descriptions that store both display name and class. As a bonus, it would simplify code in ParentRunner.getDescription().

@kcooney
Copy link
Member

kcooney commented Apr 17, 2016

The big issue I had with the original version of this pull is the new method in Description had very confusing symantics regaeding null. Please review the original change and the entire comment thread.

This change should properly handle classes that override getName().

I am personally fine with the slight amount of complexity in the implementation of ParentRunner if that leads to the new method in Description being simpler to understand.

Edit: If we can find a way to have the changes to the ParentRunner code and the Deacription API be both simple, that would be perfect.

@marcphilipp
Copy link
Member

I've read the entire comment thread but still don't see why createSuiteDescription with name and class parameters would be confusing. Sorry... :-/

@kcooney
Copy link
Member

kcooney commented Apr 17, 2016

@marcphilipp the problems are

  1. TestClass.getJavaClass() can return null
  2. It's unclear what Description.createSuiteDescription((Class<?) null, "name") should do
  3. It's unclear what ParentRunner.getDescription() should do if TestClass.getJavaClass() returns null and getName() was overridden.

For the second point, I personally dislike methods that silently allow null with special behavior, because it's easy to accidentally propagate nulls, so I think If createSuiteDescription(Class<?> testClass, String name, Annotation... annotations) exists then Description.createSuiteDescription((Class<?) null, "name") should throw an exception (note that createTestDescription((Class<?) null, "name") will throw a NullPointerException).

For the third point, the behavior in 4.12 is to use ParentRunner.getName() as the display name of the returned Description object. Unfortunately, when this happens, the behavior of Description.getTestClass() depends on the name that was passed in. In theory, if a subclass of ParentRunner could override getName() such that the test class is completely different type than the test class passed into the ParentRunner constructor.

@marcphilipp
Copy link
Member

@kcooney Thanks for the explanation! I see your point now.

I just thought it would be beneficial if we could support creating a suite description with a class and a custom name. However, I'm fine with merging this pull requests as it is.

@Gordiychuk
Copy link
Contributor Author

It PR can be merge or I should do more changes?

@marcphilipp
Copy link
Member

@kcooney Your call! :)

@kcooney
Copy link
Member

kcooney commented Jun 1, 2016

@Gordiychuk I don't think the new test classes would run in the maven build. Can you add them to the appropriate test suite?

When junit.jar located in one ClassLoader but runing tests in another ParentRunner can lost information about run class.
For example if use @ClassRule and request test class(org.junit.runner.Description#getTestClass) we can get null,
because ParentRunner instead of set Class as is to Description tranform it to class name string,
as result org.junit.runner.Description#getTestClass execute Class.forName and can't find test class.

Spring-test fail with exception if we try use SpringClassRule

```
java.lang.NullPointerException
 at org.springframework.test.context.junit4.rules.SpringClassRule.validateSpringMethodRuleConfiguration(SpringClassRule.java:186)
 at org.springframework.test.context.junit4.rules.SpringClassRule.apply(SpringClassRule.java:134)
 at org.junit.rules.RunRules.applyAll(RunRules.java:26)
 at org.junit.rules.RunRules.<init>(RunRules.java:15)
 at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:245)
 at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
```

As solution, now ParentRunner create Descriptor with explicit specify Class, name and uses annotations.
@Gordiychuk Gordiychuk force-pushed the bug_parent_runner_classloader branch from bb41b51 to 6f15096 Compare June 1, 2016 20:22
@Gordiychuk
Copy link
Contributor Author

@kcooney done, i also rebase branch on actual master state

@marcphilipp
Copy link
Member

LGTM!

@kcooney Do you want to merge it?

@kcooney
Copy link
Member

kcooney commented Jun 11, 2016

@marcphilipp will do soon. Want to take one more look.

@kcooney kcooney added the 4.13 label Jul 17, 2016
@kcooney kcooney merged commit 2b6fa70 into junit-team:master Sep 17, 2016
@kcooney
Copy link
Member

kcooney commented Sep 17, 2016

@Gordiychuk could you please update the release notes?

@Gordiychuk
Copy link
Contributor Author

@kcooney in what file?

@baev
Copy link
Contributor

baev commented Sep 19, 2016

@Gordiychuk
Copy link
Contributor Author

Done. Thanks for review changes.

@kcooney kcooney modified the milestone: 4.13 Aug 6, 2017
@kcooney kcooney removed the 4.13 label Aug 6, 2017
sebasjm pushed a commit to sebasjm/junit4 that referenced this pull request Mar 11, 2018
…-team#1252)

* Bug: ParentRunner lost test Class from a separate class loader

When junit.jar located in one ClassLoader but runing tests in another ParentRunner can lost information about run class.
For example if use @ClassRule and request test class(org.junit.runner.Description#getTestClass) we can get null,
because ParentRunner instead of set Class as is to Description tranform it to class name string,
as result org.junit.runner.Description#getTestClass execute Class.forName and can't find test class.

Spring-test fail with exception if we try use SpringClassRule

```
java.lang.NullPointerException
 at org.springframework.test.context.junit4.rules.SpringClassRule.validateSpringMethodRuleConfiguration(SpringClassRule.java:186)
 at org.springframework.test.context.junit4.rules.SpringClassRule.apply(SpringClassRule.java:134)
 at org.junit.rules.RunRules.applyAll(RunRules.java:26)
 at org.junit.rules.RunRules.<init>(RunRules.java:15)
 at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:245)
 at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
```

As solution, now ParentRunner create Descriptor with explicit specify Class, name and uses annotations.

* Restore backward compatibility with sub class of ParentRunner that override getName method
* Add ParentRunnerClassLoaderTest to AllClassesTests suite
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