cashapp / paparazzi

Render your Android screens without a physical device or emulator
https://cashapp.github.io/paparazzi/
Apache License 2.0
2.31k stars 215 forks source link

UnsatisfiedLinkError when using ValueAnimator #988

Open Thanasis17m opened 1 year ago

Thanasis17m commented 1 year ago

Description We have a project with multiple modules. In some of them, there are viewModel classes, in which we use the android.animation.ValueAnimator class to create and handle a timer.

When we add to one of these modules, the paparazzi dependency, then the unit tests that test any viewModel method using the ValueAnimator class fail.

This UnsatisfiedLinkError is reported:

Screenshot 2023-07-11 at 10 36 08 AM

More specifically, the error is reported in the viewModel, when we call this method ValueAnimator.ofInt(PROGRESS_MIN, PROGRESS_MAX)

We also tried adding the paparazzi dependency to other modules, where we use ValueAnimator similarly in viewModels and the same error was reported in the relevant unit tests.

So, for some reason, when adding paparazzi dependency to a module, then all unit tests that use ValueAnimator in the tested method, fail.

Steps to Reproduce To reproduce this, just follow the above steps.

  1. Add paparazzi dependency in your module.
  2. Create a class with a method calling any ValueAnimator method.
  3. Create a unit test, calling the method and run it.
  4. You should see the UnsatisfiedLinkError

Expected behavior What we expected was, that adding the paparazzi dependency, wouldn't affect the use of the ValueAnimator class.

Additional information:

jrodbx commented 1 year ago

Can you provide a repro/sample?

TWiStErRob commented 1 year ago
package app.cash.paparazzi.sample

import org.junit.Test

class UnsatisfiedLinkErrorTest {
    @Test fun test() {
        println(android.animation.ValueAnimator())
    }
}
alex-tiurin commented 1 year ago

Any updates? We also has

'java.lang.String android.os.SystemProperties.native_get(java.lang.String, java.lang.String)'
java.lang.UnsatisfiedLinkError: 'java.lang.String android.os.SystemProperties.native_get(java.lang.String, java.lang.String)'
    at android.os.SystemProperties.native_get(Native Method)
    at android.os.SystemProperties.native_get(SystemProperties.java:103)

I assume such errors java.lang.UnsatisfiedLinkError could be connected with jdk11 that is used in 1.3.1 - layoutlib-native-jdk11-2022.2.1-5128371-2.jar .

The new version requirs JAVA 17, but contains lib from jdk11

libs.versions.toml

joshfriend commented 1 year ago

@alex-tiurin's error can be reliably reproduced with

class SystemPropertiesTest {
  @Test fun boom() {
    println(Build.ID) // Somehow OK
    println(SystemProperties.get("ro.build.id")) // Not OK
  }
}

Here's the stacktrace I was seeing inside a real project:

java.lang.UnsatisfiedLinkError: 'java.lang.String android.os.SystemProperties.native_get(java.lang.String, java.lang.String)'
at android.os.SystemProperties.native_get(Native Method)
at android.os.SystemProperties.get(SystemProperties.java:165)
at android.os._Original_Build.getString(Build.java:1491)
at android.os._Original_Build.<clinit>(Build.java:56)
at android.view.InputEventConsistencyVerifier.<clinit>(InputEventConsistencyVerifier.java:34)
at android.view.View.<init>(View.java:5255)

Where a unit test case was eventually calling down to View(context) constructor to create an empty view.

You can get the above test case (and the one from @TWiStErRob) to pass by instead using robolectric to run the test with @RunWith(RobolectricTestRunner::class).

geoff-powell commented 1 year ago

So digging into this issue, the following test fails because Paparazzi junit rule is not correctly annotated. Since layoutlib hooks haven't been set up, the following exception occurs.

I have combined your example tests to reproduce and fix this issue.

Failing Test

package app.cash.paparazzi.sample

import android.os.Build
import android.os.SystemProperties
import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SHRINK
import org.junit.Rule
import org.junit.Test

class UnsatisfiedLinkErrorTest {
  @Test fun test() {
    println(Build.ID) // Somehow OK
    println(SystemProperties.get("ro.build.id")) // Not OK
    println(android.animation.ValueAnimator())
  }
}

Passing Test

package app.cash.paparazzi.sample

import android.os.Build
import android.os.SystemProperties
import app.cash.paparazzi.DeviceConfig
import app.cash.paparazzi.Paparazzi
import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SHRINK
import org.junit.Rule
import org.junit.Test

class UnsatisfiedLinkErrorTest {
  @get:Rule
  val paparazzi = Paparazzi(
      deviceConfig = DeviceConfig.PIXEL_3,
      renderingMode = SHRINK,
      showSystemUi = false
    )

  @Test fun test() {
    println(Build.ID) // Somehow OK
    println(SystemProperties.get("ro.build.id")) // Not OK
    println(android.animation.ValueAnimator())
  }
}

The corrective action is to instantiate the Paparazzi class and add the correct junit @Rule annotation to it. @get:Rule val paparazzi = Paparazzi()

TWiStErRob commented 1 year ago

Yes, but I think the test is not meant to be a Paparazzi test. This implies that if a module has Paparazzi, it cannot have any other complex tests.

joshfriend commented 1 year ago

Yep we talked about it on slack a bit and it spawned this agp feature request

jrodbx commented 11 months ago

Researching this a bit, using a modified version of the sample @TWiStErRob provided here: https://github.com/cashapp/paparazzi/issues/988#issuecomment-1632130499

class UnsatisfiedLinkErrorTest {
  @Test fun test() {
    ValueAnimator::class.java.declaredFields.forEach {
      println("declaredField: " + it.name)
    }
    android.animation.ValueAnimator()
  }
}

If I remove all other tests and this line from sample/build.gradle:

apply plugin: 'app.cash.paparazzi'

I get:

declaredField: INFINITE
declaredField: RESTART
declaredField: REVERSE

However, if I reapply the Paparazzi plugin, I get many more fields:

declaredField: TAG
declaredField: DEBUG
declaredField: TRACE_ANIMATION_FRACTION
declaredField: sDurationScale
declaredField: sDurationScaleChangeListeners
....

It's TRACE_ANIMATION_FRACTION which results in the exception referenced in this issue. I understand that LayoutLib would provide a new version of ValueAnimator which provides this field, but what I don't understand is why removing Paparazzi and therefore relying on the platform type (https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/animation/ValueAnimator.java;l=83?q=ValueAnimator) wouldn't still experience this issue...

TWiStErRob commented 11 months ago

@jrodbx I think the answer is that the android.jar that is by default on the classpath is only structurally equivalent to layoutlib/real Android. Therefore it does not contain any clinit code, not private stuff because those are not part of ABI.

geoff-powell commented 1 month ago

Related to #1149

as @TWiStErRob mentions, the issue is Android.jar classes are using Paparazzi's layoutlib implementations which requires some setup to make calls the the clinit code.