Visitar URL original
testAndroidHostTest fails under Gradle 9 with Cannot access input property '$1' (NoSuchFileException) on screenshot input directory · Issue #830 · takahirom/roborazzi · GitHub
Skip to content

testAndroidHostTest fails under Gradle 9 with Cannot access input property '$1' (NoSuchFileException) on screenshot input directory #830

Closed

Description

@boiler23

Summary

When running :<module>:testAndroidHostTest on a Roborazzi-enabled KMP module under Gradle 9.4.1, the task intermittently fails before any test starts with:

What went wrong:
Execution failed for task ':<module>:testAndroidHostTest'.
Cannot access input property '$1' of task ':<module>:testAndroidHostTest'.
Accessing unreadable inputs or outputs is not supported.
Declare the task as untracked by using Task.doNotTrackState().
java.nio.file.NoSuchFileException:

The file mentioned by NoSuchFileException exists on disk both before and after the failed run.

Root cause (probably)

RoborazziPlugin.kt registers the screenshots directory as a test-task input withoutwithPropertyName(...):

test.inputs.files(
  imageInputProvider
).withPathSensitivity(PathSensitivity.RELATIVE)

Two consequences (according to Claude's analysis):

  1. Gradle assigns the synthetic property name $1, which makes the error message opaque.
  2. imageInputProvider resolves to outputDir.files("."), i.e. the whole src/androidHostTest/screenshots tree. Gradle 9 walks that tree to compute the input snapshot and now fails hard if any listed file disappears between directory listing and content hashing — under Gradle 8 the same condition was tolerated. On macOS this seems easy to trigger (Spotlight indexing, IDE file watchers, parallel Gradle tasks touching the dir, etc.).

Workaround I'm using

tasks.withType<Test>().configureEach {
    if (name == "testAndroidHostTest") {
        doNotTrackState(
             "Roborazzi's screenshot input dir is read by Gradle 9 at execution time " +
                "and trips on transient NoSuchFileException."
        )
    }
}

This makes the symptom go away at the cost of losing UP-TO-DATE skipping for screenshot tests (which is tbh quite costly).

Environment

  • Roborazzi: 1.60.0
  • Gradle: 9.4.1
  • AGP: 9.2.1
  • Kotlin: 2.3.21
  • JDK: 21
  • OS: macOS 26.4.1 (arm64)

Activity

  1. takahirom commented on May 17, 2026

    @takahirom
    Owner

    Thanks. I'm investigating this issue.

  2. boiler23 commented on Jun 14, 2026

    @boiler23
    Author

    Hey, just wanted to share a short update from my end:
    I upgraded to Roborazzi 1.64.0 yesterday, but it seems the issue is still there:

    Execution failed for task ':trips:stats:testAndroidHostTest'.
    > Cannot access input property 'roborazziImageInput' of task ':trips:stats:testAndroidHostTest'. Accessing unreadable inputs or outputs is not supported. Declare the task as untracked by using Task.doNotTrackState(). For more information, please refer to https://docs.gradle.org/9.4.1/userguide/incremental_build.html#sec:disable-state-tracking in the Gradle documentation.
       > java.nio.file.NoSuchFileException:
    

    I get the impression that it happens more frequently on 1.64 than on 1.63, but I don't have any measurable evidence.

    A couple of other bits:

    1. Sometimes I get another exception:
    * What went wrong:
    Execution failed for task ':trips:list:finalizeTestRoborazziIosSimulatorArm64'.
    > kotlin.io.FileAlreadyExistsException: 
    

    The file indeed exists: it is the reference screenshot in the repository.
    Sometimes I get both exceptions for different modules in the project.

    1. It seems the issue might happen even if the module's tests are up-to-date (and after the error happens - I need to re-run the tests).

    2. Re-running Gradle tasks almost always helps, or at least rotates the error with 1.64.0. So in 2-3 re-runs, I get the green build.

    Those bullets altogether sound like a race condition to me.

  3. boiler23 commented on Jun 14, 2026

    @boiler23
    Author

    And one more consideration:
    I usually run ./gradlew testAndroidHostTest which executes all tests across all modules.
    After I stumble upon this error, running testAndroidHostTest in a specific failed module typically helps too.
    Perhaps it is a multi-module related issue?

  4. takahirom commented on Jun 14, 2026

    @takahirom
    Owner

    @boiler23
    Thanks for the detailed reports — a few questions to pin down the race:

    1. outputDir config: Do you set roborazzi { outputDir = ... } anywhere? If so, what path — and do multiple modules point at the same shared directory, or is it per-module (default build/outputs/roborazzi)?
    2. Failing path + task: Could you re-run with --stacktrace --info and paste the full NoSuchFileException: <path> and the exact failing task? (A build scan URL would be ideal.) Which dir is the vanishing file in — the output dir, the intermediate dir, or the compare dir?
    3. Single module in isolation: Does :failing:module:testAndroidHostTest run alone ever fail, or is it always green in isolation and only fails in the full multi-module run?
    4. Gradle settings: values of org.gradle.parallel, org.gradle.caching, org.gradle.configuration-cache, and is file-system watching enabled?
    5. Mode: which is running when it fails — verify, compare, or record?

    My current suspicion: the screenshot dir is registered as a tracked task input while it's also being written (runtime + the intermediate↔output copies), and Gradle 9 now hard-fails if a file disappears mid-fingerprint. The first two questions should tell us whether it's cross-module contention on a shared directory.

  5. boiler23 commented on Jun 14, 2026

    @boiler23
    Author

    @takahirom,

    1. Yes, I do set outputDir; it's different for each module. I'm doing it from my own convention plugin, applied to modules using Roborazzi. Here's the code:
    class MyRoborazziPlugin : Plugin<Project> {
        override fun apply(target: Project) = with(target) {
            pluginManager.apply(libs.plugins.roborazzi.get().pluginId)
    
            extensions.configure<RoborazziExtension> {
                outputDir.set(layout.projectDirectory.dir(SCREENSHOTS_DIR))
            }
    
            extensions.configure<KotlinMultiplatformExtension> {
                sourceSets.matching { it.name == ANDROID_HOST_TEST_SOURCE_SET }.configureEach {
                    dependencies {
                        implementation(libs.androidx.compose.ui.testManifest)
                        implementation(libs.androidx.compose.ui.uiTestJUnit4)
                        implementation(libs.test.androidx.espresso.core)
                        implementation(libs.test.androidx.ext.junit)
                        implementation(libs.test.robolectric)
                        implementation(libs.test.roborazzi.core)
                        implementation(libs.test.roborazzi.compose)
                    }
                }
            }
        }
    
        private companion object {
            const val SCREENSHOTS_DIR = "src/androidHostTest/screenshots"
            const val ANDROID_HOST_TEST_SOURCE_SET = "androidHostTest"
        }
    }
    
    
    1. I will, once I encounter the failure again.
    2. Hard to say for sure, but I'll keep an eye on it. It seems likely to me that a single module never fails.
    3. Yes, all three are enabled. Not sure about file system watching, though. I don't have any specific Gradle settings in this regard.
    4. It's always verify, which is set by default in my gradle.properties: roborazzi.test.verify=true
  6. takahirom commented on Jun 15, 2026

    @takahirom
    Owner

    @boiler23
    Thanks! It looks like the issue reproduces when multiple Roborazzi test tasks run in a single Gradle invocation (they share one screenshot output dir, so one variant's finalizer copy races another variant's input fingerprinting). When you hit the failure, which exact Gradle command / tasks are you running? e.g. a bare testAndroidHostTest, or an aggregate like check / build / allTests that also pulls in other targets' test tasks (jvm / iOS)?

  7. boiler23 commented on Jun 15, 2026

    @boiler23
    Author

    @takahirom yes, I'm running aggregated test task like this ./gradlew test allTests, that also pulls iOS tasks (the only other target I have).
    Sometimes I also add detekt/lint tasks to the command line if that matters, so the full command can be ./gradlew detektFix lintAndroid test allTests

  8. takahirom commented on Jun 16, 2026

    @takahirom
    Owner

    @boiler23
    Thanks. I think if you run the tasks separately, you can work around the issue. The reason it occurs is that we share the output directory directly between tests, and we run the tests simultaneously. Even if we fix it by making the copying atomic, there might still be some other strange problems.

  9. takahirom commented on Jun 16, 2026

    @takahirom
    Owner

    Well, when we get a good AI model to fix it, I think we can add a flag to use a separate output directory for each variant. (It might be difficult to fix for now.)

  10. boiler23 commented on Jun 17, 2026

    @boiler23
    Author

    Thank you @takahirom, I will try next time separate runs and let you know if that helps 👍

  11. boiler23 commented on Jul 1, 2026

    @boiler23
    Author

    One interesting observation:

    1. After I face the issue Cannot access input property 'roborazziImageInput' while running ./gradlew test for all modules, running ./gradlew :module:testAndroidHostTest is always green;
    2. After that, I assume the test results for this module should be cached, and its tests shouldn't re-run when I do the global ./gradlew test again, right? However, it's not the case - the issue pops up immediately again.
  12. takahirom commented on Jul 1, 2026

    @takahirom
    Owner

    Thanks. Roborazzi uses finalizedBy to maintain its caches, so this task(copying screenshots) is executed every time.

    Finalizer tasks are executed even if the finalized task fails or if the finalized task is considered UP-TO-DATE

    https://docs.gradle.org/current/userguide/controlling_task_execution.html#sec:finalizer_tasks

  13. takahirom commented on Jul 2, 2026

    @takahirom
    Owner

    @boiler23 (cc @F43nd1r — this relates to #804 too)

    I've been prototyping the flag I mentioned earlier: an opt-in option that gives each variant/target its own output directory, so Roborazzi test tasks running in a single Gradle invocation never share a directory and the race disappears at the root.

    roborazzi {
      // Experimental
      separateOutputDirs.set(true)
    }
    

    With this enabled, screenshots go into a subdirectory per task slug:

    build/outputs/roborazzi/debug/...
    build/outputs/roborazzi/androidHostTest/...
    build/outputs/roborazzi/desktop/...
    

    The intermediate directory is separated the same way, so the finalizer copy of one variant can no longer race another variant's input fingerprinting.

    Before I move forward, I'd like to check whether this actually works for you, because it has a real cost: golden images move into the per-slug subdirectory, so you'd need to re-record (or manually move) your existing goldens once. A few questions:

    1. Given that migration cost, would you enable this option in your project?
    2. Compared to the atomic-copy approach in Potential fix for #830: Use atomic copies to fix Gradle 9 screenshot directory races #845 (which keeps the shared directory and mitigates the race), do you have a preference? They're not mutually exclusive — but I'd like to know which one solves your day-to-day problem.
    3. Is the per-slug directory layout (outputs/roborazzi//) compatible with how you store goldens in VCS / CI?

    If the answers are positive I'll open a PR and ship it as an experimental option in the next release.

  14. boiler23 commented on Jul 3, 2026

    @boiler23
    Author

    @takahirom, yes, I think that should work for me.
    It was rather hard for me to build a local version of Roborazzi, so the PR I raised earlier is a blind guess, rather than a preference.
    When you implement this flag, I'll certainly try it out!

  15. takahirom commented on Jul 4, 2026

    @takahirom
    Owner

    @boiler23 Thanks. I've released this feature in https://github.com/takahirom/roborazzi/releases/tag/1.65.0. Could you check it out?

  16. boiler23 commented on Jul 4, 2026

    @boiler23
    Author

    Will do!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions