robolectric / robolectric

Android Unit Testing Framework
http://robolectric.org
Other
5.81k stars 1.36k forks source link

Unable to inflate ProgressBar when running tests on Linux with GraphicsMode NATIVE #8478

Closed dimrsilva closed 2 weeks ago

dimrsilva commented 8 months ago

Description

The ProgressBar is failing to be inflated when running tests on Linux. The same tests run correctly on Mac OS.

The tests fails with IllegalArgumentException exception when trying to load progress_medium_material drawable.

Caused by: android.content.res.Resources$NotFoundException: Drawable android:drawable/progress_medium_material with resource ID #0x1080732
Caused by: java.lang.IllegalArgumentException: The Path must start at (0,0) and end at (1,1)

Steps to Reproduce

Just create a simple activity with a ProgressBar and GraphicsMode NATIVE

@GraphicsMode(GraphicsMode.Mode.NATIVE)
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @Test
    fun launchScreen() {
        launchActivity<MainActivity>()

        Espresso.onView(withId(R.id.progressBar)).check(matches(isDisplayed()))
    }
}

Robolectric & Android Version

Robolectric: 4.10.3 Linux Kernel: 5.15.12-051512-generic MacOS (works correctly): Ventura 13.2.1

Link to a public git repo demonstrating the problem:

https://github.com/lgmro/Roboeletric_linux_issue

realdadfish commented 6 months ago

I'm experiencing the same issue on Mac OS Sonoma 14.1.1 and Robolectric 4.11.1.

realdadfish commented 1 month ago

Issue persists with Robolectric 4.12.1

realdadfish commented 1 month ago

Interestingly, this test only fails when executed on command line, when executed in the IDE things work for me. This is unrelated whether all tests or just a single test (via --tests on command line) are executed.

This is a more complete stack trace btw.

android.view.InflateException: Binary XML file line #14 in my.app:layout/dialog_loading_with_msg: Binary XML file line #14 in my.app:layout/dialog_loading_with_msg: Error inflating class android.widget.ProgressBar
    Suppressed: org.robolectric.android.internal.AndroidTestEnvironment$UnExecutedRunnablesException: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(Looper.getMainLooper()).idle() call.
Caused by: android.view.InflateException: Binary XML file line #14 in my.app:layout/dialog_loading_with_msg: Error inflating class android.widget.ProgressBar
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at android.view.LayoutInflater.$$robo$$android_view_LayoutInflater$createView(LayoutInflater.java:858)
    at android.view.LayoutInflater.createView(LayoutInflater.java)
...
Caused by: android.content.res.Resources$NotFoundException: Drawable android:drawable/progress_medium_material with resource ID #0x1080725
Caused by: java.lang.IllegalArgumentException: The Path must start at (0,0) and end at (1,1)
    at android.view.animation.PathInterpolator.initPath(PathInterpolator.java:168)
    at android.view.animation.PathInterpolator.parseInterpolatorFromTypeArray(PathInterpolator.java:119)
    at android.view.animation.PathInterpolator.__constructor__(PathInterpolator.java:104)
    at android.view.animation.PathInterpolator.<init>(PathInterpolator.java:97)
    at android.view.animation.AnimationUtils.createInterpolatorFromXml(AnimationUtils.java:429)
    at android.view.animation.AnimationUtils.loadInterpolator(AnimationUtils.java:372)
    at android.animation.AnimatorInflater.loadAnimator(AnimatorInflater.java:1057)
    at android.animation.AnimatorInflater.loadObjectAnimator(AnimatorInflater.java:1011)
    at android.animation.AnimatorInflater.createAnimatorFromXml(AnimatorInflater.java:667)
    at android.animation.AnimatorInflater.createAnimatorFromXml(AnimatorInflater.java:680)
    at android.animation.AnimatorInflater.createAnimatorFromXml(AnimatorInflater.java:642)
    at android.animation.AnimatorInflater.loadAnimator(AnimatorInflater.java:126)
    at android.graphics.drawable.AnimatedVectorDrawable$AnimatedVectorDrawableState$PendingAnimator.newInstance(AnimatedVectorDrawable.java:888)
    at android.graphics.drawable.AnimatedVectorDrawable$AnimatedVectorDrawableState.inflatePendingAnimators(AnimatedVectorDrawable.java:864)
    at android.graphics.drawable.AnimatedVectorDrawable.applyTheme(AnimatedVectorDrawable.java:683)
    at android.graphics.drawable.DrawableContainer$DrawableContainerState.applyTheme(DrawableContainer.java:983)
    at android.graphics.drawable.DrawableContainer.applyTheme(DrawableContainer.java:630)
    at com.android.internal.graphics.drawable.AnimationScaleListDrawable.applyTheme(AnimationScaleListDrawable.java:240)
    at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:683)
    at jdk.internal.reflect.GeneratedMethodAccessor11.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.robolectric.shadows.ShadowArscResourcesImpl$ResourcesImplReflector$$Reflector25.loadDrawable(Unknown Source)
    at org.robolectric.shadows.ShadowArscResourcesImpl.loadDrawable(ShadowArscResourcesImpl.java:169)
    at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java)
    at android.content.res.Resources.loadDrawable(Resources.java:993)
    at android.content.res.TypedArray.getDrawableForDensity(TypedArray.java:1007)
    at android.content.res.TypedArray.getDrawable(TypedArray.java:982)
    at android.widget.ProgressBar.__constructor__(ProgressBar.java:322)
    at android.widget.ProgressBar.<init>(ProgressBar.java:274)
    at android.widget.ProgressBar.<init>(ProgressBar.java:270)
    at android.widget.ProgressBar.<init>(ProgressBar.java:266)
realdadfish commented 1 month ago

Issue seems to be the last path component, it's (1.1, 1) instead of (1,1).

realdadfish commented 1 month ago

I'd say the path.approximate(PRECISION) returns (implementation-wise) two completely different values. The input path string is always C0.2,0 0.1,1 0.5, 1 L 1,1, so when this Path is read via XML property, this results in the following (errornous) point list (22 points in total, all with x,y,z coordinates):

[0.0, 0.0, 0.0, 0.001366025, 0.0, 0.0028686523, 0.0053478424, 0.0, 0.011230469, 0.011771066, 0.0, 0.024719238, 0.02046131, 0.0, 0.04296875, 0.031244189, 0.0, 0.06561279, 0.043945316, 0.0, 0.092285156, 0.07440477, 0.0, 0.15625, 0.110444576, 0.0, 0.2319336, 0.15066965, 0.0, 0.31640625, 0.23809525, 0.0, 0.5, 0.32552084, 0.0, 0.68359375, 0.36574593, 0.0, 0.7680664, 0.40178573, 0.0, 0.84375, 0.4322452, 0.0, 0.90771484, 0.44494632, 0.0, 0.9343872, 0.4557292, 0.0, 0.95703125, 0.46441942, 0.0, 0.97528076, 0.47084266, 0.0, 0.98876953, 0.47482446, 0.0, 0.99713135, 0.4761905, 0.0, 1.0, 1.0, 1.1, 1.0]

but if one manually parses the string by the same means, i.e.

val path = PathParser.createPathFromPathData("C0.2,0 0.1,1 0.5, 1 L 1,1")
PathInterpolator(path)

one receives the following valid point list (23 points in total):

[0.0, 0.0, 0.0, 0.0106859375, 0.017895509, 0.0028686523, 0.021478953, 0.034179688, 0.011230469, 0.033293966, 0.048999026, 0.024719238, 0.046678346, 0.0625, 0.04296875, 0.06188002, 0.0748291, 0.06561279, 0.07896006, 0.08613282, 0.092285156, 0.1184951, 0.10625, 0.15625, 0.16433229, 0.12402344, 0.2319336, 0.2150903, 0.140625, 0.31640625, 0.32521868, 0.17500001, 0.5, 0.3814707, 0.19511719, 0.5932617, 0.43652323, 0.21875, 0.68359375, 0.48905304, 0.24707031, 0.7680664, 0.5380158, 0.28125, 0.84375, 0.58287925, 0.32246095, 0.90771484, 0.6038808, 0.34606934, 0.9343872, 0.624123, 0.371875, 0.95703125, 0.6439027, 0.4000244, 0.97528076, 0.66364104, 0.43066406, 0.98876953, 0.68387085, 0.46394044, 0.99713135, 0.7051988, 0.5, 1.0, 1.0, 1.0, 1.0]

This is even in the same (debugged) test run started via Gradle. I have no idea what's going on here.

hoisie commented 1 month ago

Thanks for the debugging @realdadfish . I wonder if this could be related to reading the path values in binary resources?

hoisie commented 1 month ago

I cloned the repo https://github.com/lgmro/Roboeletric_linux_issue and ran gradlew test but everything passed. I am running Linux 6.6.13-1rodete3-amd64.

hoisie commented 1 month ago

Looks similar to this issue in LayoutLib (which uses the same underlying native libraries): https://stackoverflow.com/questions/67202559/progressbar-in-android-studio-the-path-must-start-at-0-0-and-end-at-1-1

realdadfish commented 1 month ago

Thanks for the debugging @realdadfish . I wonder if this could be related to reading the path values in binary resources?

That is what bugs me - I got the original string (C0.2,0 0.1,1 0.5, 1 L 1,1) by debugging through the code, this is the value I copied from the debugger after it was read from the XML file. And that same value des not trigger the issue when I run the path parsing code manually.

realdadfish commented 1 month ago

I have no idea of the inner workings of the native layoutlib code, eventually you could contact the team internally and ask for help?

hoisie commented 1 month ago

@realdadfish yeah we have debugged and fixed these types of issues before. The native code for Path.approximate is here: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android12-hostruntime-dev/libs/hwui/jni/Path.cpp#408. Are you 100% sure the path string is always C0.2,0 0.1,1 0.5, 1 L 1,1?

hoisie commented 1 month ago

@realdadfish is this issue 100% reproducible for you? Can it be reproduced in a place like GitHub CI?

hoisie commented 1 month ago

@jgaillard85 from LayoutLib provided a great insight that may be the root cause of this issue. In LayoutLib there is a snippet of code:

// Use English locale for number format to ensure correct parsing of floats when using strtof
setlocale(LC_NUMERIC, "en_US.UTF-8");

If the LC_NUMERIC locale is set to e.g. German, perhaps the comma is interpreted as part of a float.

https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/jni/LayoutlibLoader.cpp;l=449-450

It looks like PathParser does use strtof: https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/PathParser.cpp;l=103

We should probably set the LC_NUMERIC locale to en_US.UTF-8 in Robolectric native graphics.

hoisie commented 1 month ago

Looks like this was the issue indeed:

https://github.com/robolectric/robolectric/actions/runs/8652528011/job/23726339628#step:7:4667

That tests fails when Linux is set to a German locale.

I can make a point release that fixes this issue.

realdadfish commented 1 month ago

Wow, awesome! Thanks for finding and fixing it! Maybe, just maybe, PathParser should check and output a warning if LC_NUMERIC does not have the expected value? Or, at least, it should document that on the CPP implementation?