0

Reaching out for help since I have been beating my head against a wall on an issue I have never seen before.

I have a standard fragment activity, using espresso for testing. I have a static manager in an associated module library linked to by the app.

The Issue:

When the test runs, it makes a static call into my ServiceManager. When the code is executed in onCreateView, in the stack trace it is calling a different method in the manager, not the one it should. Weirder yet, it is passing arguments that make no sense when tracing. The code is as follows (items simplified to illustrate what is going on).

ServiceManager.java - comes from a project library

public class ServiceManager {
  private static final ServiceManager INSTANCE = new ServiceManager();

  private UserManager userManager;

  public static initForTest(Context context) {
    // some storage initialization here
    INSTANCE.userManager = new UserManagerImpl();
  }

  public static ServiceManager getInstance() {
    return INSTANCE;
  }

  public static UserManager getUserManager() {
    return INSTANCE.userManager;
  }
}

UserManager.java - comes from a project library

public interface UserManager {
  void registerListener(UserListener listener);

  void refreshDataFromSource(String id);
}

UserManagerImpl.java - comes from a project library

public class UserManagerImpl implements UserManager {
  public void registerListener(UserListener listener) {
    // code really does not matter here, but I have a listener manager
    ManagerListeners.register(UserListener.class, this);
  }

  public void refreshDataFromSource(String id) {
    // kicks off an async task of mine
    Log.i(TAG, "initiating refresh for %s", id);

    UserRefreshTask task = new UserRefreshTask(
        new TaskCallbackWithReturn<Consumer>() {
          @Override
          public void done(Consumer consumer) {
            refreshAllListeners(true);
        }

        @Override
        public void fail() {
          refreshAllListeners(false);
        }
      }, id);

      task.execute();
  }
}

NewUserStartFragment.java - in the app project

public class UserProfileStartFragment extends Fragment implements UserListener {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                       Bundle savedInstanceState) {
    // Get the view from fragment_newuserstart.xml
    View view = inflater.inflate(R.layout.fragment_newuserstart, container, false);

    // register listeners
     ServiceManager.getInstance().getUserManager().registerListener(this);

    // rest does not matter since we don't get to it.
    return view;
  }
}

The backing (dead simple) activity NewUserActivity.java - from the app project

public class NewUserActivity extends FragmentActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_newuser);

    if (savedInstanceState == null) {
      FragmentUtils.replace(this, R.id.fragment_userscreen, new UserProfileStartFragment());
    }

    Log.i(TAG, "Activity created");
  }
}

And a simple test NewUserStartFragmentTest.java - tests in the app project

@RunWith(AndroidJUnit4.class)
public class UserProfileStartFragmentTest {
  // on activity rule, activity initialization delayed on false
  @Rule
  public ActivityTestRule<NewUserActivity> mActivityRule =
      new ActivityTestRule<>(NewUserActivity.class, true, false);

  private NewUserActivity userActivity;

  @Before
  public void setUp() throws Exception {
    Instrumentation instrumentation
        = InstrumentationRegistry.getInstrumentation();
    Context context = instrumentation.getTargetContext();

    // service manager at this point will be initialized
    ServiceManager.initForTest(context);
  }

  @After
  public void tearDown() throws Exception {
    // ensure activity is dead
    ActivityUtils.finish(userActivity);

    ServiceManager.reset();
  }

  @Test
  public void testInitialScreen() throws Exception {
    userActivity = mActivityRule.launchActivity(new Intent());

    onView(withId(R.id.image_profile_photo)).check(matches(notNullValue()));
    onView(withId(R.id.button_email)).check(matches(notNullValue()));
    onView(withId(R.id.button_existing)).check(matches(notNullValue()));
    onView(withId(R.id.button_cancel)).check(matches(notNullValue()));
  }
}

And now the wierdness

When the test runs, it always reports the following.

java.lang.IllegalArgumentException: consumerId must not be null or empty
at com.google.common.base.Preconditions.checkArgument(Preconditions.java:122)
at com.arryved.android.applibrary.tasks.UserRefreshTask.<init>(UserRefreshTask.java:31)
**at com.arryved.android.applibrary.manager.usermanager.UserManagerImpl.refreshDataFromSource(UserManagerImpl.java:300)
at com.arryved.emptor.fragments.UserProfileStartFragment.onCreateView(UserProfileStartFragment.java:41)**
at android.support.v4.app.Fragment.performCreateView(Fragment.java:1965)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1078)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1259)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:738)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1624)
at android.support.v4.app.FragmentController.execPendingActions(FragmentController.java:330)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:547)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1220)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnStart(MonitoringInstrumentation.java:546)
at android.app.Activity.performStart(Activity.java:5953)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2261)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
at android.app.ActivityThread.access$800(ActivityThread.java:144)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

UM, IT SHOULD NOT BE EXECUTING refreshDataFromSource. IT SHOULD BE EXECUTING registerListener.

So, I then decided to add a stop point and debug. what is stranger, it is executing the code, but not passing a string, rather passing the fragment as the argument. Java variable typing alone should cause this to fail! This tells me it has to be a byte code mix up. Is it dex? Is it something else? I have never seen anything like this before.

I have tried on a MacOs android studio and a Linux android studio. I have done build cleans both from the command line and from studio. I have run against several different emulators and an actual Nexus 5 device. Does not matter, same results which tells me it is confused after build and not because of code.

Oh, and another point of reference. Just running the app the fragment works as expected. This seems to be occurring when I just run the test. (But I worry there is something else sinister going on in dex so I don't trust the app unless I have this figured out in tests).

Here is my build.gradle for the app.

apply plugin: 'com.android.application'

apply plugin: 'com.google.gms.google-services'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.1"

  defaultConfig {
      applicationId "com.arryved.emptor"
      minSdkVersion 19
      targetSdkVersion 23
      versionCode 1
      versionName "1.0"
      testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

      // Enabling multidex support.
      multiDexEnabled true
    }
    buildTypes {
      release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  packagingOptions {
      exclude 'META-INF/LICENSE.txt'
      exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
      exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
  }

  dexOptions {
      javaMaxHeapSize "4g"
  }
}

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])

  compile 'com.android.support:appcompat-v7:23.1.0'
  compile 'com.android.support:recyclerview-v7:23.1.0'

  androidTestCompile 'com.android.support.test:runner:0.4'
  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') {
    exclude group: 'com.google.guava', module: 'guava'
  }
  // add this for intent mocking support
  androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2') {
    exclude group: 'com.google.guava', module: 'guava'
  }
  // add this for webview testing support
  androidTestCompile('com.android.support.test.espresso:espresso-web:2.2.1') {
    exclude group: 'com.google.guava', module: 'guava'
  }

  compile project(':AppLibrary')
}

And for my AppLibrary build.gradle

apply plugin: 'com.android.library'

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    classpath 'com.android.tools.build:gradle:1.5.0'
  }
}

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.1"

  defaultConfig {
    minSdkVersion 19
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    // Enabling multidex support.
    multiDexEnabled true
  }
  buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  packagingOptions {
      exclude 'META-INF/LICENSE.txt'
  }

  dexOptions {
      javaMaxHeapSize "4g"
  }
}

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.android.support:multidex:1.0.0'
  compile("com.google.android.gms:play-services:8.3.0") {
    exclude group: 'com.android.support', module: 'support-v4'
  }

  compile 'com.android.support:appcompat-v7:23.1.0'
  compile 'com.android.support:recyclerview-v7:23.1.0'

  androidTestCompile 'com.android.support.test:runner:0.4'
  androidTestCompile 'com.android.support.test:rules:0.4'
}

--------------UPDATE---------------

Just to see what happens, I updated the service manager to deal in the hard implementation (UserManagerImpl) not the interface (UserManager). Tests pass when using the implementation not the interface reference. So, at this point the best I can ascertain is that during the linking cycle, android or dex is getting confused about the interface. And when I change it back, the test is back to failing as shown above (thus not a random build issue either).

David
  • 31
  • 2
  • you're checking code but no imports, you have a mistake in `import` expressions – piotrek1543 Dec 25 '15 at 15:40
  • I intentionally left out the imports since they were of no consequence. – David Dec 26 '15 at 16:24
  • A quick update ... The issue actually boiled down to multidex and API v19 vs v21. The VM got confused when indexing methods and somehow reused numbers, and is only present in v19. As soon as I changed to v21 the issue went away. So I have since changed my build.gradle with a "productFlavors" section to do most emulator testing against v21 and do manual testing against v19. I never would have though this issue would have manifested this way. – David Dec 26 '15 at 16:26
  • congrats, please add SOLUTION section in your post by editing it – piotrek1543 Dec 27 '15 at 13:31

0 Answers0