From 55f585d72c0e35d5083c0cecc95ef371b5273c28 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sat, 20 Dec 2025 18:08:23 +0100 Subject: [PATCH 01/22] Refactor build system for instrumentation, use version catalog etc --- build-logic/gradle/libs.versions.toml | 80 ++++++++ build-logic/src/main/kotlin/Dependencies.kt | 186 +++++++++--------- build-logic/src/main/kotlin/Deployment.kt | 10 +- build-logic/src/main/kotlin/Environment.kt | 4 +- build-logic/src/main/kotlin/Tasks.kt | 41 ++-- .../extensions/VersionCatalogExtensions.kt | 27 +++ instrumentation/build.gradle.kts | 112 +++++++++-- instrumentation/buildSrc/settings.gradle.kts | 7 + instrumentation/compose/build.gradle.kts | 128 ++++-------- instrumentation/core/build.gradle.kts | 131 ++++-------- instrumentation/extensions/build.gradle.kts | 101 ++-------- instrumentation/runner/build.gradle.kts | 131 ++++-------- instrumentation/sample/build.gradle.kts | 124 +++++------- instrumentation/settings.gradle.kts | 6 + .../testutil-reflect/build.gradle.kts | 2 +- instrumentation/testutil/build.gradle.kts | 84 ++------ 16 files changed, 533 insertions(+), 641 deletions(-) create mode 100644 build-logic/gradle/libs.versions.toml create mode 100644 build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt create mode 100644 instrumentation/buildSrc/settings.gradle.kts diff --git a/build-logic/gradle/libs.versions.toml b/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..ce86aa8e --- /dev/null +++ b/build-logic/gradle/libs.versions.toml @@ -0,0 +1,80 @@ +[versions] +androidxActivity = "1.10.1" +androidxMultidex = "2.0.1" +androidxTestAnnotation = "1.0.1" +androidxTestCore = "1.6.1" +androidxTestMonitor = "1.7.2" +androidxTestRunner = "1.6.2" +apiguardian = "1.1.2" +compose = "1.10.0" +dokka = "2.0.0" +espresso = "3.6.1" +instantTaskExecutorExtension = "1.0.0" +junit4 = "4.13.2" +#noinspection NewerVersionAvailable +junit5 = "5.14.1" +junit6 = "6.0.1" +konftoml = "1.1.2" +korte = "2.4.12" +kotlin = "2.3.0" +kotlinBinaryCompValidator = "0.17.0" +kotlinCoroutines = "1.10.2" +mockitoCore = "5.16.0" +mockitoKotlin = "5.4.0" +nexusPublish = "2.0.0" +robolectric = "4.14.1" +shadow = "8.1.1" +truth = "1.4.4" + +[plugins] +# Intentionally missing version declaration, as different versions are applied depending on context +android-app = { id = "com.android.application" } +android-library = { id = "com.android.library" } +android-junit = { id = "de.mannodermaus.android-junit5" } + +compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-binarycompvalidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlinBinaryCompValidator" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } + +[libraries] +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } +androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "androidxMultidex" } +androidx-test-annotation = { module = "androidx.test:annotation", version.ref = "androidxTestAnnotation" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidxTestMonitor" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } +apiguardian = { module = "org.apiguardian:apiguardian-api", version.ref = "apiguardian" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } +compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-uitooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-test-core = { module = "androidx.compose.ui:ui-test", version.ref = "compose" } +compose-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } +compose-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +instanttaskexecutor-extension = { module = "io.github.neboskreb:instant-task-executor-extension", version.ref = "instantTaskExecutorExtension" } +#noinspection SimilarGradleDependency +junit-framework-bom5 = { module = "org.junit:junit-bom", version.ref = "junit5" } +#noinspection SimilarGradleDependency +junit-framework-bom6 = { module = "org.junit:junit-bom", version.ref = "junit6" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } +junit-platform-commons = { module = "org.junit.platform:junit-platform-commons" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +junit-platform-runner = { module = "org.junit.platform:junit-platform-runner" } +junit-vintage-api = { module = "junit:junit", version.ref = "junit4" } +junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" } +konftoml = { module = "com.uchuhimo:konf-toml", version.ref = "konftoml" } +korte = { module = "com.soywiz.korlibs.korte:korte", version.ref = "korte" } +kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinCoroutines" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +truth-core = { module = "com.google.truth:truth", version.ref = "truth" } +truth-extensions = { module = "com.google.truth.extensions:truth-java8-extension", version.ref = "truth" } diff --git a/build-logic/src/main/kotlin/Dependencies.kt b/build-logic/src/main/kotlin/Dependencies.kt index b75bcd1b..e9fc15f5 100644 --- a/build-logic/src/main/kotlin/Dependencies.kt +++ b/build-logic/src/main/kotlin/Dependencies.kt @@ -1,93 +1,93 @@ -@file:Suppress("ClassName") - -object libs { - object versions { - const val kotlin = "2.3.0" - const val junitJupiter = "5.14.0" - const val junitVintage = "5.14.0" - const val junitPlatform = "1.14.0" - - const val compose = "1.10.0" - const val androidXMultidex = "2.0.1" - const val androidXTestAnnotation = "1.0.1" - const val androidXTestCore = "1.6.1" - const val androidXTestMonitor = "1.7.2" - const val androidXTestRunner = "1.6.2" - - const val activityCompose = "1.10.1" - const val apiGuardian = "1.1.2" - const val coroutines = "1.10.2" - const val dokka = "2.0.0" - const val espresso = "3.6.1" - const val junit4 = "4.13.2" - const val konfToml = "1.1.2" - const val kotlinxBinaryCompatibilityValidator = "0.17.0" - const val nexusPublish = "2.0.0" - const val korte = "2.4.12" - const val mockitoCore = "5.16.0" - const val mockitoKotlin = "5.4.0" - const val robolectric = "4.14.1" - const val shadow = "8.1.1" - const val truth = "1.4.4" - } - - object plugins { - fun android(version: SupportedAgp) = "com.android.tools.build:gradle:${version.version}" - const val composeCompiler = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${versions.kotlin}" - const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin}" - const val shadow = "com.github.johnrengelman:shadow:${libs.versions.shadow}" - const val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${libs.versions.dokka}" - } - - // Libraries - val androidTools = run { - // The version of this library is linked to AGP - // (essentially: "AGP + 23.0.0") - val agpVersionParts = SupportedAgp.oldest.version.split('.') - val toolsVersion = "${23 + agpVersionParts.first().toInt()}." + agpVersionParts.drop(1).joinToString(".") - "com.android.tools:common:$toolsVersion" - } - - const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}" - const val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}" - - const val junitJupiterApi = "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}" - const val junitJupiterParams = "org.junit.jupiter:junit-jupiter-params:${versions.junitJupiter}" - const val junitJupiterEngine = "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}" - const val junitVintageEngine = "org.junit.vintage:junit-vintage-engine:${versions.junitVintage}" - const val junitPlatformCommons = "org.junit.platform:junit-platform-commons:${versions.junitPlatform}" - const val junitPlatformLauncher = "org.junit.platform:junit-platform-launcher:${versions.junitPlatform}" - const val junitPlatformRunner = "org.junit.platform:junit-platform-runner:${versions.junitPlatform}" - const val apiguardianApi = "org.apiguardian:apiguardian-api:${versions.apiGuardian}" - - const val composeUi = "androidx.compose.ui:ui:${versions.compose}" - const val composeUiTooling = "androidx.compose.ui:ui-tooling:${versions.compose}" - const val composeFoundation = "androidx.compose.foundation:foundation:${versions.compose}" - const val composeMaterial = "androidx.compose.material:material:${versions.compose}" - const val composeActivity = "androidx.activity:activity-compose:${versions.activityCompose}" - - // Testing - const val junit4 = "junit:junit:${versions.junit4}" - const val korte = "com.soywiz.korlibs.korte:korte:${versions.korte}" - const val konfToml = "com.uchuhimo:konf-toml:${versions.konfToml}" - const val mockitoCore = "org.mockito:mockito-core:${versions.mockitoCore}" - const val mockitoKotlin = "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" - const val truth = "com.google.truth:truth:${versions.truth}" - const val truthJava8Extensions = "com.google.truth.extensions:truth-java8-extension:${versions.truth}" - const val robolectric = "org.robolectric:robolectric:${versions.robolectric}" - - const val androidXMultidex = "androidx.multidex:multidex:${versions.androidXMultidex}" - const val androidXTestAnnotation = "androidx.test:annotation:${versions.androidXTestAnnotation}" - const val androidXTestCore = "androidx.test:core:${versions.androidXTestCore}" - const val androidXTestMonitor = "androidx.test:monitor:${versions.androidXTestMonitor}" - const val androidXTestRunner = "androidx.test:runner:${versions.androidXTestRunner}" - const val espressoCore = "androidx.test.espresso:espresso-core:${versions.espresso}" - - const val composeUiTest = "androidx.compose.ui:ui-test:${versions.compose}" - const val composeUiTestJUnit4 = "androidx.compose.ui:ui-test-junit4:${versions.compose}" - const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:${versions.compose}" - - // Documentation - // For the latest version refer to GitHub repo neboskreb/instant-task-executor-extension - const val instantTaskExecutorExtension = "io.github.neboskreb:instant-task-executor-extension:1.0.0" -} +//@file:Suppress("ClassName") +// +//object libs2 { +// object versions { +// const val kotlin = "2.3.0" +// const val junitJupiter = "5.14.0" +// const val junitVintage = "5.14.0" +// const val junitPlatform = "1.14.0" +// +// const val compose = "1.10.0" +// const val androidXMultidex = "2.0.1" +// const val androidXTestAnnotation = "1.0.1" +// const val androidXTestCore = "1.6.1" +// const val androidXTestMonitor = "1.7.2" +// const val androidXTestRunner = "1.6.2" +// +// const val activityCompose = "1.10.1" +// const val apiGuardian = "1.1.2" +// const val coroutines = "1.10.2" +// const val dokka = "2.0.0" +// const val espresso = "3.6.1" +// const val junit4 = "4.13.2" +// const val konfToml = "1.1.2" +// const val kotlinxBinaryCompatibilityValidator = "0.17.0" +// const val nexusPublish = "2.0.0" +// const val korte = "2.4.12" +// const val mockitoCore = "5.16.0" +// const val mockitoKotlin = "5.4.0" +// const val robolectric = "4.14.1" +// const val shadow = "8.1.1" +// const val truth = "1.4.4" +// } +// +// object plugins { +// fun android(extensions.version: SupportedAgp) = "com.android.tools.build:gradle:${extensions.version.extensions.version}" +// const val composeCompiler = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${versions.kotlin}" +// const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${extensions.libs.versions.kotlin}" +// const val shadow = "com.github.johnrengelman:shadow:${extensions.libs.versions.shadow}" +// const val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${extensions.libs.versions.dokka}" +// } +// +// // Libraries +// val androidTools = run { +// // The extensions.version of this extensions.library is linked to AGP +// // (essentially: "AGP + 23.0.0") +// val agpVersionParts = SupportedAgp.oldest.extensions.version.split('.') +// val toolsVersion = "${23 + agpVersionParts.first().toInt()}." + agpVersionParts.drop(1).joinToString(".") +// "com.android.tools:common:$toolsVersion" +// } +// +// const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}" +// const val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}" +// +// const val junitJupiterApi = "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}" +// const val junitJupiterParams = "org.junit.jupiter:junit-jupiter-params:${versions.junitJupiter}" +// const val junitJupiterEngine = "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}" +// const val junitVintageEngine = "org.junit.vintage:junit-vintage-engine:${versions.junitVintage}" +// const val junitPlatformCommons = "org.junit.platform:junit-platform-commons:${versions.junitPlatform}" +// const val junitPlatformLauncher = "org.junit.platform:junit-platform-launcher:${versions.junitPlatform}" +// const val junitPlatformRunner = "org.junit.platform:junit-platform-runner:${versions.junitPlatform}" +// const val apiguardianApi = "org.apiguardian:apiguardian-api:${versions.apiGuardian}" +// +// const val composeUi = "androidx.compose.ui:ui:${versions.compose}" +// const val composeUiTooling = "androidx.compose.ui:ui-tooling:${versions.compose}" +// const val composeFoundation = "androidx.compose.foundation:foundation:${versions.compose}" +// const val composeMaterial = "androidx.compose.material:material:${versions.compose}" +// const val composeActivity = "androidx.activity:activity-compose:${versions.activityCompose}" +// +// // Testing +// const val junit4 = "junit:junit:${versions.junit4}" +// const val korte = "com.soywiz.korlibs.korte:korte:${versions.korte}" +// const val konfToml = "com.uchuhimo:konf-toml:${versions.konfToml}" +// const val mockitoCore = "org.mockito:mockito-core:${versions.mockitoCore}" +// const val mockitoKotlin = "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}" +// const val truth = "com.google.truth:truth:${versions.truth}" +// const val truthJava8Extensions = "com.google.truth.extensions:truth-java8-extension:${versions.truth}" +// const val robolectric = "org.robolectric:robolectric:${versions.robolectric}" +// +// const val androidXMultidex = "androidx.multidex:multidex:${versions.androidXMultidex}" +// const val androidXTestAnnotation = "androidx.test:annotation:${versions.androidXTestAnnotation}" +// const val androidXTestCore = "androidx.test:core:${versions.androidXTestCore}" +// const val androidXTestMonitor = "androidx.test:monitor:${versions.androidXTestMonitor}" +// const val androidXTestRunner = "androidx.test:runner:${versions.androidXTestRunner}" +// const val espressoCore = "androidx.test.espresso:espresso-core:${versions.espresso}" +// +// const val composeUiTest = "androidx.compose.ui:ui-test:${versions.compose}" +// const val composeUiTestJUnit4 = "androidx.compose.ui:ui-test-junit4:${versions.compose}" +// const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:${versions.compose}" +// +// // Documentation +// // For the latest extensions.version refer to GitHub repo neboskreb/instant-task-executor-extension +// const val instantTaskExecutorExtension = "io.github.neboskreb:instant-task-executor-extension:1.0.0" +//} diff --git a/build-logic/src/main/kotlin/Deployment.kt b/build-logic/src/main/kotlin/Deployment.kt index 5fa0f154..5e8cf978 100644 --- a/build-logic/src/main/kotlin/Deployment.kt +++ b/build-logic/src/main/kotlin/Deployment.kt @@ -34,7 +34,7 @@ fun Project.configureDeployment(deployConfig: Deployed) { // and will raise an error on inconsistent data) rootProject.configureRootDeployment(deployConfig, credentials) - val isAndroid = plugins.findPlugin("com.android.library") != null + val isAndroid = plugins.findPlugin("com.android.extensions.library") != null val isGradlePlugin = plugins.hasPlugin("java-gradle-plugin") apply { @@ -133,13 +133,13 @@ private fun Project.configureRootDeployment(deployConfig: Deployed, credentials: } // Validate the integrity of published versions - // (all subprojects must use the same group ID and version number or else an error is raised) + // (all subprojects must use the same group ID and extensions.version number or else an error is raised) if (version != "unspecified") { if (version != deployConfig.currentVersion || group != deployConfig.groupId) { throw IllegalStateException("A subproject tried to set '${deployConfig.groupId}:${deployConfig.currentVersion}' " + "as the coordinates for the artifacts of the repository, but '$group:$version' was already set " + "previously by a different subproject. As per the requirements of the Nexus Publishing plugin, " + - "all subprojects must use the same version number! Please check Artifacts.kt for inconsistencies!") + "all subprojects must use the same extensions.version number! Please check Artifacts.kt for inconsistencies!") } else { // Already configured and correct return @@ -175,7 +175,7 @@ private fun MavenPublication.applyPublicationDetails( if (isAndroid) { artifact(buildDir.file("outputs/aar/${project.name}-release.aar").get().asFile) } else { - artifact(buildDir.file("libs/${project.name}-$version.jar")) + artifact(buildDir.file("extensions.libs/${project.name}-$version.jar")) } artifact(androidSourcesJar) artifact(javadocJar) @@ -211,7 +211,7 @@ private fun MavenPublication.applyPublicationDetails( "Found a BOM declaration in the dependencies of project" + "${project.path}: $dep. Prefer declaring its " + "transitive artifacts explicitly by " + - "adding a version contraint to them." + "adding a extensions.version contraint to them." ) } diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt index 6ade4345..1d8092a6 100644 --- a/build-logic/src/main/kotlin/Environment.kt +++ b/build-logic/src/main/kotlin/Environment.kt @@ -31,7 +31,7 @@ enum class SupportedAgp( } val shortVersion: String = run { - // Extract first two components of the Maven dependency's version string. + // Extract first two components of the Maven dependency's extensions.version string. val components = version.split('.') if (components.size < 2) { throw IllegalArgumentException("Cannot derive AGP configuration name from: $this") @@ -58,7 +58,7 @@ sealed class Platform(val name: String) { } /** - * Encapsulation for "deployable" library artifacts, + * Encapsulation for "deployable" extensions.library artifacts, * containing all sorts of configuration related to Maven coordinates, for instance. */ class Deployed internal constructor( diff --git a/build-logic/src/main/kotlin/Tasks.kt b/build-logic/src/main/kotlin/Tasks.kt index 1b747a71..155493f5 100644 --- a/build-logic/src/main/kotlin/Tasks.kt +++ b/build-logic/src/main/kotlin/Tasks.kt @@ -1,3 +1,8 @@ +import extensions.agp +import extensions.kgp +import extensions.library +import extensions.libs +import extensions.version import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.api.DefaultTask import org.gradle.api.Project @@ -33,8 +38,8 @@ fun Project.configureTestResources() { "MIN_SDK_VERSION" to Android.sampleMinSdkVersion.toString(), "TARGET_SDK_VERSION" to Android.targetSdkVersion.toString(), - "KOTLIN_VERSION" to libs.versions.kotlin, - "JUNIT_JUPITER_VERSION" to libs.versions.junitJupiter, + "KOTLIN_VERSION" to libs.version("kotlin"), + "JUNIT_JUPITER_VERSION" to libs.version("junit5"), // TODO "JUNIT5_ANDROID_LIBS_VERSION" to Artifacts.Instrumentation.Core.latestStableVersion, // Collect all supported AGP versions into a single string. @@ -81,19 +86,14 @@ fun Project.configureTestResources() { description = "Local dependencies used for compiling & running " + "tests source code in Gradle functional tests against AGP ${plugin.version}" extendsFrom(configurations.getByName("implementation")) - - val agpDependency = libs.plugins.android(plugin).substringBeforeLast(":") - project.dependencies.add(this.name, "${agpDependency}:${plugin.version}") + project.dependencies.add(this.name, libs.agp(plugin)) // For Android Gradle Plugins before 9.x, add the Kotlin Gradle Plugin explicitly, // acknowledging the different plugin variants introduced in Kotlin 1.7. - // Acknowledging the minimum required Gradle version, request the correct variant for KGP + // Acknowledging the minimum required Gradle extensions.version, request the correct variant for KGP // (see https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html#plugin-with-variants) if (plugin < SupportedAgp.AGP_9_0) { - project.dependencies.add( - this.name, - "org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin}" - ).apply { + project.dependencies.add(this.name, libs.kgp).apply { with(this as ExternalModuleDependency) { attributes { attribute( @@ -178,13 +178,13 @@ fun Copy.configureCreateVersionClassTask( "INSTRUMENTATION_EXTENSIONS" to Artifacts.Instrumentation.Extensions.artifactId, "INSTRUMENTATION_RUNNER" to Artifacts.Instrumentation.Runner.artifactId, - // Find an appropriate version of the instrumentation library, - // depending on the version of how the plugin is configured + // Find an appropriate extensions.version of the instrumentation extensions.library, + // depending on the extensions.version of how the plugin is configured "INSTRUMENTATION_VERSION" to instrumentationVersion, // JUnit 5.12+ requires the platform launcher on the runtime classpath; - // to prevent issues with version mismatching, the plugin applies this for users - "JUNIT_PLATFORM_LAUNCHER" to libs.junitPlatformLauncher + // to prevent issues with extensions.version mismatching, the plugin applies this for users + "JUNIT_PLATFORM_LAUNCHER" to project.libs.library("junit-platform-launcher") ) ), ReplaceTokens::class.java ) @@ -192,14 +192,14 @@ fun Copy.configureCreateVersionClassTask( } /** - * Helper Task class for generating an up-to-date version of the project's README.md. - * Using a template file, the plugin's version constants & other dependency versions + * Helper Task class for generating an up-to-date extensions.version of the project's README.md. + * Using a template file, the plugin's extensions.version constants & other dependency versions * are automatically injected into the README. */ abstract class GenerateReadme : DefaultTask() { companion object { private val PLACEHOLDER_REGEX = Regex("\\\$\\{(.+)}") - private val EXTERNAL_DEP_REGEX = Regex("libs\\.(.+)") + private val EXTERNAL_DEP_REGEX = Regex("extensions.libs\\.(.+)") private val CONSTANT_REGEX = Regex("constants\\.(.+)") private const val PLUGIN_VERSION = "pluginVersion" @@ -270,8 +270,9 @@ abstract class GenerateReadme : DefaultTask() { val externalDependency = match3.groups.last()?.value ?: throw InvalidPlaceholder(match3) - val field = libs.javaClass.getField(externalDependency) - field.get(null) as String +// val field = extensions.libs.javaClass.getField(externalDependency) +// field.get(null) as String + "" // TODO: Connect this again } } } @@ -298,7 +299,7 @@ abstract class GenerateReadme : DefaultTask() { constants[match[1]] = match[2] } - // Special case for AGP version + // Special case for AGP extensions.version CONSTANTS_FILE_REGEX2.findAll(text).forEach { match -> constants[match[1]] = match.groupValues .drop(2) diff --git a/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt b/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt new file mode 100644 index 00000000..1ae3af11 --- /dev/null +++ b/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt @@ -0,0 +1,27 @@ +package extensions + +import SupportedAgp +import org.gradle.api.Project +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.getByType + +internal val Project.libs: VersionCatalog + get() = extensions.getByType().named("extensions.libs") + +fun VersionCatalog.agp(version: SupportedAgp): String { + return "com.android.tools.build:gradle:${version.version}" +} + +val VersionCatalog.kgp: String + get() = "org.jetbrains.kotlin:kotlin-gradle-plugin:${version("kotlin")}" + +fun VersionCatalog.library(name: String): Provider { + return findLibrary(name).get() +} + +fun VersionCatalog.version(name: String): String { + return findVersion(name).get().requiredVersion +} diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index a0331297..9262e326 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -1,21 +1,103 @@ +import com.android.build.api.dsl.LibraryExtension +import com.android.build.gradle.BaseExtension +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { - id("io.github.gradle-nexus.publish-plugin").version(libs.versions.nexusPublish) - id("org.jetbrains.kotlinx.binary-compatibility-validator").version(libs.versions.kotlinxBinaryCompatibilityValidator) -} + alias(libs.plugins.android.app).version(SupportedAgp.newestStable.version).apply(false) + alias(libs.plugins.android.junit).version(Artifacts.Plugin.latestStableVersion).apply(false) + alias(libs.plugins.android.library).version(SupportedAgp.newestStable.version).apply(false) -buildscript { - dependencies { - classpath(libs.plugins.kotlin) - classpath(libs.plugins.dokka) - classpath(libs.plugins.composeCompiler) - classpath(libs.plugins.android(SupportedAgp.newestStable)) - } + alias(libs.plugins.compose).apply(false) + alias(libs.plugins.dokka).apply(false) + alias(libs.plugins.kotlin.android).apply(false) + alias(libs.plugins.kotlin.jvm).apply(false) + + alias(libs.plugins.kotlin.binarycompvalidator) + alias(libs.plugins.publish) } apiValidation { - ignoredPackages.add("de.mannodermaus.junit5.internal") - ignoredPackages.add("de.mannodermaus.junit5.compose.internal") - ignoredProjects.add("sample") - ignoredProjects.add("testutil") - ignoredProjects.add("testutil-reflect") + ignoredPackages.add("de.mannodermaus.junit5.internal") + ignoredPackages.add("de.mannodermaus.junit5.compose.internal") + ignoredProjects.add("sample") + ignoredProjects.add("testutil") + ignoredProjects.add("testutil-reflect") +} + +subprojects { + apply(plugin = "explicit-api-mode") + + val jvmTarget = JvmTarget.JVM_17 + val javaVersion = JavaVersion.VERSION_17 + + // Configure Kotlin + plugins.withType { + tasks.withType>().configureEach { + compilerOptions { + this.progressiveMode.set(true) + if (this is KotlinJvmCompilerOptions) { + this.jvmTarget.set(jvmTarget) + this.freeCompilerArgs.add("-Xjvm-default=all") + } + } + } + } + + // Configure Android + plugins.withId("com.android.base") { + configure { + compileSdkVersion(Android.compileSdkVersion) + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility(javaVersion) + targetCompatibility(javaVersion) + } + + with(buildFeatures) { + buildConfig = false + resValues = false + } + + testOptions { + unitTests.isReturnDefaultValues = true + } + + if (this is LibraryExtension) { + lint { + // JUnit 4 refers to java.lang.management APIs, which are absent on Android. + warning.add("InvalidPackage") + targetSdk = Android.targetSdkVersion + } + + packaging { + resources.excludes.add("META-INF/AL2.0") + resources.excludes.add("META-INF/LGPL2.1") + resources.excludes.add("META-INF/LICENSE.md") + resources.excludes.add("META-INF/LICENSE-notice.md") + } + + testOptions { + targetSdk = Android.targetSdkVersion + } + } + } + } + + // Configure testing + tasks.withType { + failFast = true + testLogging { + events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + exceptionFormat = TestExceptionFormat.FULL + } + } } diff --git a/instrumentation/buildSrc/settings.gradle.kts b/instrumentation/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..30756a72 --- /dev/null +++ b/instrumentation/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../build-logic/gradle/libs.versions.toml")) + } + } +} diff --git a/instrumentation/compose/build.gradle.kts b/instrumentation/compose/build.gradle.kts index 88f13394..82d84048 100644 --- a/instrumentation/compose/build.gradle.kts +++ b/instrumentation/compose/build.gradle.kts @@ -1,101 +1,55 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - id("com.android.library") - kotlin("android") - id("explicit-api-mode") - id("de.mannodermaus.android-junit5").version(Artifacts.Plugin.latestStableVersion) - id("org.jetbrains.kotlin.plugin.compose") + alias(libs.plugins.android.junit) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose) } -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5.compose" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = Android.testComposeMinSdkVersion - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" - } - - buildFeatures { - compose = true - buildConfig = false - resValues = false - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } - - lint { - targetSdk = Android.targetSdkVersion - } + namespace = "de.mannodermaus.junit5.compose" - packaging { - resources.excludes.add("META-INF/AL2.0") - resources.excludes.add("META-INF/LGPL2.1") - } -} + defaultConfig { + minSdk = Android.testComposeMinSdkVersion + } -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - } + buildFeatures { + compose = true + } } junitPlatform { - // Using local dependency instead of Maven coordinates - instrumentationTests.enabled = false -} - -tasks.withType { - failFast = true - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } + // Using local dependency instead of Maven coordinates + instrumentationTests.enabled = false } dependencies { - implementation(project(":core")) - implementation(libs.kotlinStdLib) - implementation(libs.kotlinCoroutinesCore) - - implementation(libs.junitJupiterApi) - implementation(libs.junit4) - implementation(libs.espressoCore) - - implementation(libs.composeActivity) - implementation(libs.composeUi) - implementation(libs.composeUiTooling) - implementation(libs.composeFoundation) - implementation(libs.composeMaterial) - api(libs.composeUiTest) - api(libs.composeUiTestJUnit4) - implementation(libs.composeUiTestManifest) - - testImplementation(libs.junitJupiterApi) - testImplementation(libs.junitJupiterParams) - testRuntimeOnly(libs.junitJupiterEngine) - - androidTestImplementation(libs.junitJupiterApi) - androidTestImplementation(libs.junitJupiterParams) - androidTestImplementation(libs.espressoCore) - - androidTestRuntimeOnly(project(":runner")) - androidTestRuntimeOnly(libs.androidXTestRunner) + implementation(project(":core")) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.coroutines) + + implementation(libs.junit.jupiter.api) + implementation(libs.junit.vintage.api) + implementation(libs.espresso) + + implementation(libs.androidx.activity.compose) + implementation(libs.compose.ui) + implementation(libs.compose.uitooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + api(libs.compose.test.core) + api(libs.compose.test.junit4) + implementation(libs.compose.test.manifest) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testRuntimeOnly(libs.junit.jupiter.engine) + + androidTestImplementation(libs.junit.jupiter.api) + androidTestImplementation(libs.junit.jupiter.params) + androidTestImplementation(libs.espresso) + + androidTestRuntimeOnly(project(":runner")) + androidTestRuntimeOnly(libs.androidx.test.runner) } -project.configureDeployment(Artifacts.Instrumentation.Compose) +//project.configureDeployment(Artifacts.Instrumentation.Compose) diff --git a/instrumentation/core/build.gradle.kts b/instrumentation/core/build.gradle.kts index 4dbc036e..874f483c 100644 --- a/instrumentation/core/build.gradle.kts +++ b/instrumentation/core/build.gradle.kts @@ -1,76 +1,23 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { - id("com.android.library") - kotlin("android") - id("explicit-api-mode") - id("de.mannodermaus.android-junit5").version(Artifacts.Plugin.latestStableVersion) + alias(libs.plugins.android.junit) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = Android.testCoreMinSdkVersion - multiDexEnabled = true - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } + namespace = "de.mannodermaus.junit5" - buildFeatures { - buildConfig = false - resValues = false - } - - lint { - // JUnit 4 refers to java.lang.management APIs, which are absent on Android. - warning.add("InvalidPackage") - targetSdk = Android.targetSdkVersion - } - - packaging { - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } + defaultConfig { + minSdk = Android.testCoreMinSdkVersion + multiDexEnabled = true + } } junitPlatform { - filters { - // See TaggedTests.kt for usage of this tag - excludeTags("nope") - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - freeCompilerArgs.add("-Xjvm-default=all") - } -} - -tasks.withType { - failFast = true - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } + filters { + // See TaggedTests.kt for usage of this tag + excludeTags("nope") + } } // Use local project dependencies on android-test instrumentation libraries @@ -78,36 +25,36 @@ tasks.withType { val instrumentationLibraryRegex = Regex("de\\.mannodermaus\\.junit5:android-test-(.+):") configurations.all { - if ("debugAndroidTestRuntimeClasspath" in name) { - resolutionStrategy.dependencySubstitution.all { - instrumentationLibraryRegex.find(requested.toString())?.let { result -> - useTarget(project(":${result.groupValues[1]}")) - } + if ("debugAndroidTestRuntimeClasspath" in name) { + resolutionStrategy.dependencySubstitution.all { + instrumentationLibraryRegex.find(requested.toString())?.let { result -> + useTarget(project(":${result.groupValues[1]}")) + } + } } - } } dependencies { - implementation(libs.kotlinStdLib) - implementation(libs.junitJupiterApi) - api(libs.androidXTestCore) - // This is required by the "instrumentation-runner" companion library, - // since it can't provide any JUnit 5 runtime libraries itself - // due to fear of prematurely incrementing the minSdkVersion requirement. - runtimeOnly(libs.junitPlatformRunner) - runtimeOnly(libs.junitJupiterEngine) - - // This transitive dependency of JUnit 5 is required to be on the runtime classpath, - // since otherwise ART will print noisy logs to console when trying to resolve any - // of the annotations of JUnit 5 (see #291 for more info) - runtimeOnly(libs.apiguardianApi) - - androidTestImplementation(libs.junitJupiterApi) - androidTestImplementation(libs.junitJupiterParams) - androidTestImplementation(libs.espressoCore) - androidTestRuntimeOnly(project(":runner")) - - testImplementation(project(":testutil")) + implementation(libs.kotlin.stdlib) + implementation(libs.junit.jupiter.api) + api(libs.androidx.test.core) + // This is required by the "instrumentation-runner" companion library, + // since it can't provide any JUnit 5 runtime libraries itself + // due to fear of prematurely incrementing the minSdkVersion requirement. + runtimeOnly(libs.junit.platform.runner) + runtimeOnly(libs.junit.jupiter.engine) + + // This transitive dependency of JUnit 5 is required to be on the runtime classpath, + // since otherwise ART will print noisy logs to console when trying to resolve any + // of the annotations of JUnit 5 (see #291 for more info) + runtimeOnly(libs.apiguardian) + + androidTestImplementation(libs.junit.jupiter.api) + androidTestImplementation(libs.junit.jupiter.params) + androidTestImplementation(libs.espresso) + androidTestRuntimeOnly(project(":runner")) + + testImplementation(project(":testutil")) } -project.configureDeployment(Artifacts.Instrumentation.Core) +//project.configureDeployment(Artifacts.Instrumentation.Core) diff --git a/instrumentation/extensions/build.gradle.kts b/instrumentation/extensions/build.gradle.kts index 0759bd5d..0cc9e354 100644 --- a/instrumentation/extensions/build.gradle.kts +++ b/instrumentation/extensions/build.gradle.kts @@ -1,99 +1,24 @@ -import libs.plugins.android -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - repositories { - google() - mavenCentral() - sonatypeSnapshots() - } - - dependencies { - val latest = Artifacts.Plugin.latestStableVersion - classpath("de.mannodermaus.gradle.plugins:android-junit5:$latest") - } -} - plugins { - id("com.android.library") - kotlin("android") - id("explicit-api-mode") + alias(libs.plugins.android.junit) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } -apply { - plugin("de.mannodermaus.android-junit5") -} - -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5.extensions" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = Android.testRunnerMinSdkVersion - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - buildFeatures { - buildConfig = false - resValues = false - } - - lint { - // JUnit 4 refers to java.lang.management APIs, which are absent on Android. - warning.add("InvalidPackage") - targetSdk = Android.targetSdkVersion - } - - packaging { - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - } -} - -tasks.withType { - failFast = true - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } -} + namespace = "de.mannodermaus.junit5.extensions" -configurations.all { - // The Instrumentation Test Runner uses the plugin, - // which in turn provides the Instrumentation Test Runner again - - // that's kind of deep. - // To avoid conflicts, prefer using the local classes - // and exclude the dependency from being pulled in externally. - exclude(module = Artifacts.Instrumentation.Extensions.artifactId) + defaultConfig { + minSdk = Android.testRunnerMinSdkVersion + } } dependencies { - implementation(libs.androidXTestAnnotation) - implementation(libs.androidXTestRunner) - implementation(libs.junitJupiterApi) + implementation(libs.androidx.test.annotation) + implementation(libs.androidx.test.runner) + implementation(libs.junit.jupiter.api) - testImplementation(project(":testutil")) - testRuntimeOnly(libs.junitJupiterEngine) + testImplementation(project(":testutil")) + testRuntimeOnly(libs.junit.jupiter.engine) } -project.configureDeployment(Artifacts.Instrumentation.Extensions) +//project.configureDeployment(Artifacts.Instrumentation.Extensions) diff --git a/instrumentation/runner/build.gradle.kts b/instrumentation/runner/build.gradle.kts index dfc8bb50..23106ae9 100644 --- a/instrumentation/runner/build.gradle.kts +++ b/instrumentation/runner/build.gradle.kts @@ -1,109 +1,44 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -buildscript { - repositories { - google() - mavenCentral() - sonatypeSnapshots() - } - - dependencies { - val latest = Artifacts.Plugin.latestStableVersion - classpath("de.mannodermaus.gradle.plugins:android-junit5:$latest") - } -} - plugins { - id("com.android.library") - kotlin("android") - id("explicit-api-mode") -} - -apply { - plugin("de.mannodermaus.android-junit5") + alias(libs.plugins.android.junit) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5.runner" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = Android.testRunnerMinSdkVersion - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - buildFeatures { - buildConfig = false - resValues = false - } + namespace = "de.mannodermaus.junit5.runner" - lint { - // JUnit 4 refers to java.lang.management APIs, which are absent on Android. - warning.add("InvalidPackage") - targetSdk = Android.targetSdkVersion - } - - packaging { - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - } -} - -tasks.withType { - failFast = true - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } + defaultConfig { + minSdk = Android.testRunnerMinSdkVersion + } } configurations.all { - // The Instrumentation Test Runner uses the plugin, - // which in turn provides the Instrumentation Test Runner again - - // that's kind of deep. - // To avoid conflicts, prefer using the local classes - // and exclude the dependency from being pulled in externally. - exclude(module = Artifacts.Instrumentation.Runner.artifactId) + // The Instrumentation Test Runner uses the plugin, + // which in turn provides the Instrumentation Test Runner again - + // that's kind of deep. + // To avoid conflicts, prefer using the local classes + // and exclude the dependency from being pulled in externally. + exclude(module = Artifacts.Instrumentation.Runner.artifactId) } dependencies { - implementation(libs.androidXTestMonitor) - implementation(libs.androidXTestRunner) - implementation(libs.kotlinStdLib) - implementation(libs.junit4) - - // This module's JUnit 5 dependencies cannot be present on the runtime classpath, - // since that would prematurely raise the minSdkVersion requirement for target applications, - // even though not all product flavors might want to use JUnit 5. - // Therefore, only compile against those APIs, and have them provided at runtime - // by the "instrumentation" companion library instead. - compileOnly(libs.junitJupiterApi) - compileOnly(libs.junitJupiterParams) - compileOnly(libs.junitPlatformRunner) - - testImplementation(project(":testutil")) - testImplementation(libs.robolectric) - testRuntimeOnly(libs.junitJupiterEngine) -} - -project.configureDeployment(Artifacts.Instrumentation.Runner) + implementation(libs.androidx.test.monitor) + implementation(libs.androidx.test.runner) + implementation(libs.kotlin.stdlib) + implementation(libs.junit.vintage.api) + + // This module's JUnit 5 dependencies cannot be present on the runtime classpath, + // since that would prematurely raise the minSdkVersion requirement for target applications, + // even though not all product flavors might want to use JUnit 5. + // Therefore, only compile against those APIs, and have them provided at runtime + // by the "instrumentation" companion library instead. + compileOnly(libs.junit.jupiter.api) + compileOnly(libs.junit.jupiter.params) + compileOnly(libs.junit.platform.runner) + + testImplementation(project(":testutil")) + testImplementation(libs.robolectric) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +//project.configureDeployment(Artifacts.Instrumentation.Runner) diff --git a/instrumentation/sample/build.gradle.kts b/instrumentation/sample/build.gradle.kts index ae01ce8d..47e1cdb7 100644 --- a/instrumentation/sample/build.gradle.kts +++ b/instrumentation/sample/build.gradle.kts @@ -1,90 +1,66 @@ -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - id("com.android.application") - kotlin("android") - id("jacoco") - id("de.mannodermaus.android-junit5").version(Artifacts.Plugin.latestStableVersion) + alias(libs.plugins.android.app) + alias(libs.plugins.android.junit) + alias(libs.plugins.kotlin.android) + id("jacoco") } -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5.sample" - compileSdk = Android.compileSdkVersion - - defaultConfig { - applicationId = "de.mannodermaus.junit5.sample" - minSdk = Android.sampleMinSdkVersion - targetSdk = Android.targetSdkVersion - versionCode = 1 - versionName = "1.0" - - // Make sure to use the AndroidJUnitRunner (or a sub-class) in order to hook in the JUnit 5 Test Builder - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - // These two lines are not needed for a normal integration; - // this sample project disables the automatic integration, so it must be done manually - testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" - testInstrumentationRunnerArguments["configurationParameters"] = "junit.jupiter.execution.parallel.enabled=true,junit.jupiter.execution.parallel.mode.default=concurrent" - - buildFeatures { - buildConfig = true - } - - buildConfigField("boolean", "MY_VALUE", "true") - - testOptions { - animationsDisabled = true + namespace = "de.mannodermaus.junit5.sample" + + defaultConfig { + applicationId = "de.mannodermaus.junit5.sample" + minSdk = Android.sampleMinSdkVersion + targetSdk = Android.targetSdkVersion + versionCode = 1 + versionName = "1.0" + + // Make sure to use the AndroidJUnitRunner (or a sub-class) in order to hook in the JUnit 5 Test Builder + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // These two lines are not needed for a normal integration; + // this sample project disables the automatic integration, so it must be done manually + testInstrumentationRunnerArguments["runnerBuilder"] = + "de.mannodermaus.junit5.AndroidJUnit5Builder" + testInstrumentationRunnerArguments["configurationParameters"] = + "junit.jupiter.execution.parallel.enabled=true,junit.jupiter.execution.parallel.mode.default=concurrent" + + buildFeatures { + buildConfig = true + } + + buildConfigField("boolean", "MY_VALUE", "true") + + testOptions { + animationsDisabled = true + } } - } - - // Add Kotlin source directory to all source sets - sourceSets.forEach { - it.java.srcDir("src/${it.name}/kotlin") - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - } } junitPlatform { - // Configure JUnit 5 tests here - filters("debug") { - excludeTags("slow") - } - - // Using local dependency instead of Maven coordinates - instrumentationTests.enabled = false -} + // Configure JUnit 5 tests here + filters("debug") { + excludeTags("slow") + } -tasks.withType { - testLogging.events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + // Using local dependency instead of Maven coordinates + instrumentationTests.enabled = false } dependencies { - implementation(libs.kotlinStdLib) + implementation(libs.kotlin.stdlib) - testImplementation(libs.junitJupiterApi) - testImplementation(libs.junitJupiterParams) - testRuntimeOnly(libs.junitJupiterEngine) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testRuntimeOnly(libs.junit.jupiter.engine) - androidTestImplementation(libs.junit4) - androidTestImplementation(libs.androidXTestRunner) + androidTestImplementation(libs.junit.vintage.api) + androidTestImplementation(libs.androidx.test.runner) - // Android Instrumentation Tests wth JUnit 5 - androidTestImplementation(libs.junitJupiterApi) - androidTestImplementation(libs.junitJupiterParams) - androidTestImplementation(libs.espressoCore) - androidTestImplementation(project(":core")) - androidTestRuntimeOnly(project(":runner")) + // Android Instrumentation Tests wth JUnit 5 + androidTestImplementation(libs.junit.jupiter.api) + androidTestImplementation(libs.junit.jupiter.params) + androidTestImplementation(libs.espresso) + androidTestImplementation(project(":core")) + androidTestRuntimeOnly(project(":runner")) } diff --git a/instrumentation/settings.gradle.kts b/instrumentation/settings.gradle.kts index a7929044..0211ad49 100644 --- a/instrumentation/settings.gradle.kts +++ b/instrumentation/settings.gradle.kts @@ -35,4 +35,10 @@ dependencyResolutionManagement { mavenContent { snapshotsOnly() } } } + + versionCatalogs { + create("libs") { + from(files("../build-logic/gradle/libs.versions.toml")) + } + } } diff --git a/instrumentation/testutil-reflect/build.gradle.kts b/instrumentation/testutil-reflect/build.gradle.kts index 444baaa3..010aecd3 100644 --- a/instrumentation/testutil-reflect/build.gradle.kts +++ b/instrumentation/testutil-reflect/build.gradle.kts @@ -1,3 +1,3 @@ plugins { - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) } diff --git a/instrumentation/testutil/build.gradle.kts b/instrumentation/testutil/build.gradle.kts index d2172ada..b3453ad6 100644 --- a/instrumentation/testutil/build.gradle.kts +++ b/instrumentation/testutil/build.gradle.kts @@ -1,75 +1,27 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { - id("com.android.library") - kotlin("android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } -val javaVersion = JavaVersion.VERSION_11 - android { - namespace = "de.mannodermaus.junit5.testutil" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = 19 - multiDexEnabled = true - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - buildFeatures { - buildConfig = false - resValues = false - } - - lint { - // JUnit 4 refers to java.lang.management APIs, which are absent on Android. - warning.add("InvalidPackage") - targetSdk = Android.targetSdkVersion - } + namespace = "de.mannodermaus.junit5.testutil" - packaging { - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } -} - -kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) - } -} - -tasks.withType { - failFast = true - testLogging { - events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) - exceptionFormat = TestExceptionFormat.FULL - } + defaultConfig { + minSdk = 19 + multiDexEnabled = true + } } dependencies { - implementation(project(":testutil-reflect")) - implementation(libs.androidXMultidex) - - api(libs.androidXTestMonitor) - api(libs.truth) - api(libs.truthJava8Extensions) - api(libs.mockitoCore) - api(libs.mockitoKotlin) - api(libs.junitJupiterApi) - api(libs.junitJupiterParams) - api(libs.junitPlatformRunner) + implementation(project(":testutil-reflect")) + implementation(libs.androidx.multidex) + + api(libs.androidx.test.monitor) + api(libs.truth.core) + api(libs.truth.extensions) + api(libs.mockito.core) + api(libs.mockito.kotlin) + api(libs.junit.jupiter.api) + api(libs.junit.jupiter.params) + api(libs.junit.platform.runner) } From 0e0dc13e8955f55716c4e9188a3244976304d9ab Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sat, 20 Dec 2025 18:43:01 +0100 Subject: [PATCH 02/22] Add variants (JU5 & 6) to instrumentation modules --- build-logic/src/main/kotlin/Environment.kt | 8 +++ .../kotlin/extensions/StringExtensions.kt | 6 ++ .../extensions/VersionCatalogExtensions.kt | 2 +- instrumentation/build.gradle.kts | 56 ++++++++++++++++++- .../junit5/testutil/AndroidBuildUtils.kt | 30 +++++----- .../junit5/testutil/CollectingRunListener.kt | 7 +-- 6 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 build-logic/src/main/kotlin/extensions/StringExtensions.kt diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt index 1d8092a6..97819d67 100644 --- a/build-logic/src/main/kotlin/Environment.kt +++ b/build-logic/src/main/kotlin/Environment.kt @@ -4,6 +4,14 @@ import org.gradle.api.Project import java.io.File import java.util.Properties +enum class SupportedJUnit( + val label: String, + val minSdk: Int +) { + JUnit5(label = "five", minSdk = 26), + JUnit6(label = "six", minSdk = 35) +} + enum class SupportedAgp( val version: String, val gradle: String, diff --git a/build-logic/src/main/kotlin/extensions/StringExtensions.kt b/build-logic/src/main/kotlin/extensions/StringExtensions.kt new file mode 100644 index 00000000..40e49b19 --- /dev/null +++ b/build-logic/src/main/kotlin/extensions/StringExtensions.kt @@ -0,0 +1,6 @@ +package extensions + +import java.util.Locale.getDefault + +fun String.capitalized(): String = + replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() } diff --git a/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt b/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt index 1ae3af11..03497779 100644 --- a/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt +++ b/build-logic/src/main/kotlin/extensions/VersionCatalogExtensions.kt @@ -8,7 +8,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.getByType -internal val Project.libs: VersionCatalog +val Project.libs: VersionCatalog get() = extensions.getByType().named("extensions.libs") fun VersionCatalog.agp(version: SupportedAgp): String { diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index 9262e326..ad83e1ac 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.api.dsl.LibraryExtension import com.android.build.gradle.BaseExtension +import extensions.capitalized import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -32,9 +33,9 @@ apiValidation { subprojects { apply(plugin = "explicit-api-mode") - val jvmTarget = JvmTarget.JVM_17 - val javaVersion = JavaVersion.VERSION_17 - + val jvmTarget = JvmTarget.JVM_21 + val javaVersion = JavaVersion.toVersion(jvmTarget.target) + // Configure Kotlin plugins.withType { tasks.withType>().configureEach { @@ -48,6 +49,13 @@ subprojects { } } + // Configure Java + plugins.withId("java") { + configure { + toolchain { languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) } + } + } + // Configure Android plugins.withId("com.android.base") { configure { @@ -71,6 +79,48 @@ subprojects { unitTests.isReturnDefaultValues = true } + // Create product flavors for each supported generation of JUnit, + // then declare the corresponding BOM for each of them + // to provide dependencies to each target + val supportedTargets = SupportedJUnit.values() + + flavorDimensions("target") + productFlavors { + supportedTargets.forEachIndexed { index, junit -> + register(junit.label) { + dimension = "target" + isDefault = index == supportedTargets.lastIndex + } + } + } + + dependencies { + supportedTargets.forEach { junit -> + val configNames = listOf( + "${junit.label}Api", + "${junit.label}Implementation", + "${junit.label}CompileOnly", + "${junit.label}RuntimeOnly", + "test${junit.label.capitalized()}Implementation", + "test${junit.label.capitalized()}CompileOnly", + "test${junit.label.capitalized()}RuntimeOnly", + "androidTest${junit.label.capitalized()}Implementation", + "androidTest${junit.label.capitalized()}CompileOnly", + "androidTest${junit.label.capitalized()}RuntimeOnly", + ) + + configNames.forEach { configName -> + add( + configurationName = configName, + dependencyNotation = when (junit) { + SupportedJUnit.JUnit5 -> platform(libs.junit.framework.bom5) + SupportedJUnit.JUnit6 -> platform(libs.junit.framework.bom6) + } + ) + } + } + } + if (this is LibraryExtension) { lint { // JUnit 4 refers to java.lang.management APIs, which are absent on Android. diff --git a/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/AndroidBuildUtils.kt b/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/AndroidBuildUtils.kt index 1def9467..6199a767 100644 --- a/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/AndroidBuildUtils.kt +++ b/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/AndroidBuildUtils.kt @@ -7,21 +7,25 @@ import com.google.common.truth.Truth.assertThat import de.mannodermaus.junit5.testutil.reflect.getFieldReflectively import de.mannodermaus.junit5.testutil.reflect.setStaticValue -object AndroidBuildUtils { - - fun withApiLevel(api: Int, block: () -> Unit) = withMockedStaticField( - fieldName = "SDK_INT", - value = api, - block = block, - ) +public object AndroidBuildUtils { + + public fun withApiLevel(api: Int, block: () -> Unit) { + withMockedStaticField( + fieldName = "SDK_INT", + value = api, + block = block, + ) + } - fun withManufacturer(name: String, block: () -> Unit) = withMockedStaticField( - fieldName = "MANUFACTURER", - value = name, - block = block, - ) + public fun withManufacturer(name: String, block: () -> Unit) { + withMockedStaticField( + fieldName = "MANUFACTURER", + value = name, + block = block, + ) + } - fun withMockedInstrumentation(arguments: Bundle = Bundle(), block: () -> Unit) { + public fun withMockedInstrumentation(arguments: Bundle = Bundle(), block: () -> Unit) { val (oldInstrumentation, oldArguments) = try { InstrumentationRegistry.getInstrumentation() to InstrumentationRegistry.getArguments() } catch (ignored: Throwable) { diff --git a/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/CollectingRunListener.kt b/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/CollectingRunListener.kt index 7d549a2f..86918791 100644 --- a/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/CollectingRunListener.kt +++ b/instrumentation/testutil/src/main/kotlin/de/mannodermaus/junit5/testutil/CollectingRunListener.kt @@ -7,9 +7,8 @@ import org.junit.runner.notification.RunListener /** * A JUnit 4 [RunListener] that collects information about executed and failed tests. */ -class CollectingRunListener : RunListener() { - - data class Results( +public class CollectingRunListener : RunListener() { + public data class Results( val successfulTests: List, val failedTests: List, val ignoredTests: List @@ -31,5 +30,5 @@ class CollectingRunListener : RunListener() { ignored += description } - fun getResults() = Results(success, failures, ignored) + public fun getResults(): Results = Results(success, failures, ignored) } From 4d57be0055cf206bb1ca18e38e08233bb15f39fa Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sat, 20 Dec 2025 19:11:59 +0100 Subject: [PATCH 03/22] Implement JU5&6 backbones for instrumentation modules A few bridge APIs were needed to overcome incompatibilities between the versions --- build-logic/gradle/libs.versions.toml | 2 +- instrumentation/build.gradle.kts | 3 +-- instrumentation/core/build.gradle.kts | 3 ++- .../internal/compat/ExtensionContextCompat.kt | 18 +++++++++++++++ .../junit5/ActivityScenarioExtension.kt | 11 +++++----- .../internal/compat/ExtensionContextCompat.kt | 18 +++++++++++++++ .../extensions/GrantPermissionExtension.kt | 2 +- .../GrantPermissionExtensionTests.kt | 6 ++++- instrumentation/runner/build.gradle.kts | 3 ++- .../discovery/EmptyConfigurationParameters.kt | 19 ++++++++++++++++ .../internal/runners/TestPlanAdapter.kt | 22 +++++++++++++++++++ .../internal/discovery/EmptyTestPlan.kt | 20 ++++------------- .../internal/formatters/TestNameFormatter.kt | 1 + .../AndroidJUnitPlatformRunnerListener.kt | 2 +- .../runners/AndroidJUnitPlatformTestTree.kt | 20 +++++------------ .../discovery/EmptyConfigurationParameters.kt | 15 +++++++++++++ .../internal/runners/TestPlanAdapter.kt | 14 ++++++++++++ .../mannodermaus/junit5/sample/ActivityOne.kt | 6 ++--- instrumentation/testutil/build.gradle.kts | 3 ++- 19 files changed, 140 insertions(+), 48 deletions(-) create mode 100644 instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt create mode 100644 instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt create mode 100644 instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt create mode 100644 instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt create mode 100644 instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt create mode 100644 instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt diff --git a/build-logic/gradle/libs.versions.toml b/build-logic/gradle/libs.versions.toml index ce86aa8e..29cf7bfe 100644 --- a/build-logic/gradle/libs.versions.toml +++ b/build-logic/gradle/libs.versions.toml @@ -66,7 +66,7 @@ junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } junit-platform-commons = { module = "org.junit.platform:junit-platform-commons" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } -junit-platform-runner = { module = "org.junit.platform:junit-platform-runner" } +junit-platform-suiteapi = { module = "org.junit.platform:junit-platform-suite-api" } junit-vintage-api = { module = "junit:junit", version.ref = "junit4" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" } konftoml = { module = "com.uchuhimo:konf-toml", version.ref = "konftoml" } diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index ad83e1ac..0c56a5b6 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -35,7 +35,7 @@ subprojects { val jvmTarget = JvmTarget.JVM_21 val javaVersion = JavaVersion.toVersion(jvmTarget.target) - + // Configure Kotlin plugins.withType { tasks.withType>().configureEach { @@ -43,7 +43,6 @@ subprojects { this.progressiveMode.set(true) if (this is KotlinJvmCompilerOptions) { this.jvmTarget.set(jvmTarget) - this.freeCompilerArgs.add("-Xjvm-default=all") } } } diff --git a/instrumentation/core/build.gradle.kts b/instrumentation/core/build.gradle.kts index 874f483c..18737a9b 100644 --- a/instrumentation/core/build.gradle.kts +++ b/instrumentation/core/build.gradle.kts @@ -41,7 +41,8 @@ dependencies { // This is required by the "instrumentation-runner" companion library, // since it can't provide any JUnit 5 runtime libraries itself // due to fear of prematurely incrementing the minSdkVersion requirement. - runtimeOnly(libs.junit.platform.runner) + runtimeOnly(libs.junit.platform.launcher) + runtimeOnly(libs.junit.platform.suiteapi) runtimeOnly(libs.junit.jupiter.engine) // This transitive dependency of JUnit 5 is required to be on the runtime classpath, diff --git a/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt b/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt new file mode 100644 index 00000000..98bd1096 --- /dev/null +++ b/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt @@ -0,0 +1,18 @@ +package de.mannodermaus.junit5.internal.compat + +import org.junit.jupiter.api.extension.ExtensionContext +import kotlin.reflect.KClass + +// JUnit 5 facade of ExtensionContext.Store APIs +// that were deprecated/removed in subsequent versions of the framework. + +internal fun ExtensionContext.Store.computeIfAbsentCompat( + key: K, + defaultCreator: (K) -> V +): Any = getOrComputeIfAbsent(key, defaultCreator) + +internal fun ExtensionContext.Store.computeIfAbsentCompat( + key: K, + defaultCreator: (K) -> V, + requiredType: KClass +): V = getOrComputeIfAbsent(key, defaultCreator, requiredType.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt index 43b06212..83230c2e 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt @@ -8,6 +8,7 @@ import android.util.Log import androidx.test.core.app.ActivityScenario import de.mannodermaus.junit5.ActivityScenarioExtension.Companion.launch import de.mannodermaus.junit5.internal.LOG_TAG +import de.mannodermaus.junit5.internal.compat.computeIfAbsentCompat import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext @@ -209,10 +210,10 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario) : B // Create a global lock for restricting test execution to one-by-one; // this is necessary to ensure that only one ActivityScenario is ever active at a time, // preventing violations of Android's instrumentation and Espresso - val lock = store.getOrComputeIfAbsent( - /* key = */ LOCK_KEY, - /* defaultCreator = */ { ReentrantLock() }, - /* requiredType = */ ReentrantLock::class.java, + val lock = store.computeIfAbsentCompat( + key = LOCK_KEY, + defaultCreator = { ReentrantLock() }, + requiredType = ReentrantLock::class, ) if (state) { @@ -223,7 +224,7 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario) : B } private fun logConcurrentExecutionWarningOnce(store: ExtensionContext.Store) { - store.getOrComputeIfAbsent(WARNING_KEY) { + store.computeIfAbsentCompat(WARNING_KEY) { setOf( " [WARNING!] UI tests using ActivityScenarioExtension should not be executed in CONCURRENT mode.", " We will try to disable parallelism for Espresso tests, but this may be error-prone", diff --git a/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt b/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt new file mode 100644 index 00000000..2c1c9983 --- /dev/null +++ b/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/internal/compat/ExtensionContextCompat.kt @@ -0,0 +1,18 @@ +package de.mannodermaus.junit5.internal.compat + +import org.junit.jupiter.api.extension.ExtensionContext +import kotlin.reflect.KClass + +// JUnit 6 facade of ExtensionContext.Store APIs +// that didn't exist in previous versions of the framework. + +internal fun ExtensionContext.Store.computeIfAbsentCompat( + key: K, + defaultCreator: (K) -> V +): Any = computeIfAbsent(key, defaultCreator) + +internal fun ExtensionContext.Store.computeIfAbsentCompat( + key: K, + defaultCreator: (K) -> V, + requiredType: KClass +): V = computeIfAbsent(key, defaultCreator, requiredType.java) diff --git a/instrumentation/extensions/src/main/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtension.kt b/instrumentation/extensions/src/main/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtension.kt index 057ea63c..bb834a76 100644 --- a/instrumentation/extensions/src/main/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtension.kt +++ b/instrumentation/extensions/src/main/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtension.kt @@ -66,7 +66,7 @@ internal constructor(private val permissionGranter: PermissionGranter) : BeforeE /* BeforeEachCallback */ - override fun beforeEach(context: ExtensionContext?) { + override fun beforeEach(context: ExtensionContext) { permissionGranter.requestPermissions() } } diff --git a/instrumentation/extensions/src/test/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtensionTests.kt b/instrumentation/extensions/src/test/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtensionTests.kt index f1a264c3..a7b42c74 100644 --- a/instrumentation/extensions/src/test/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtensionTests.kt +++ b/instrumentation/extensions/src/test/kotlin/de/mannodermaus/junit5/extensions/GrantPermissionExtensionTests.kt @@ -9,6 +9,8 @@ import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.extension.ExtensionContext +import org.mockito.Mockito.mock import java.lang.reflect.Modifier class GrantPermissionExtensionTests { @@ -81,8 +83,10 @@ class GrantPermissionExtensionTests { private fun runExtension(vararg permissions: String) { val extension = GrantPermissionExtension(granter) + val context = mock() + extension.grantPermissions(permissions) - extension.beforeEach(null) + extension.beforeEach(context) } private class TestPermissionGranter : PermissionGranter { diff --git a/instrumentation/runner/build.gradle.kts b/instrumentation/runner/build.gradle.kts index 23106ae9..96699131 100644 --- a/instrumentation/runner/build.gradle.kts +++ b/instrumentation/runner/build.gradle.kts @@ -34,7 +34,8 @@ dependencies { // by the "instrumentation" companion library instead. compileOnly(libs.junit.jupiter.api) compileOnly(libs.junit.jupiter.params) - compileOnly(libs.junit.platform.runner) + compileOnly(libs.junit.platform.launcher) + compileOnly(libs.junit.platform.suiteapi) testImplementation(project(":testutil")) testImplementation(libs.robolectric) diff --git a/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt new file mode 100644 index 00000000..e4a3b731 --- /dev/null +++ b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt @@ -0,0 +1,19 @@ +package de.mannodermaus.junit5.internal.discovery + +import androidx.annotation.RequiresApi +import org.junit.platform.engine.ConfigurationParameters +import java.util.Optional + +/** + * JUnit 5 version of the [ConfigurationParameters] interface, + * including the deprecated APIs that were removed in subsequent versions of the framework. + */ +@RequiresApi(26) +internal object EmptyConfigurationParameters : ConfigurationParameters { + override fun get(key: String) = Optional.empty() + override fun getBoolean(key: String) = Optional.empty() + override fun keySet() = emptySet() + + @Deprecated("Deprecated in Java", ReplaceWith("keySet().size")) + override fun size() = 0 +} diff --git a/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt new file mode 100644 index 00000000..d56eeee1 --- /dev/null +++ b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt @@ -0,0 +1,22 @@ +package de.mannodermaus.junit5.internal.runners + +import org.junit.platform.launcher.TestIdentifier +import org.junit.platform.launcher.TestPlan + +/** + * JUnit 5 version of the [TestPlanAdapter], + * including the deprecated APIs that were removed in subsequent versions of the framework. + */ +internal open class TestPlanAdapter( + val delegate: TestPlan +) : TestPlan( + /* containsTests = */ delegate.containsTests(), + /* configurationParameters = */ delegate.configurationParameters, + /* outputDirectoryCreator = */ delegate.outputDirectoryCreator +) { + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun getChildren(parentId: String): Set { + return delegate.getChildren(parentId) + } +} diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyTestPlan.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyTestPlan.kt index 904cb4f6..f4611d80 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyTestPlan.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyTestPlan.kt @@ -1,12 +1,10 @@ package de.mannodermaus.junit5.internal.discovery import androidx.annotation.RequiresApi -import org.junit.platform.engine.ConfigurationParameters import org.junit.platform.engine.OutputDirectoryCreator import org.junit.platform.engine.TestDescriptor import org.junit.platform.launcher.TestPlan import java.io.File -import java.util.Optional /** * A JUnit TestPlan that does absolutely nothing. @@ -15,24 +13,14 @@ import java.util.Optional */ @RequiresApi(26) internal object EmptyTestPlan : TestPlan( - false, - emptyConfigurationParameters, - emptyOutputDirectoryCreator + /* containsTests = */ false, + /* configurationParameters = */ EmptyConfigurationParameters, + /* outputDirectoryCreator = */ emptyOutputDirectoryCreator ) -@RequiresApi(26) -private val emptyConfigurationParameters = object : ConfigurationParameters { - override fun get(key: String?) = Optional.empty() - override fun getBoolean(key: String?) = Optional.empty() - override fun keySet() = emptySet() - - @Deprecated("Deprecated in Java", ReplaceWith("keySet().size")) - override fun size() = 0 -} - @RequiresApi(26) private val emptyOutputDirectoryCreator = object : OutputDirectoryCreator { private val path = File.createTempFile("empty-output", ".nop").toPath() override fun getRootDirectory() = path - override fun createOutputDirectory(testDescriptor: TestDescriptor?) = path + override fun createOutputDirectory(testDescriptor: TestDescriptor) = path } diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/formatters/TestNameFormatter.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/formatters/TestNameFormatter.kt index 99c76ab8..40acf8e9 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/formatters/TestNameFormatter.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/formatters/TestNameFormatter.kt @@ -46,5 +46,6 @@ internal object TestNameFormatter { .replace("()", "") .replace('(', '[') .replace(')', ']') + .replace("\"", "") } } diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformRunnerListener.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformRunnerListener.kt index c5197d3d..038bee42 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformRunnerListener.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformRunnerListener.kt @@ -22,7 +22,7 @@ internal class AndroidJUnitPlatformRunnerListener( private val notifier: RunNotifier ) : TestExecutionListener { - override fun reportingEntryPublished(testIdentifier: TestIdentifier?, entry: ReportEntry?) { + override fun reportingEntryPublished(testIdentifier: TestIdentifier, entry: ReportEntry) { // No-op, but must be declared to avoid AbstractMethodError } diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformTestTree.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformTestTree.kt index c77a8145..9b7c703e 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformTestTree.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitPlatformTestTree.kt @@ -160,7 +160,7 @@ internal class AndroidJUnitPlatformTestTree( return source.javaClass.name } else if (source is MethodSource) { - val methodParameterTypes = source.methodParameterTypes + val methodParameterTypes = source.methodParameterTypes.orEmpty() return if (methodParameterTypes.isBlank()) { source.methodName } else { @@ -180,14 +180,9 @@ internal class AndroidJUnitPlatformTestTree( /** * Custom drop-in TestPlan for Android purposes. */ - private class ModifiedTestPlan(val delegate: TestPlan) : - TestPlan( - /* containsTests = */ delegate.containsTests(), - /* configurationParameters = */ delegate.configurationParameters, - /* outputDirectoryCreator = */ delegate.outputDirectoryCreator - ) { - - fun getRealParent(child: TestIdentifier?): Optional { + private class ModifiedTestPlan(delegate: TestPlan) : TestPlanAdapter(delegate) { + + fun getRealParent(child: TestIdentifier): Optional { // Because the overridden "getParent()" from the superclass is modified, // expose this additional method to access the actual parent identifier of the given child. // This is needed when composing the display name of a dynamic test. @@ -216,7 +211,7 @@ internal class AndroidJUnitPlatformTestTree( /* Unchanged */ - override fun addInternal(testIdentifier: TestIdentifier?) { + override fun addInternal(testIdentifier: TestIdentifier) { delegate.addInternal(testIdentifier) } @@ -232,11 +227,6 @@ internal class AndroidJUnitPlatformTestTree( return delegate.getChildren(parentId) } - @Suppress("OVERRIDE_DEPRECATION") - override fun getChildren(parentId: String): Set { - return delegate.getChildren(parentId) - } - override fun getTestIdentifier(uniqueId: UniqueId): TestIdentifier { return delegate.getTestIdentifier(uniqueId) } diff --git a/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt new file mode 100644 index 00000000..db7dbf3b --- /dev/null +++ b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyConfigurationParameters.kt @@ -0,0 +1,15 @@ +package de.mannodermaus.junit5.internal.discovery + +import androidx.annotation.RequiresApi +import org.junit.platform.engine.ConfigurationParameters +import java.util.Optional + +/** + * JUnit 6 version of the [ConfigurationParameters] interface. + */ +@RequiresApi(26) +internal object EmptyConfigurationParameters : ConfigurationParameters { + override fun get(key: String) = Optional.empty() + override fun getBoolean(key: String) = Optional.empty() + override fun keySet() = emptySet() +} diff --git a/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt new file mode 100644 index 00000000..837778a8 --- /dev/null +++ b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/runners/TestPlanAdapter.kt @@ -0,0 +1,14 @@ +package de.mannodermaus.junit5.internal.runners + +import org.junit.platform.launcher.TestPlan + +/** + * JUnit 6 version of the [TestPlanAdapter]. + */ +internal open class TestPlanAdapter( + val delegate: TestPlan +) : TestPlan( + /* containsTests = */ delegate.containsTests(), + /* configurationParameters = */ delegate.configurationParameters, + /* outputDirectoryCreator = */ delegate.outputDirectoryCreator +) diff --git a/instrumentation/sample/src/main/kotlin/de/mannodermaus/junit5/sample/ActivityOne.kt b/instrumentation/sample/src/main/kotlin/de/mannodermaus/junit5/sample/ActivityOne.kt index c768a914..1e797793 100644 --- a/instrumentation/sample/src/main/kotlin/de/mannodermaus/junit5/sample/ActivityOne.kt +++ b/instrumentation/sample/src/main/kotlin/de/mannodermaus/junit5/sample/ActivityOne.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.widget.Button import android.widget.TextView -class ActivityOne : Activity() { +public class ActivityOne : Activity() { private val textView by lazy { findViewById(R.id.textView) } private val button by lazy { findViewById