diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index afeed6c4f7..255be5fed7 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -78,7 +78,7 @@ class AndroidLibraryConventionPlugin : Plugin { // AGP's built-in Kotlin no longer auto-selects the kotlin-test // JUnit variant, so provide it explicitly for all library modules. "testImplementation"(libs.findLibrary("kotlin-test-junit").get()) - // Provides robolectric.properties pinning SDK to 36 (latest Robolectric supports). + // Provides robolectric.properties, which pins the Robolectric SDK level. if (path != ":libs:test-utils") { "testImplementation"(project(":libs:test-utils")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 385fcc4aaa..a72da76ab7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,7 +89,7 @@ espresso = "3.7.0" mockito-kotlin = "6.3.0" mockk = "1.14.11" turbine = "1.2.1" -robolectric = "4.16.1" +robolectric = "4.17" slf4j-simple = "2.0.19" androidxUiJUnit = "1.11.4" mixpanel = "8.10.0" diff --git a/libs/test-utils/src/main/resources/robolectric.properties b/libs/test-utils/src/main/resources/robolectric.properties index 564b8fa356..aff638fb93 100644 --- a/libs/test-utils/src/main/resources/robolectric.properties +++ b/libs/test-utils/src/main/resources/robolectric.properties @@ -1,3 +1,4 @@ -# Pin Robolectric to SDK 36 — the latest version it supports (4.16.1). -# This avoids needing @Config(sdk = [36]) on every test class. +# Pin Robolectric to SDK 36 so tests don't need @Config(sdk = ...) on every class. +# Robolectric 4.17 supports SDK 37 (= compileSdk/targetSdk); moving the pin is a +# deliberate change to what every unpinned test runs against, so it lands separately. sdk=36 diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c8d4d7ba1..71ee3ac63e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -374,6 +374,26 @@ run { "org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion", "org.jetbrains.kotlin:kotlin-metadata-jvm:$kotlinVersion", ) + // Robolectric reaches into JDK internals that the module system closes off by + // default on Java 9+. From 4.17 it uses jdk.internal.access.SharedSecrets to set + // up FileDescriptor, which throws IllegalAccessException on our Java 21 toolchain + // unless that package is opened; the rest mirror Robolectric's own test config so + // shadows that reflect into java.nio, java.text and the AWT font stack keep working. + // This has to live here rather than in a convention plugin: :libs:encryption:utils + // runs Robolectric from androidHostTest and applies the AGP/KMP plugins directly, + // so no flipcash convention plugin sees it. + val robolectricJvmArgs = listOf( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.net=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.security=ALL-UNNAMED", + "--add-opens=java.base/java.text=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.access=ALL-UNNAMED", + "--add-opens=java.desktop/java.awt.font=ALL-UNNAMED", + ) gradle.lifecycle.beforeProject { if (path == ":") { extra["flipcash.koverModules"] = koverModulesForRoot @@ -389,5 +409,8 @@ run { tasks.matching { task -> task.name.contains("kapt") }.configureEach { enabled = false } + tasks.withType(org.gradle.api.tasks.testing.Test::class.java).configureEach { + jvmArgs(robolectricJvmArgs) + } } }