NodeInfo#getAnnotation returns instance of Cglib-generated that does not actually implement Annotation
Issue description
org.spockframework.runtime.model.NodeInfo#getAnnotation returns an instance of a generated class which is not implementing Annotation, hence it doesn't actually work when it's used as a ClassCastException is thrown.
How to reproduce
In a Spock extension, call getAnnotation on a SpecInfo instance as shown:
Title t = node.getAnnotation( Title )
This error will happen (when there's no annotation on the spec, at least):
org.spockframework.runtime.model.SpecInfo$$EnhancerByCGLIB$$b2e871b8 cannot be cast to java.lang.annotation.Annotation
I would've expected null to be returned and there should be no error.
Java/JDK
openjdk version "1.8.0_232"
Groovy version
- Groovy 3.0.1
- Spock 2.0-M2-groovy-3.0
Can you maybe provide a minimal reproducer project? And do you have the same problem when you use ByteBuddy instead of cglib?
Ah!! The problem seems to be on a test I have!!! The SpecInfo instance was an actual mock! So, what's changed is the mocking done by Spock, not SpecInfo itself!
Sorry about the misunderstanding. Not sure if this is still a bug, but I would expect a Mock to return null on non-mocked methods?
import org.spockframework.runtime.model.SpecInfo
import spock.lang.Specification
import spock.lang.Title
class MySpec extends Specification {
def "my first test"() {
given:
def spec = Stub(SpecInfo)
when:
def t = spec.getAnnotation(Title)
then:
t == null
}
}
This reproduces the problem. It works if I use Mock instead.
Now I'm a bit confused.
You say in your next to last comment you expect a Mock to return null.
But in your last comment you show that you use a Stub and say with Mock it works as expected.
Mocks are expected to return 0 or null on unmocked methods,
Stubs are expectec to do their very best to return some empty or dummy response, so for example Optional.empty(), an empty list, an empty array or for non-collections another stub.
This also is not new to 2.0 but is the same since I know Spock.
Mock and Stub should both return null for this method in my opinion:
public <T extends Annotation> T getAnnotation(Class<T> clazz)
This broke my tests after upgrading to 2.0, so I am pretty sure it's new.
Ok, just tested with both Cglib and ByteBuddy using Spock version 1.3-groovy-2.5, and they both throw this Exception. Really not sure why my tests had previously been working with that Stub, I suppose it's because the methods being mocked and the methods being called have changed after the Spock upgrade, and somehow this came up...
Closing the ticket as the behaviour is indeed the same as before.
You got me slightly wrong and should reopen the issue.
Yes, Stub should return a dummy object there, namely another stub.
But it should work properly and not result in a ClassCastException.
I guess the code thinks this is Object return type, not seing the bound on the type parameter and thus producing a wrong stub as return value.
Looks like org.spockframework.mock.runtime.StaticMockMethod#getReturnType doesn't handle generics with bounds correctly, it just returns java.lang.Object here. The code in the whole org.spockframework.gentyref.* package is from 2010/13 there is probably more information available in Java 8, at least the java.lang.reflect#getReturnType returns interface java.lang.annotation.Annotation. Also resolving generics based on method parameters is another task in itself.
I've updated the gentyref classes to the latest version and encountered the following problem https://github.com/coekie/gentyref/issues/5
I have opened the PR #1731 which fixes the specific issue with the getAnnotation() method not returning a stub instanceof Annotation.
But as @leonard84 already mentioned, this will not solve the "resolving generics based on method parameters" problem.
So the method <T extends Annotation> T getAnnotation(Class<T>) with a call like stub.getAnnotation(Nullable) should return a new stub instanceof Nullable, but is only instanceof Annotation after the PR #1731.