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

Added support for mock class creation using Byte Buddy #626

Merged
merged 1 commit into from Sep 30, 2016
Merged

Added support for mock class creation using Byte Buddy #626

merged 1 commit into from Sep 30, 2016

Conversation

raphw
Copy link
Contributor

@raphw raphw commented Jul 29, 2016

Adds support for generating classes using Byte Buddy instead of cglib when Byte Buddy is found on the class path. (Other than on AppVeyor, running :spock-specs:test works fine on my machine.)

@@ -3,6 +3,6 @@
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, duplicated entries look strange.

@szpak
Copy link
Member

szpak commented Jul 29, 2016

Thanks Rafael. I like the way Byte Buddy works in Mockito and I would be happy seeing it in Spock.

+1 for merging it before 1.1.0-final (as it could potentially change the default behavior if Byte Buddy is already on a classpath).

@raphw
Copy link
Contributor Author

raphw commented Jul 29, 2016

Glad to hear. There are some things I would like to mention before merging this:

  1. Byte Buddy can preserve annotations on overridden methods and the super type. This better mimics the original type if any code picks up annotations to interact with an instance. (A cglib mock does not preserve annotations).
  2. It enables default methods on additional interfaces (can be disabled). cglib does not support this
    so there can be a change in behaviour if a user does not expect this.
  3. Byte Buddy transparently handles bridge methods. Excluding bridges as done with cglib is generally not a good idea as there are so-called visibility-bridges which should be overridden. This is a change in behaviour.
  4. Byte Buddy handles generics gracefully, e.g. overrides methods appropriately and adds all bridges that the specification demands. It is even possible to extend a generic type (e.g. implement List<String> instead of a raw List.) Byte Buddy would however need to receive such a generic type as an argument.

What is your view on that?

@raphw
Copy link
Contributor Author

raphw commented Jul 30, 2016

On a side note: IntelliJ files should not be version controlled. Those files are considered internal and my installation just picked up the files and changed them without me even noticing. This happens every time I open the project.

@szpak
Copy link
Member

szpak commented Jul 30, 2016

Byte Buddy can preserve annotations on overridden methods and the super type. This better mimics the original type if any code picks up annotations to interact with an instance. (A cglib mock does not preserve annotations).

Sounds to be useful and quite safe to me.

It enables default methods on additional interfaces (can be disabled). cglib does not support this so there can be a change in behaviour if a user does not expect this.

I think that currently Spock doesn't support additional interfaces in mocks at all.

Byte Buddy transparently handles bridge methods. Excluding bridges as done with cglib is generally not a good idea as there are so-called visibility-bridges which should be overridden. This is a change in behaviour.

I cannot recall any situation when I hit that issue, however, I can image that it would be good to support that. I think that 1.1 already has introduced some incompatibilities and IMHO that one would be acceptable.

Byte Buddy handles generics gracefully, e.g. overrides methods appropriately and adds all bridges that the specification demands. It is even possible to extend a generic type (e.g. implement List instead of a raw List.) Byte Buddy would however need to receive such a generic type as an argument.

There are already some issues with mocking generic types in Spock. I would need to dig into to say how does it look.

However, there is one issue that came to my mind. Currently all internal tests in Spock have been switched to ByteBuddy (as it is on a classpath) and we lost regression testing against cglib (in Mockito cglib support was dropped, so it was not a problem). I don't know how much feasible is to already have Byte Buddy on a classpath (as a 3rd party dependency?) in the real project and still want to use cglib in Spock, but in a case of regression people may want to be able to switch to cglib for particular mocks (like additional configuration parameter in IMockConfiguration).

I don't have a good idea how to provide testing for both ByteBuddy and cglib without using some global configuration parameter (like a system property) set in an additional Gradle test task (for cglib). However, maybe we don't need it or maybe we may even want to treat cglib support as the 2nd class citizen (aka "deprecated mode")?

@robfletcher @sbglasius @leonard84 @Fuud WDYT?

@robfletcher
Copy link
Contributor

Can we get an example of the bridge method issue? I don't quite follow what we're losing or gaining there.

It would be a very good idea to have some kind of testing in place for cglib unless we are dropping support for it entirely.

@raphw
Copy link
Contributor Author

raphw commented Aug 4, 2016

The problem with bridge methods is that they are much more complex than they might appear. For this reason, Byte Buddy does not expose bridge methods or requires them to be handled manually in any context. For example, with cglib, you simply exclude any method with the bridge modifier. This can cause trouble in scenarios like:

/* package-private*/ class Foo { 
  public String foo() { return "foo"; } 
}
public class Bar extends Foo { }

The Java compiler will add a visibility-bridge for Bar to promote foo to become factually public (as no method within a package-private type is treated as public by core reflection) and compile the following class:

public class Bar extends Foo { 
  public /* synthetic bridge */ String foo() { return super.foo(); } 
}

By excluding bridge methods from interception, you do no longer mock the foo method with your cglib exclusion. This is typically a problem when working with inner/nested classes which often are compiled into a package-private type.

Also, cglib does not implement reified methods for a mocked type what can cause problems or change in behavior with some libraries and tools. So to speak, if you are mocking some:

public class Foo<T> {
  public void foo(T val) { } 
}
public class Bar extends Foo<String> { }

Byte Buddy will add the correct bridge when instrumenting Bar just as any compiler would do when extending Bar and overriding foo.

public class Bar$SpockMock extends Bar {
  public void foo(String val) { }
  public /* synthetic bridge */ void foo(Object val) { foo((String) val); }
}

Note that some frameworks will expect to being able to look up the actual method via a call similar to type.getMethod("foo", String.class) where the cglib mock would fail as it only overrides the Object signature method and does not implement the factual signature as required by the JLS.

As for testing cglib: Maybe Spock can supply a system property that decides if Byte Buddy is considered. This would allow both to (a) retain old testing behavior even if Byte Buddy is already on the class path (b) to run a build with mocks created by cglib. As an alternative, in Mockito, this is solved by using plugins where a build can register a custom byte code provider.

Class<?> enhancedType = CACHE.find(classLoader, type, additionalInterfaces);

if (enhancedType == null) {
synchronized (type) {
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this problematic, if someone else decides to lock based on a class. Why not lock on an internal object or use a real lock?

Copy link
Contributor Author

@raphw raphw Aug 7, 2016

Choose a reason for hiding this comment

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

The implicit cglib cache locks on the class object already today. This is just a recreation.

You are right that this can cause problems if the class object is already locked by another thread. Using a single monitor for this would however not allow concurrently creating mocks for different classes from different threads. The question is, what limitation is preferred.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah your are right, both approaches have their drawbacks. Could you document the rationale for this decision in a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typically, people create a single mock type per class. The alternative where Byte Buddy would need to create multiple mock types for mocking a type plus additional (explicitly specified) interfaces is rather the exception. Given that most mock types derive of a single type, the application is lock-free for these scenarios. Covering the 99% scenario lock free is probably a sane approach. Also, this requires locking only a single monitor such that dead locks are impossible (live locks are however still possible).

Locking on the class loader is more likely to cause problems, though, as class loaders typically lock on themselves during class loading. Creating a mock can cause class loading as using the reflection API (for finding methods to override) causes eager resolution of a type where all types references in methods are loaded. This is typically not a problem but can be one with parallel capable class loaders.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation, but I meant a short source code comment why we lock on class an not on an internal lock. Just to document that this is intentional and why.

@leonard84
Copy link
Member

@szpak valid points

but in a case of regression people may want to be able to switch to cglib for particular mocks (like additional configuration parameter in IMockConfiguration).

IMHO mixing implementations would make it more complicated.

I don't have a good idea how to provide testing for both ByteBuddy and cglib without using some global configuration parameter (like a system property) set in an additional Gradle test task (for cglib). However, maybe we don't need it or maybe we may even want to treat cglib support as the 2nd class citizen (aka "deprecated mode")?

As long as we support it we need to test it. As for how, I think it should be possible with gradle to reuse the test sources and have two test projects with different dependency sets. Or maybe even in the same project using two test configurations (I need to refresh my gradle-fu).

} else if (cglibAvailable) {
proxy = CglibMockFactory.createMock(mockType, additionalInterfaces,
constructorArgs, mockInterceptor, classLoader, useObjenesis);
} else {
throw new CannotCreateMockException(mockType,
". Mocking of non-interface types requires the CGLIB library. Please put cglib-nodep-2.2 or higher on the class path."
". Mocking of non-interface types requires the CGLIB library. Please put Byte Buddy (at least 1.4.0) or cglib-nodep-2.2 or higher on the class path."
Copy link
Member

Choose a reason for hiding this comment

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

Not really your issue but if you are already touching it. The message should say that we need at least version 3.2 of cglib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed it.

@raphw
Copy link
Contributor Author

raphw commented Aug 19, 2016

As an additional incentive to merge this: Byte Buddy supports Java 9 by now, cglib does not (yet?). ;)

@leonard84
Copy link
Member

Yes, as soon as the testing issue is resolved. We need tests to run against both cglib and ByteBuddy.

@raphw
Copy link
Contributor Author

raphw commented Aug 23, 2016

I added a property -Dorg.spockframework.mock.ignoreByteBuddy=true which can be set to use cglib if both Byte Buddy and cglib are available. This can also be used by end-users if required. Alternatively, it would be possible to simply rerun the tests without Byte Buddy on the class path.

I am not a Gradle person. Were you waiting for me to add support for testing both? I would need to read up my Gradle 101 to do this.

@szpak
Copy link
Member

szpak commented Aug 23, 2016

Learning Gradle could be beneficial for you, @raphw ;)

However, if it's not the first point on your TODO list I would be probably able to get it a try once finishing my holiday season (probably somewhere at the edge of September and October ;) ). Luckily, someone else will be able to handle it earlier.

@raphw
Copy link
Contributor Author

raphw commented Sep 1, 2016

@szpak @leonard84 I recently had the pleasure of writing a Byte Buddy plugin for Gradle and got to know the build system a bit better. I went for a simple solution where running Gradle with:

gradle test -DuseCglib=true

runs all tests with cglib rather then Byte Buddy. From the comments in your .travis.yml, it does however seem like you do not want additional builds.

Furthermore, AppVeyor does not seem to even attempt building the project. Neither do I understand why Travis does no longer build the branch.

@leonard84
Copy link
Member

@raphw my approach would be to define another test task, e.g., testCglib which is then added to the ci-builds in the main gradle file.

@raphw
Copy link
Contributor Author

raphw commented Sep 1, 2016

@leonard84 Would you know how to do that? I googled a bit, but there seems no easy way of duplicating a task in Gradle.

@leonard84
Copy link
Member

If you look at https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/ you can see how to define another test task. In your case I think it would suffice to just set the SystemProperty and maybe use another output directory for the reports.

@raphw
Copy link
Contributor Author

raphw commented Sep 6, 2016

@leonard84 Thanks for the link. I followed the recommendation and have a testCglib task integrated in the build.

@szpak
Copy link
Member

szpak commented Sep 6, 2016

Thanks @raphw!

LGTM

@leonard84
Copy link
Member

thanks @raphw you did all what is neccessary as far as I can see, the reason why we haven't merged yet is that it now doubles our test-time which nearly double the total build time.
Maybe we can tweak the testCglib task a bit to only include tests which really use class-mocks and only run on CI-builds by default.

@raphw
Copy link
Contributor Author

raphw commented Sep 9, 2016

It would be possible to filter out some tests but then again, code generation is a very complex process that is hard to test in isolation. Maybe, the tests should only run on CI but not on a standard build?

@raphw
Copy link
Contributor Author

raphw commented Sep 15, 2016

I changed the build now to only attach the testCglib task to the check task if it is run on a CI server. Otherwise, it is still possible to run the task manually on a local machine, if required.

Would this suffice?

@robfletcher
Copy link
Contributor

I'm for merging this… any objections?

@raphw
Copy link
Contributor Author

raphw commented Sep 30, 2016

Ping @leonard84 @szpak - What is the state on this?

@szpak
Copy link
Member

szpak commented Sep 30, 2016

I've already approved your changes in a review so I cannot be against :)

@robfletcher robfletcher merged commit d96f3ac into spockframework:master Sep 30, 2016
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.

None yet

4 participants