Fixed
Status Update
Comments
su...@twofortyfouram.com <su...@twofortyfouram.com> #2
This is a threading bug in Espresso's ActivityTestRule.
Looking at the source code, we can see that ActivityTestRule.finishActivity() calls mActivity.finish() from the Instrumentation thread. This isn't correct. The fix would be to update ActivityTestRule to call finish on the UI thread. This is the method in question:
/**
* Finishes the currently launched Activity.
*
* @throws IllegalStateException if the Activity is not running.
*/
public void finishActivity() {
checkState(
mActivity != null,
"Activity was not launched. If you manually finished it, you must launch it"
+ " again before finishing it.");
mActivity.finish(); // <- PROBLEM HERE
afterActivityFinished();
mActivity = null;
mInstrumentation.waitForIdleSync();
}
There is a second problem in the same class with ActivityTestRule.getActivityResult(). Activity state is not synchronized. The problems are:
- Call to `checkState(activity.isFinishing(), "Activity is not finishing!");` is not thread safe because activity.isFinishing() shouldn't be called from the Instrumentation thread.
- mResultCode and mResultData are inconsistently synchronized. The fields in Activity are synchronized by `this`, but they are not synchronized with `mActivity` inside the ActivityTestRule.getActivityResult() method.
public ActivityResult getActivityResult() {
T activity = mActivity;
checkState(activity.isFinishing(), "Activity is not finishing!"); // <- PROBLEM HERE
try {
Field resultCodeField = Activity.class.getDeclaredField(FIELD_RESULT_CODE);
resultCodeField.setAccessible(true);
Field resultDataField = Activity.class.getDeclaredField(FIELD_RESULT_DATA);
resultDataField.setAccessible(true);
return new ActivityResult(
(int) resultCodeField.get(activity), (Intent) resultDataField.get(activity)); // <- PROBLEM HERE
} catch (NoSuchFieldException e) {
throw new RuntimeException(
"Looks like the Android Activity class has changed its"
+ "private fields for mResultCode or mResultData. "
+ "Time to update the reflection code.",
e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Field mResultCode or mResultData is not accessible", e);
}
}
There is a third problem with the API of these two methods: they cannot be used together. Calling `finishActivity` nulls out the mActivity reference, meaning that `getActivityResult` cannot be called afterwards. This means a user of these methods must call finish() on the Activity manually, which is error prone given the threading issues already identified here in Google's own code. One possible option: consider making finishActivity() non-void and return the Activity result.
Looking at the source code, we can see that ActivityTestRule.finishActivity() calls mActivity.finish() from the Instrumentation thread. This isn't correct. The fix would be to update ActivityTestRule to call finish on the UI thread. This is the method in question:
/**
* Finishes the currently launched Activity.
*
* @throws IllegalStateException if the Activity is not running.
*/
public void finishActivity() {
checkState(
mActivity != null,
"Activity was not launched. If you manually finished it, you must launch it"
+ " again before finishing it.");
mActivity.finish(); // <- PROBLEM HERE
afterActivityFinished();
mActivity = null;
mInstrumentation.waitForIdleSync();
}
There is a second problem in the same class with ActivityTestRule.getActivityResult(). Activity state is not synchronized. The problems are:
- Call to `checkState(activity.isFinishing(), "Activity is not finishing!");` is not thread safe because activity.isFinishing() shouldn't be called from the Instrumentation thread.
- mResultCode and mResultData are inconsistently synchronized. The fields in Activity are synchronized by `this`, but they are not synchronized with `mActivity` inside the ActivityTestRule.getActivityResult() method.
public ActivityResult getActivityResult() {
T activity = mActivity;
checkState(activity.isFinishing(), "Activity is not finishing!"); // <- PROBLEM HERE
try {
Field resultCodeField = Activity.class.getDeclaredField(FIELD_RESULT_CODE);
resultCodeField.setAccessible(true);
Field resultDataField = Activity.class.getDeclaredField(FIELD_RESULT_DATA);
resultDataField.setAccessible(true);
return new ActivityResult(
(int) resultCodeField.get(activity), (Intent) resultDataField.get(activity)); // <- PROBLEM HERE
} catch (NoSuchFieldException e) {
throw new RuntimeException(
"Looks like the Android Activity class has changed its"
+ "private fields for mResultCode or mResultData. "
+ "Time to update the reflection code.",
e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Field mResultCode or mResultData is not accessible", e);
}
}
There is a third problem with the API of these two methods: they cannot be used together. Calling `finishActivity` nulls out the mActivity reference, meaning that `getActivityResult` cannot be called afterwards. This means a user of these methods must call finish() on the Activity manually, which is error prone given the threading issues already identified here in Google's own code. One possible option: consider making finishActivity() non-void and return the Activity result.
sl...@google.com <sl...@google.com>
nk...@google.com <nk...@google.com> #3
This will be fixed in the next release v1.0.2.
Description
Version used: 0.3
What steps will reproduce the problem?
1.Override method finish() of Activity, do some UI work in customized finish()
2.Use ActivityTestRule to launch the Activity and do some tests
How are you running your tests (via Android Studio, Gradle, adb, etc.)? Android Studio and Gradle, same result
What is the expected output?
The test case can work normally.
What do you see instead?
An exception happens, AndroidJunitRunner tried to call finish() of the Activity from non-UI thread, which caused the crash.
stack trace is
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6247)
at android.view.ViewRootImpl.playSoundEffect(ViewRootImpl.java:5222)
at android.view.View.playSoundEffect(View.java:17876)
at android.view.View.performClick(View.java:4755)
at com.example.android.activityinstrumentation.ReadExternalFileActivity.finish(ReadExternalFileActivity.java:196)
at android.support.test.rule.ActivityTestRule.finishActivity(ActivityTestRule.java:234)
at android.support.test.rule.ActivityTestRule$ActivityStatement.evaluate(ActivityTestRule.java:259)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
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.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
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.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:59)
at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:262)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1837)