Skip to content

Absolute paths in Test tasks's systemProperties cause remote build cache misses #1874

@remcomokveld

Description

@remcomokveld

Description

The Paparazzi plugin configures the test tasks with a bunch of paths and those paths are set up as absolute paths. system properties are part of the cache key so if CI writes remote build cache entries while having the repository checked out in a directory like /tmp/my-repository and local machines have it in ~/Projects/my-repository the cache key of test tasks is different resulting in a cache miss.

Steps to Reproduce

The problem can be reproduced by updating the unit test app.cash.paparazzi.gradle.PaparazziPluginTest.cacheable() to run the second run in a different directory. In that case assertions that the task came from cache fail

  @Test
  fun cacheable() {
    val fixtureRoot = File("src/test/projects/cacheable")
    // Also validate remote cache by running the test in two separate directories that use the same build cache
    val secondFixturesRoot = fixtureRoot.parentFile.resolve("cacheable-2").registerForDeletionOnExit()
    fixtureRoot.copyRecursively(secondFixturesRoot)
    fixtureRoot.resolve("build-cache").registerForDeletionOnExit()

    val firstRun = gradleRunner
      .withArguments("testDebug", "--build-cache", "--stacktrace")
      .runFixture(fixtureRoot) { build() }

    with(firstRun.task(":preparePaparazziDebugResources")) {
      assertThat(this).isNotNull()
      assertThat(this!!.outcome).isNotEqualTo(FROM_CACHE)
    }
    with(firstRun.task(":testDebugUnitTest")) {
      assertThat(this).isNotNull()
      assertThat(this!!.outcome).isNotEqualTo(FROM_CACHE)
    }

    val secondRun = gradleRunner
      .withArguments("testDebug", "--build-cache", "--stacktrace")
      .runFixture(secondFixturesRoot) { build() }

    with(secondRun.task(":preparePaparazziDebugResources")) {
      assertThat(this).isNotNull()
      assertThat(this!!.outcome).isEqualTo(FROM_CACHE)
    }
    with(secondRun.task(":testDebugUnitTest")) {
      assertThat(this).isNotNull()
      assertThat(this!!.outcome).isEqualTo(FROM_CACHE)
    }
  }

Expected behavior

A second run without input changes in a project that is in a different directory should come from cache. The recommended way of setting up remote build cache is to store cache entries on CI so that local dev machines can use them. If there are absolute paths in the cache key those local machines would never be able to use them

Workaround

I was able to work around this by adding the following gradle logic

androidComponents {
    onVariants { variant ->
        variant.hostTests.forEach { (_, hostTest) ->
            hostTest.configureTestTask { testTask ->
                testTask.systemProperties.toMap().forEach { (key, value) ->
                    if (key.toString().startsWith("paparazzi.") && value is String && value.startsWith(projectDir.absolutePath)) {
                        testTask.systemProperty(key, value.replace(projectDir.absolutePath, "."))
                    }
                }
                testTask.systemProperty("paparazzi.artifacts.cache.dir", "./gradle-user-home")
            }
        }
    }
}

and initialize paparazzi as follows

    private val gradleUserHome = File(
        System.getenv("GRADLE_USER_HOME")
            ?: System.getProperty("user.home")?.plus(File.separator)?.plus(".gradle")
            ?: error("Could not determine gradle user home")
    )
    private val paparazzi = Paparazzi(
        environment = detectEnvironment().run {
            copy(
                localResourceDirs = localResourceDirs.map { asCanonicalPath(it) },
                moduleResourceDirs = moduleResourceDirs.map { asCanonicalPath(it) },
                libraryResourceDirs = libraryResourceDirs.map { asCanonicalPath(it) },
                allModuleAssetDirs = allModuleAssetDirs.map { asCanonicalPath(it) },
                libraryAssetDirs = libraryAssetDirs.map { asCanonicalPath(it) },
                appTestDir = File(appTestDir).canonicalPath
            )
        },
    )

    private fun asCanonicalPath(path: String) = when {
        path.startsWith("./gradle-user-home/") -> gradleUserHome.resolve(path.removePrefix("./gradle-user-home/")).canonicalPath
        path.startsWith("./") -> File(path).canonicalPath
        else -> path
    }

Additional information:

  • Paparazzi Version: 1.3.5
  • OS: MacOS
  • Compile SDK: 35
  • Gradle Version: 8.10.2
  • Android Gradle Plugin Version: 8.8.0

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions