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

Not working with classes with lambdas (retrolambda) #2364

Closed
SammyVimes opened this issue Mar 16, 2016 · 25 comments
Closed

Not working with classes with lambdas (retrolambda) #2364

SammyVimes opened this issue Mar 16, 2016 · 25 comments

Comments

@SammyVimes
Copy link

When you try to shadow class, which has lambdas inside, its creationfails with
NoClassDefFoundError with cause:
java.lang.ClassCastException: org.objectweb.asm.tree.InvokeDynamicInsnNode cannot be cast to org.objectweb.asm.tree.MethodInsnNode at org.robolectric.internal.bytecode.InstrumentingClassLoader$ClassInstrumentor.filterSpecialMethods(InstrumentingClassLoader.java:683)

@karlicoss
Copy link
Contributor

Just curious: how is it possible to have 'real' (not the ones which went through retrolambda) lambdas in an Android class? What are you trying to shadow?

@SammyVimes
Copy link
Author

I forgot to mention that I use retrolambda. Seems like classes got instrumented by robolectric before they are processed by retrolambda.

@SammyVimes SammyVimes changed the title Not working with java8 lambdas Not working with classes with lambdas (retrolambda) Mar 18, 2016
@jaredsburrows
Copy link
Contributor

I use JDK8 + Retrolambda just fine. Please show some example code.

@SammyVimes
Copy link
Author

@jaredsburrows
@karlicoss

class Taskhandler {
    [...]
    public void onHandleOperationResult(final Object result,
                                            final String error,
                                            final OperationRunner operationRunner,
                                            final Operation operation) {
            mainHandler.post(() -> {
                [....]
            });
}
@Implements(TaskHandler.class)
public class TaskHandlerShadow {
    [...]
    @Implementation
    public void onHandleOperationResult(final Object result,
                                        final String error,
                                        final OperationRunner operationRunner,
                                        final Operation operation) { 
        [...]
    }
}

And then constructor new TaskHandler() is called I got

java.lang.NoClassDefFoundError ....TaskHandler
...
Caused by: java.lang.ClassNotFoundException: couldn't load ....TaskHandler
...
Caused by: java.lang.ClassCastException: org.objectweb.asm.tree.InvokeDynamicInsnNode cannot be cast to org.objectweb.asm.tree.MethodInsnNode

I suppose it's a bug in InstrumentingClassLoader, because in case of INVOKEDYNAMIC branch, MethodInsnNode is used instead of InvokeDynamicInsnNode. In asm's Opcodes interface there is a comment for INVOKEDYNAMIC -- //visitInvokeDynamicInsn.

@jaredsburrows
Copy link
Contributor

Usually when people use Retrolambda, they use it with RxJava. Why are you creating custom handlers? Maybe your Shadow is wrong? Shadows should be last resort.

@SammyVimes
Copy link
Author

@jaredsburrows
Usually there should not be ClassCastExceptions, no matter what library is used or not used :D. It certainly is a bug.
About RxJava: I simply don't have enough experience in it for production, so I wrote some custom classes, which are easy as a brick.

@jongerrish
Copy link
Contributor

Do you really need to shadow your own classes?

I recommend using a mocking library instead for that.

I do think you're right though, this could be a bug related to the use of
intrinsics that were added recently. I believe you can switch this off with
a system property though.
On Mar 21, 2016 10:20 AM, "Semyon Danilov" notifications@github.com wrote:

@jaredsburrows https://github.com/jaredsburrows
Usually there should not be ClassCastExceptions, no matter what library is
used or not used :D. It certainly is a bug.
About RxJava: I simply don't have enough experience in it to use it in
production, so I wrote some custom classes, which are easy as a brick.


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#2364 (comment)

@SammyVimes
Copy link
Author

@jongerrish
I am not sure if I should shadow them. I considered using PowerMock's supress, but it seems to be Shadow's analogue.
This class's (that I shadow) object, is instantiating deep inside in app's logic. I actually think of something like factory injecting, but it's sure is not so easy to implement in Android (comparing with Spring etc).

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

I've just encountered the same issue, trying to shadow a class, which uses retrolambdas.
So I am getting
java.lang.NoClassDefFoundError
caused by java.lang.ClassNotFoundException
caused by java.lang.ClassCastException: org.objectweb.asm.tree.InvokeDynamicInsnNode cannot be cast to org.objectweb.asm.tree.MethodInsnNode.
It happens only when I try to run tests from Android Studio; test are passed when I run them from console.
Do you have any ideas to fix it without removing shadow classes?

@jaredsburrows
Copy link
Contributor

jaredsburrows commented Jul 28, 2016

@budnyjj Can you show an example test? I use retrolambda in my apps just fine.

@SammyVimes Any updates?

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

Engine.java:

public class Engine {
    public static final Callback EMPTY_CALLBACK = args -> {
    };
}

EngineShadow.java:

@Implements(Engine.class)
public class EngineShadow {

}

CustomShadowTestRunner.java:

public class CustomShadowTestRunner extends RobolectricGradleTestRunner {

    public CustomShadowTestRunner(Class<?> testClass) throws InitializationError {
        super(testClass);
    }

    @Override
    public InstrumentationConfiguration createClassLoaderConfig() {
        return InstrumentationConfiguration.newBuilder()
                .addInstrumentedClass(Engine.class.getName())
                .build();
    }

I'm running my tests on Linux as well as on Mac using robolectric 3.0 with retrolambda plugin 3.2.5.

@jaredsburrows
Copy link
Contributor

So this should work normally. Are you only seeing this error when using shadows + retrolambda?

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

And the test is:

@Config(shadows = {EngineShadow.class})
@RunWith(CustomShadowTestRunner.class)
public class JSEngineTest {
    // TODO: add unit tests
    @Test
    public void testBasic() {
        Assert.assertTrue(true);
    }
}

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

Yes, each time I replace

public static final Callback EMPTY_CALLBACK = args -> {
    };

with

    public static final Callback EMPTY_CALLBACK = new Callback() {
        @Override
        public void invoke(String[] args) {

        }
    }

it passes.

@jaredsburrows
Copy link
Contributor

@SammyVimes @budnyjj This seems to work fine for me. I am using Robolectric 3.1.1.

gradle clean testDebug --tests="*JSEngineTest*"

CustomShadowTestRunner.java

import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;

public class CustomShadowTestRunner extends RobolectricTestRunner {

    public CustomShadowTestRunner(Class<?> testClass) throws InitializationError {
        super(testClass);
    }

    @Override
    public InstrumentationConfiguration createClassLoaderConfig(Config config) {
        super.createClassLoaderConfig(config);

        return InstrumentationConfiguration.newBuilder()
                .addInstrumentedClass(Engine.class.getName())
                .build();
    }
}

Engine.java

public class Engine {

    interface Callback {
        void invoke(String[] args);
    }

    public static final Callback EMPTY_CALLBACK = args -> {
    };

}

EngineShadow.java

import org.robolectric.annotation.Implements;

@Implements(Engine.class)
public class EngineShadow {

}

JSEngineTest.java

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

@Config(shadows = {EngineShadow.class})
@RunWith(CustomShadowTestRunner.class)
public class JSEngineTest {

    @Test
    public void testBasic() {
        Assert.assertTrue(true);
    }
}

@jaredsburrows
Copy link
Contributor

@SammyVimes Can we close this? @jongerrish @xian

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

Sorry, my mistake. Here is failing test body:

@Config(constants = BuildConfig.class, shadows = {EngineShadow.class}, sdk = 21)
@RunWith(CustomShadowTestRunner.class)
public class ExampleUnitTest {
    @Test
    public void testBasic() {
        Engine.EMPTY_CALLBACK.invoke(new String[]{"abc"});
        assertTrue(true);
    }
}

Error message:

java.lang.NoClassDefFoundError: budnyjj/robolambda/Engine
    at budnyjj.robolambda.ExampleUnitTest.testBasic(ExampleUnitTest.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
    at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
    at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Caused by: java.lang.ClassNotFoundException: couldn't load budnyjj.robolambda.Engine
    at org.robolectric.internal.bytecode.InstrumentingClassLoader.findClass(InstrumentingClassLoader.java:155)
    at org.robolectric.internal.bytecode.InstrumentingClassLoader.loadClass(InstrumentingClassLoader.java:95)
    ... 28 more
Caused by: java.lang.ClassCastException: org.objectweb.asm.tree.InvokeDynamicInsnNode cannot be cast to org.objectweb.asm.tree.MethodInsnNode
    at org.robolectric.internal.bytecode.InstrumentingClassLoader$ClassInstrumentor.filterNasties(InstrumentingClassLoader.java:626)
    at org.robolectric.internal.bytecode.InstrumentingClassLoader$ClassInstrumentor.instrument(InstrumentingClassLoader.java:386)
    at org.robolectric.internal.bytecode.InstrumentingClassLoader.getInstrumentedBytes(InstrumentingClassLoader.java:243)
    at org.robolectric.internal.bytecode.InstrumentingClassLoader.findClass(InstrumentingClassLoader.java:148)
    ... 29 more

@jaredsburrows
Copy link
Contributor

@budnyjj Now that fails for me. This is just hard for Robolectric to handle since Retrolambda takes Java8 byte code and turn it into Java7 byte code.

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

What are the options to do in this situation?
It turns that Robolectric and Retrolambda are not compatible with each other in general, right?

@jaredsburrows
Copy link
Contributor

jaredsburrows commented Jul 28, 2016

I guess. What is the use that the class you are using needs a shadow? or maybe for now, do not shadow classes with java 8 code or just switch those lambdas to a java 7 implementation.

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

This shadow class should mock the original one, which has a number of native methods.

@jaredsburrows
Copy link
Contributor

You can just try mocking it for now or use a java 7 implementation instead of lambdas for the class that you are shadowing.

@budnyjj
Copy link

budnyjj commented Jul 28, 2016

Yes, I'll try to mock them.
Thank you!

@prattpratt
Copy link

prattpratt commented Jul 29, 2016

I have the same issue. But I'm using plain IDEA for development rather than AS. And I noticed that if I use JUnit Runner (IDE internals configured in Settings) then such test really fails but if I choose Gradle Runner - such tests passes. So my conclusion is that when IDE runs tests using JUnit Runner then tests get Java 8 code with real lambdas rather than retrolambdas. And seems that roboelectric does not support Java 8. I looked in the source code where stacktrace points to. And there's no special handling for invokedynamic opcode. It's the same as for all other invokeXXX methods.

@jongerrish
Copy link
Contributor

Closing this as it hasn't been updated in a while. If its still an issue with Robolectric 4.0 please reopen with a reproducible test case and we'll prioritize.

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

No branches or pull requests

6 participants