diff --git a/.circleci/config.yml b/.circleci/config.yml index fbcd6d03..4e0f696d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,12 +3,12 @@ defaults: &defaults working_directory: ~/root/project resource_class: large docker: - - image: cimg/android:2023.04.1 + - image: cimg/android:2025.12.1 environment: GRADLE_OPTS: -Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.daemon=false -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false cache_key: &cache_key - key: jars-{{ checksum "build-logic/build.gradle.kts" }}-{{ checksum "plugin/build.gradle.kts" }}-{{ checksum "plugin/android-junit5/build.gradle.kts" }}-{{ checksum "plugin/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "instrumentation/build.gradle.kts" }}-{{ checksum "instrumentation/core/build.gradle.kts" }}-{{ checksum "instrumentation/compose/build.gradle.kts" }}-{{ checksum "instrumentation/extensions/build.gradle.kts" }}-{{ checksum "instrumentation/runner/build.gradle.kts" }}-{{ checksum "instrumentation/sample/build.gradle.kts" }}-{{ checksum "instrumentation/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "build-logic/src/main/kotlin/Environment.kt" }}-{{ checksum "build-logic/src/main/kotlin/Dependencies.kt" }} + key: jars-{{ checksum "build-logic/build.gradle.kts" }}-{{ checksum "plugin/build.gradle.kts" }}-{{ checksum "plugin/android-junit5/build.gradle.kts" }}-{{ checksum "plugin/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "instrumentation/build.gradle.kts" }}-{{ checksum "instrumentation/core/build.gradle.kts" }}-{{ checksum "instrumentation/compose/build.gradle.kts" }}-{{ checksum "instrumentation/extensions/build.gradle.kts" }}-{{ checksum "instrumentation/runner/build.gradle.kts" }}-{{ checksum "instrumentation/sample/build.gradle.kts" }}-{{ checksum "instrumentation/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "build-logic/src/main/kotlin/Environment.kt" }}-{{ checksum "build-logic/gradle/libs.versions.toml" }} commands: construct_signing_key: @@ -35,17 +35,10 @@ jobs: command: cd instrumentation && ./gradlew androidDependencies - run: name: (Instrumentation) Build - command: | - cd instrumentation - ./gradlew assembleRelease :core:assembleDebug \ - :core:assembleDebugAndroidTest \ - :compose:assembleDebugAndroidTest \ - :extensions:assembleDebug \ - :runner:assembleDebug \ - :sample:assembleDebug --stacktrace + command: cd instrumentation && ./gradlew assemble :core:assembleAndroidTest --stacktrace - run: name: (Instrumentation) Test - command: cd instrumentation && ./gradlew :core:check :extensions:check :runner:check :compose:check --stacktrace + command: cd instrumentation && ./gradlew check --stacktrace - save_cache: <<: *cache_key paths: @@ -60,18 +53,30 @@ jobs: sudo gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json sudo gcloud --quiet config set project ${GOOGLE_PROJECT_ID} - run: - name: Test with Firebase Test Lab + name: Test with Firebase Test Lab (JUnit 5) command: > sudo gcloud firebase test android run \ --type instrumentation \ - --app instrumentation/sample/build/outputs/apk/debug/sample-debug.apk \ - --test instrumentation/core/build/outputs/apk/androidTest/debug/core-debug-androidTest.apk \ - --environment-variables runnerBuilder=de.mannodermaus.junit5.AndroidJUnit5Builder \ + --app instrumentation/sample/build/outputs/apk/five/debug/sample-five-debug.apk \ + --test instrumentation/core/build/outputs/apk/androidTest/five/debug/core-five-debug-androidTest.apk \ + --environment-variables runnerBuilder=de.mannodermaus.junit5.AndroidJUnitFrameworkBuilder \ --test-runner-class androidx.test.runner.AndroidJUnitRunner \ --device model=redfin,version=30,locale=en_US,orientation=portrait \ --device model=oriole,version=33,locale=en_US,orientation=portrait \ --results-bucket cloud-test-${GOOGLE_PROJECT_ID} \ --timeout 15m + - run: + name: Test with Firebase Test Lab (JUnit 6) + command: > + sudo gcloud firebase test android run \ + --type instrumentation \ + --app instrumentation/sample/build/outputs/apk/six/debug/sample-six-debug.apk \ + --test instrumentation/core/build/outputs/apk/androidTest/six/debug/core-six-debug-androidTest.apk \ + --environment-variables runnerBuilder=de.mannodermaus.junit5.AndroidJUnitFrameworkBuilder \ + --test-runner-class androidx.test.runner.AndroidJUnitRunner \ + --device model=pa3q,version=35,locale=en_US,orientation=portrait \ + --results-bucket cloud-test-${GOOGLE_PROJECT_ID} \ + --timeout 15m - run: name: Install gsutil dependency and copy test results data command: | @@ -112,12 +117,12 @@ jobs: name: (Instrumentation) Build & Deploy command: | cd instrumentation - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository --stacktrace + ./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseStagingRepositories --stacktrace - run: name: (Plugin) Build & Deploy command: | cd plugin - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository --stacktrace + ./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseStagingRepositories --stacktrace - store_artifacts: path: plugin/android-junit5/build/publications destination: plugin/publications/snapshots diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 3deb449c..64118725 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -8,5 +8,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + - uses: gradle/actions/wrapper-validation@bfd569614358980afc8f89c2730eee75bb97efdf # v5 diff --git a/.images/logo-inverted.svg b/.images/logo-inverted.svg new file mode 100644 index 00000000..0507192a --- /dev/null +++ b/.images/logo-inverted.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.images/logo.af b/.images/logo.af new file mode 100644 index 00000000..fa8b1bf5 Binary files /dev/null and b/.images/logo.af differ diff --git a/.images/logo.png b/.images/logo.png deleted file mode 100644 index 1940fc21..00000000 Binary files a/.images/logo.png and /dev/null differ diff --git a/.images/logo.psd b/.images/logo.psd deleted file mode 100644 index abf3b98a..00000000 Binary files a/.images/logo.psd and /dev/null differ diff --git a/.images/logo.svg b/.images/logo.svg new file mode 100644 index 00000000..b01aa2a8 --- /dev/null +++ b/.images/logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index f8640716..15e2e670 100644 --- a/README.md +++ b/README.md @@ -3,237 +3,131 @@ To update the content of this README, please apply modifications to `README.md.template` instead, and run the `generateReadme` task from Gradle. --> -# android-junit5 [![CircleCI](https://circleci.com/gh/mannodermaus/android-junit5/tree/main.svg?style=svg)][circleci] +

+ + + + Android JUnit + +

-A Gradle plugin that allows for the execution of [JUnit 5][junit5gh] tests in Android environments using **Android Gradle Plugin 8.2 or later.** +[![CircleCI](https://circleci.com/gh/mannodermaus/android-junit5/tree/main.svg?style=svg)][circleci] + +A Gradle plugin that allows for the execution of [JUnit Framework][junit-framework-github] tests in Android environments using **Android Gradle Plugin 8.2 or later.** ## How? -This plugin configures the unit test tasks for each build variant of a project to run on the JUnit Platform. Furthermore, it provides additional configuration options for these tests [through a DSL][wiki-dsl] and facilitates the usage of JUnit 5 for instrumentation tests. +This plugin configures the unit test tasks for each build variant of a project to run on the JUnit Platform. +Furthermore, it provides additional configuration options for these tests [through a DSL][wiki-dsl] +and facilitates the usage of JUnit Framework for instrumentation tests. -Instructions on how to write tests with the JUnit 5 framework can be found [in their User Guide][junit5ug]. To get a first look at its features, a small showcase project can be found [here][sampletests]. +Instructions on how to write tests with the JUnit Framework can be found [in their User Guide][junit-framework-guide]. +To get a first look at its features, a small showcase project can be found [here][sampletests]. ## Setup -To get started, declare the plugin in your `app` module's build script alongside the latest version. Snapshots of the development version are available through [Sonatype's `snapshots` repository][sonatyperepo]. - -
- Kotlin +To get started, declare the plugin in your `app` module's build script alongside the latest version. +Snapshots of the development version are available through [Sonatype's `snapshots` repository][sonatyperepo]. - ```kotlin - plugins { +```kotlin +plugins { + // 1. Apply the plugin id("de.mannodermaus.android-junit5") version "1.14.0.0" - } +} - dependencies { - // (Required) Writing and executing Unit Tests on the JUnit Platform - testImplementation("org.junit.jupiter:junit-jupiter-api:5.14.0") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.14.0") +dependencies { + // 2. Add JUnit Framework BOM and the required dependencies + testImplementation(platform("org.junit:junit-bom:5.14.1")) + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") - // (Optional) If you need "Parameterized Tests" - testImplementation("org.junit.jupiter:junit-jupiter-params:5.14.0") - - // (Optional) If you also have JUnit 4-based tests + // 3. Add JUnit Vintage if you also have JUnit 4 tests (e.g. Robolectric) testImplementation("junit:junit:4.13.2") - testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.14.0") - } - ``` -
- -
- Groovy - - ```groovy - plugins { - id "de.mannodermaus.android-junit5" version "1.14.0.0" - } - - dependencies { - // (Required) Writing and executing Unit Tests on the JUnit Platform - testImplementation "org.junit.jupiter:junit-jupiter-api:5.14.0" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.14.0" - - // (Optional) If you need "Parameterized Tests" - testImplementation "org.junit.jupiter:junit-jupiter-params:5.14.0" - - // (Optional) If you also have JUnit 4-based tests - testImplementation "junit:junit:4.13.2" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.14.0" - } - ``` -
- -
- -### Alternative: Legacy DSL - -If you prefer to use the legacy way to declare the dependency instead, remove the `version()` block from above and declare the plugin in your _root project's build script_ like so: - -
- Kotlin - - ```kotlin - // In the root project's build.gradle.kts: - buildscript { - dependencies { - classpath("de.mannodermaus.gradle.plugins:android-junit5:1.14.0.0") - } - } - - // In the app module's build.gradle.kts: - plugins { - id("de.mannodermaus.android-junit5") - } - ``` -
- -
- Groovy - - ```groovy - // In the root project's build.gradle: - buildscript { - dependencies { - classpath "de.mannodermaus.gradle.plugins:android-junit5:1.14.0.0" - } - } - - // In the app module's build.gradle: - apply plugin: "de.mannodermaus.android-junit5" - ``` -
- -
+ testRuntimeOnly("org.junit.vintage:junit-vintage-engine") +} +``` More information on Getting Started can be found [on the wiki][wiki-gettingstarted]. ## Requirements -The latest version of this plugin requires: -* Android Gradle Plugin `8.2` or above -* Gradle `8.2` or above +The latest version of this plugin requires at least: +* Android Gradle Plugin `8.2` +* Gradle `8.2` ## Instrumentation Test Support -You can use JUnit 5 to run instrumentation tests on emulators and physical devices, too. Because the framework is built on Java 8 from the ground up, these instrumentation tests will only run on devices running Android 8.0 (API 26) or newer – older phones will skip the execution of these tests completely, marking them as "ignored". +You can also write instrumentation tests with new JUnit APIs and execute them on emulators and physical devices. +Depending on the Java requirements of the JUnit Framework version, these instrumentation tests will only run on devices +that meet these requirements, however. These tests are ignored and their execution will be skipped on older devices. -Before you can write instrumentation tests with JUnit Jupiter, make sure that your module is using the `androidx.test.runner.AndroidJUnitRunner` (or a subclass of it) as its `testInstrumentationRunner`. Then, simply add a dependency on `junit-jupiter-api` to the `androidTestImplementation` configuration in your build script and the plugin will automatically configure JUnit 5 tests for you: +- JUnit 5 requires Java 8 and is only supported by devices running Android 8.0 (API 26) or newer +- JUnit 6 requires Java 17 and is only supported by devices running Android 15 (API 35) or newer -
- Kotlin - - ```kotlin - dependencies { - androidTestImplementation("org.junit.jupiter:junit-jupiter-api:5.14.0") - } - ``` -
+Before you can write instrumentation tests with JUnit Jupiter, +make sure that your module is using the `androidx.test.runner.AndroidJUnitRunner` +(or a subclass of it) as its `testInstrumentationRunner`. Then, simply add a dependency on JUnit Jupiter API +to the `androidTestImplementation` configuration in your build script and the plugin will +automatically configure JUnit 5 tests for you: -
- Groovy - - ```groovy - dependencies { - androidTestImplementation "org.junit.jupiter:junit-jupiter-api:5.14.0" - } - ``` -
+```kotlin +dependencies { + androidTestImplementation(platform("org.junit:junit-bom:5.14.1")) + androidTestImplementation("org.junit.jupiter:junit-jupiter-api") +} +``` -By enabling JUnit 5 for instrumentation tests, you will gain access to `ActivityScenarioExtension` (among other things), which helps with the orchestration of `Activity` classes. Check [the wiki][wiki-home] for more info. +By enabling JUnit Framework for instrumentation tests, you will gain access to `ActivityScenarioExtension` among other things, +which helps with the orchestration of `Activity` classes. Check [the wiki][wiki-home] for more info. ### Extensions -An optional artifact with more helper extensions is available for specific use cases. -It contains the following APIs: +An optional artifact with `extensions` is available for specific use cases. It contains the following APIs: - `GrantPermissionExtension` for granting permissions before each test Can you think of more? Let's discuss in the issues section! -
- Kotlin - - ```kotlin - junitPlatform { +```kotlin +junitPlatform { instrumentationTests.includeExtensions.set(true) - } - ``` -
- -
- Groovy - - ```groovy - junitPlatform { - instrumentationTests.includeExtensions.set(true) - } - ``` -
+} +``` ### Jetpack Compose -To test `@Composable` functions on device with JUnit 5, first enable support for instrumentation tests as described above. -Then, add the Compose test dependency to your `androidTestImplementation` configuration -and the plugin will autoconfigure JUnit 5 Compose support for you! +To test `@Composable` functions on devices compatible with the JUnit Framework, +enable support for instrumentation tests as described above. Then add the Compose test dependency +to your `androidTestImplementation` configuration and the plugin will autoconfigure JUnit 5 Compose support for you. + +```kotlin +dependencies { + // Setup from the previous section for enabling instrumentation tests... -
- Kotlin - - ```kotlin - dependencies { // Compose test framework androidTestImplementation("androidx.compose.ui:ui-test-android:$compose_version") // Needed for createComposeExtension() and createAndroidComposeExtension() debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") - } - ``` -
- -
- Groovy - - ```groovy - dependencies { - // Compose test framework - androidTestImplementation "androidx.compose.ui:ui-test-android:$compose_version" - - // Needed for createComposeExtension() and createAndroidComposeExtension() - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" - } - ``` -
+} +``` -[The wiki][wiki-home] includes a section on how to test your Composables with JUnit 5. +[The wiki][wiki-home] includes a section on how to test your Composables. ### Override the version of instrumentation test libraries By default, the plugin will make sure to use a compatible version of the instrumentation test libraries when it sets up the artifacts automatically. However, it is possible to choose a custom version instead via its DSL: -
- Kotlin - - ```kotlin - junitPlatform { +junitPlatform { instrumentationTests.version.set("1.9.0") - } - ``` -
- -
- Groovy - - ```groovy - junitPlatform { - instrumentationTests.version.set("1.9.0") - } - ``` -
+} ## Official Support -At this time, Google hasn't shared any immediate plans to bring first-party support for JUnit 5 to Android. The following list is an aggregation of pending feature requests: +At this time, Google hasn't shared any immediate plans to bring first-party support for anything beyond JUnit 4 to Android. +The following list is an aggregation of pending feature requests: - [InstantTaskExecutorRule uses @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -- why? (issuetracker.google.com)](https://issuetracker.google.com/u/0/issues/79189568) - [Add support for JUnit 5 (issuetracker.google.com)](https://issuetracker.google.com/issues/127100532) @@ -241,49 +135,37 @@ At this time, Google hasn't shared any immediate plans to bring first-party supp ## Support for @Rules -Since JUnit 5 has replaced the `@Rule` mechanism with Extensions, the following artifacts help bridge the gap until Android officially transitions to JUnit 5. +Since the JUnit Framework has replaced the `@Rule` mechanism with the concept of an `Extension`, +the following artifacts help bridge the gap until Android officially transitions, if ever. ### InstantExecutorExtension Replaces `InstantTaskExecutorRule` in JUnit 5. -
- Kotlin - - ```kotlin - dependencies { - testImplementation("io.github.neboskreb:instant-task-executor-extension:1.0.0") - } - ``` -
- -
- Groovy - - ```groovy - dependencies { - testImplementation 'io.github.neboskreb:instant-task-executor-extension:1.0.0' - } - ``` -
+```kotlin +dependencies { + testImplementation("io.github.neboskreb:instant-task-executor-extension:1.0.0") +} +``` For more details see [instant-task-executor-extension](https://github.com/neboskreb/instant-task-executor-extension) on GitHub. ## Building Locally -This repository contains multiple modules, divided into two sub-projects. The repository's root directory contains build logic shared across the sub-projects, which in turn use symlinks to connect to the common build scripts in their parent folder. +This repository contains multiple modules, divided into two sub-projects. +The repository's root directory contains build logic shared across the sub-projects, +which in turn use symlinks to connect to the common build scripts in their parent folder. -- `instrumentation`: The root folder for Android-based modules, namely the instrumentation libraries & a sample application. After cloning, open this project in Android Studio. -- `plugin`: The root folder for Java-based modules, namely the Gradle plugin for JUnit 5 on Android, as well as its test module. After cloning, open this project in IntelliJ IDEA. +- `instrumentation`: The root folder for the instrumentation libraries & a sample. Open this folder in Android Studio. +- `plugin`: The root folder for the Gradle plugin. Open this folder in IntelliJ IDEA. ## Plugin Compatibility Map For users that cannot match the current minimum version requirement of the Android Gradle Plugin requested by this plugin, -refer to the table below to find a suitable alternative version. Note that **no active development will go into these -legacy versions**, so please consider upgrading to at least Android Gradle Plugin 8.2 -before filing an issue with the latest one. +refer to the table below to find a suitable alternative version. Note that **no active development will go into legacy versions**, +so please consider upgrading to at least AGP 8.2 before filing an issue with the latest one. -|Your AGP Version|Suggested JUnit5 Plugin Version| +|Your AGP Version|Suggested Plugin Version| |---|---| |`>= 8.2.0`|`1.14.0.0`| |`8.0.0` - `8.1.4`|`1.12.2.0`| @@ -312,8 +194,8 @@ limitations under the License. See also the [full License text](LICENSE). - [junit5gh]: https://github.com/junit-team/junit5 - [junit5ug]: https://junit.org/junit5/docs/current/user-guide + [junit-framework-github]: https://github.com/junit-team/junit-framework + [junit-framework-guide]: https://docs.junit.org [circleci]: https://circleci.com/gh/mannodermaus/android-junit5 [sonatyperepo]: https://central.sonatype.com/repository/maven-snapshots [sampletests]: instrumentation/sample diff --git a/README.md.template b/README.md.template index 46d1ac7f..fbec7198 100644 --- a/README.md.template +++ b/README.md.template @@ -1,234 +1,128 @@ -# android-junit5 [![CircleCI](https://circleci.com/gh/mannodermaus/android-junit5/tree/main.svg?style=svg)][circleci] +

+ + + + Android JUnit + +

-A Gradle plugin that allows for the execution of [JUnit 5][junit5gh] tests in Android environments using **Android Gradle Plugin ${constants.minimumRequiredAgpVersion} or later.** +[![CircleCI](https://circleci.com/gh/mannodermaus/android-junit5/tree/main.svg?style=svg)][circleci] + +A Gradle plugin that allows for the execution of [JUnit Framework][junit-framework-github] tests in Android environments using **Android Gradle Plugin ${constants.minimumRequiredAgpVersion} or later.** ## How? -This plugin configures the unit test tasks for each build variant of a project to run on the JUnit Platform. Furthermore, it provides additional configuration options for these tests [through a DSL][wiki-dsl] and facilitates the usage of JUnit 5 for instrumentation tests. +This plugin configures the unit test tasks for each build variant of a project to run on the JUnit Platform. +Furthermore, it provides additional configuration options for these tests [through a DSL][wiki-dsl] +and facilitates the usage of JUnit Framework for instrumentation tests. -Instructions on how to write tests with the JUnit 5 framework can be found [in their User Guide][junit5ug]. To get a first look at its features, a small showcase project can be found [here][sampletests]. +Instructions on how to write tests with the JUnit Framework can be found [in their User Guide][junit-framework-guide]. +To get a first look at its features, a small showcase project can be found [here][sampletests]. ## Setup -To get started, declare the plugin in your `app` module's build script alongside the latest version. Snapshots of the development version are available through [Sonatype's `snapshots` repository][sonatyperepo]. - -
- Kotlin +To get started, declare the plugin in your `app` module's build script alongside the latest version. +Snapshots of the development version are available through [Sonatype's `snapshots` repository][sonatyperepo]. - ```kotlin - plugins { +```kotlin +plugins { + // 1. Apply the plugin id("de.mannodermaus.android-junit5") version "${pluginVersion}" - } - - dependencies { - // (Required) Writing and executing Unit Tests on the JUnit Platform - testImplementation("${libs.junitJupiterApi}") - testRuntimeOnly("${libs.junitJupiterEngine}") - - // (Optional) If you need "Parameterized Tests" - testImplementation("${libs.junitJupiterParams}") - - // (Optional) If you also have JUnit 4-based tests - testImplementation("${libs.junit4}") - testRuntimeOnly("${libs.junitVintageEngine}") - } - ``` -
- -
- Groovy - - ```groovy - plugins { - id "de.mannodermaus.android-junit5" version "${pluginVersion}" - } - - dependencies { - // (Required) Writing and executing Unit Tests on the JUnit Platform - testImplementation "${libs.junitJupiterApi}" - testRuntimeOnly "${libs.junitJupiterEngine}" - - // (Optional) If you need "Parameterized Tests" - testImplementation "${libs.junitJupiterParams}" - - // (Optional) If you also have JUnit 4-based tests - testImplementation "${libs.junit4}" - testRuntimeOnly "${libs.junitVintageEngine}" - } - ``` -
- -
- -### Alternative: Legacy DSL - -If you prefer to use the legacy way to declare the dependency instead, remove the `version()` block from above and declare the plugin in your _root project's build script_ like so: - -
- Kotlin - - ```kotlin - // In the root project's build.gradle.kts: - buildscript { - dependencies { - classpath("de.mannodermaus.gradle.plugins:android-junit5:${pluginVersion}") - } - } - - // In the app module's build.gradle.kts: - plugins { - id("de.mannodermaus.android-junit5") - } - ``` -
- -
- Groovy - - ```groovy - // In the root project's build.gradle: - buildscript { - dependencies { - classpath "de.mannodermaus.gradle.plugins:android-junit5:${pluginVersion}" - } - } - - // In the app module's build.gradle: - apply plugin: "de.mannodermaus.android-junit5" - ``` -
- -
+} + +dependencies { + // 2. Add JUnit Framework BOM and the required dependencies + testImplementation(platform("${libs.junit.framework.bom5}")) + testImplementation("${libs.junit.jupiter.api}") + testImplementation("${libs.junit.jupiter.params}") + testRuntimeOnly("${libs.junit.jupiter.engine}") + + // 3. Add JUnit Vintage if you also have JUnit 4 tests (e.g. Robolectric) + testImplementation("${libs.junit.vintage.api}") + testRuntimeOnly("${libs.junit.vintage.engine}") +} +``` More information on Getting Started can be found [on the wiki][wiki-gettingstarted]. ## Requirements -The latest version of this plugin requires: -* Android Gradle Plugin `${constants.minimumRequiredAgpVersion}` or above -* Gradle `${constants.minimumRequiredGradleVersion}` or above +The latest version of this plugin requires at least: +* Android Gradle Plugin `${constants.minimumRequiredAgpVersion}` +* Gradle `${constants.minimumRequiredGradleVersion}` ## Instrumentation Test Support -You can use JUnit 5 to run instrumentation tests on emulators and physical devices, too. Because the framework is built on Java 8 from the ground up, these instrumentation tests will only run on devices running Android 8.0 (API 26) or newer – older phones will skip the execution of these tests completely, marking them as "ignored". - -Before you can write instrumentation tests with JUnit Jupiter, make sure that your module is using the `androidx.test.runner.AndroidJUnitRunner` (or a subclass of it) as its `testInstrumentationRunner`. Then, simply add a dependency on `junit-jupiter-api` to the `androidTestImplementation` configuration in your build script and the plugin will automatically configure JUnit 5 tests for you: +You can also write instrumentation tests with new JUnit APIs and execute them on emulators and physical devices. +Depending on the Java requirements of the JUnit Framework version, these instrumentation tests will only run on devices +that meet these requirements, however. These tests are ignored and their execution will be skipped on older devices. -
- Kotlin - - ```kotlin - dependencies { - androidTestImplementation("${libs.junitJupiterApi}") - } - ``` -
+- JUnit 5 requires Java 8 and is only supported by devices running Android 8.0 (API 26) or newer +- JUnit 6 requires Java 17 and is only supported by devices running Android 15 (API 35) or newer -
- Groovy +Before you can write instrumentation tests with JUnit Jupiter, +make sure that your module is using the `androidx.test.runner.AndroidJUnitRunner` +(or a subclass of it) as its `testInstrumentationRunner`. Then, simply add a dependency on JUnit Jupiter API +to the `androidTestImplementation` configuration in your build script and the plugin will +automatically configure JUnit 5 tests for you: - ```groovy - dependencies { - androidTestImplementation "${libs.junitJupiterApi}" - } - ``` -
+```kotlin +dependencies { + androidTestImplementation(platform("${libs.junit.framework.bom5}")) + androidTestImplementation("${libs.junit.jupiter.api}") +} +``` -By enabling JUnit 5 for instrumentation tests, you will gain access to `ActivityScenarioExtension` (among other things), which helps with the orchestration of `Activity` classes. Check [the wiki][wiki-home] for more info. +By enabling JUnit Framework for instrumentation tests, you will gain access to `ActivityScenarioExtension` among other things, +which helps with the orchestration of `Activity` classes. Check [the wiki][wiki-home] for more info. ### Extensions -An optional artifact with more helper extensions is available for specific use cases. -It contains the following APIs: +An optional artifact with `extensions` is available for specific use cases. It contains the following APIs: - `GrantPermissionExtension` for granting permissions before each test Can you think of more? Let's discuss in the issues section! -
- Kotlin - - ```kotlin - junitPlatform { +```kotlin +junitPlatform { instrumentationTests.includeExtensions.set(true) - } - ``` -
- -
- Groovy - - ```groovy - junitPlatform { - instrumentationTests.includeExtensions.set(true) - } - ``` -
+} +``` ### Jetpack Compose -To test `@Composable` functions on device with JUnit 5, first enable support for instrumentation tests as described above. -Then, add the Compose test dependency to your `androidTestImplementation` configuration -and the plugin will autoconfigure JUnit 5 Compose support for you! +To test `@Composable` functions on devices compatible with the JUnit Framework, +enable support for instrumentation tests as described above. Then add the Compose test dependency +to your `androidTestImplementation` configuration and the plugin will autoconfigure JUnit 5 Compose support for you. + +```kotlin +dependencies { + // Setup from the previous section for enabling instrumentation tests... -
- Kotlin - - ```kotlin - dependencies { // Compose test framework androidTestImplementation("androidx.compose.ui:ui-test-android:$compose_version") // Needed for createComposeExtension() and createAndroidComposeExtension() debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") - } - ``` -
- -
- Groovy - - ```groovy - dependencies { - // Compose test framework - androidTestImplementation "androidx.compose.ui:ui-test-android:$compose_version" - - // Needed for createComposeExtension() and createAndroidComposeExtension() - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" - } - ``` -
+} +``` -[The wiki][wiki-home] includes a section on how to test your Composables with JUnit 5. +[The wiki][wiki-home] includes a section on how to test your Composables. ### Override the version of instrumentation test libraries By default, the plugin will make sure to use a compatible version of the instrumentation test libraries when it sets up the artifacts automatically. However, it is possible to choose a custom version instead via its DSL: -
- Kotlin - - ```kotlin - junitPlatform { - instrumentationTests.version.set("${instrumentationVersion}") - } - ``` -
- -
- Groovy - - ```groovy - junitPlatform { +junitPlatform { instrumentationTests.version.set("${instrumentationVersion}") - } - ``` -
+} ## Official Support -At this time, Google hasn't shared any immediate plans to bring first-party support for JUnit 5 to Android. The following list is an aggregation of pending feature requests: +At this time, Google hasn't shared any immediate plans to bring first-party support for anything beyond JUnit 4 to Android. +The following list is an aggregation of pending feature requests: - [InstantTaskExecutorRule uses @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -- why? (issuetracker.google.com)](https://issuetracker.google.com/u/0/issues/79189568) - [Add support for JUnit 5 (issuetracker.google.com)](https://issuetracker.google.com/issues/127100532) @@ -236,49 +130,37 @@ At this time, Google hasn't shared any immediate plans to bring first-party supp ## Support for @Rules -Since JUnit 5 has replaced the `@Rule` mechanism with Extensions, the following artifacts help bridge the gap until Android officially transitions to JUnit 5. +Since the JUnit Framework has replaced the `@Rule` mechanism with the concept of an `Extension`, +the following artifacts help bridge the gap until Android officially transitions, if ever. ### InstantExecutorExtension Replaces `InstantTaskExecutorRule` in JUnit 5. -
- Kotlin - - ```kotlin - dependencies { - testImplementation("${libs.instantTaskExecutorExtension}") - } - ``` -
- -
- Groovy - - ```groovy - dependencies { - testImplementation '${libs.instantTaskExecutorExtension}' - } - ``` -
+```kotlin +dependencies { + testImplementation("${libs.instanttaskexecutor.extension}") +} +``` For more details see [instant-task-executor-extension](https://github.com/neboskreb/instant-task-executor-extension) on GitHub. ## Building Locally -This repository contains multiple modules, divided into two sub-projects. The repository's root directory contains build logic shared across the sub-projects, which in turn use symlinks to connect to the common build scripts in their parent folder. +This repository contains multiple modules, divided into two sub-projects. +The repository's root directory contains build logic shared across the sub-projects, +which in turn use symlinks to connect to the common build scripts in their parent folder. -- `instrumentation`: The root folder for Android-based modules, namely the instrumentation libraries & a sample application. After cloning, open this project in Android Studio. -- `plugin`: The root folder for Java-based modules, namely the Gradle plugin for JUnit 5 on Android, as well as its test module. After cloning, open this project in IntelliJ IDEA. +- `instrumentation`: The root folder for the instrumentation libraries & a sample. Open this folder in Android Studio. +- `plugin`: The root folder for the Gradle plugin. Open this folder in IntelliJ IDEA. ## Plugin Compatibility Map For users that cannot match the current minimum version requirement of the Android Gradle Plugin requested by this plugin, -refer to the table below to find a suitable alternative version. Note that **no active development will go into these -legacy versions**, so please consider upgrading to at least Android Gradle Plugin ${constants.minimumRequiredAgpVersion} -before filing an issue with the latest one. +refer to the table below to find a suitable alternative version. Note that **no active development will go into legacy versions**, +so please consider upgrading to at least AGP ${constants.minimumRequiredAgpVersion} before filing an issue with the latest one. -|Your AGP Version|Suggested JUnit5 Plugin Version| +|Your AGP Version|Suggested Plugin Version| |---|---| |`>= 8.2.0`|`${pluginVersion}`| |`8.0.0` - `8.1.4`|`1.12.2.0`| @@ -307,8 +189,8 @@ limitations under the License. See also the [full License text](LICENSE). - [junit5gh]: https://github.com/junit-team/junit5 - [junit5ug]: https://junit.org/junit5/docs/current/user-guide + [junit-framework-github]: https://github.com/junit-team/junit-framework + [junit-framework-guide]: https://docs.junit.org [circleci]: https://circleci.com/gh/mannodermaus/android-junit5 [sonatyperepo]: https://central.sonatype.com/repository/maven-snapshots [sampletests]: instrumentation/sample diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 85ba6c55..7d3ec70d 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,13 +1,14 @@ plugins { - `kotlin-dsl` - java + `kotlin-dsl` + java } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation(gradleApi()) - testImplementation("junit:junit:+") + implementation(gradleApi()) + + testImplementation(libs.junit.vintage.api) } diff --git a/build-logic/gradle/libs.versions.toml b/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..d529165f --- /dev/null +++ b/build-logic/gradle/libs.versions.toml @@ -0,0 +1,83 @@ +[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] +# Intentionally using dynamic version; it is substituted when creating test configurations for each AGP version +agp = "com.android.tools.build:gradle:+" +android-tools = "com.android.tools:common:+" +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-launcher = { module = "org.junit.platform:junit-platform-launcher" } +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" } +kgp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +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 deleted file mode 100644 index b75bcd1b..00000000 --- a/build-logic/src/main/kotlin/Dependencies.kt +++ /dev/null @@ -1,93 +0,0 @@ -@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" -} diff --git a/build-logic/src/main/kotlin/Deployment.kt b/build-logic/src/main/kotlin/Deployment.kt index 5fa0f154..0876baeb 100644 --- a/build-logic/src/main/kotlin/Deployment.kt +++ b/build-logic/src/main/kotlin/Deployment.kt @@ -1,5 +1,6 @@ @file:Suppress("UNCHECKED_CAST") +import groovy.lang.Closure import groovy.util.Node import org.gradle.api.NamedDomainObjectCollection import org.gradle.api.Project @@ -8,14 +9,19 @@ import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.plugins.ExtensionAware import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.internal.PublicationInternal +import org.gradle.api.publish.maven.MavenArtifact import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.internal.publication.DefaultMavenPublication import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.closureOf +import org.gradle.kotlin.dsl.maybeCreate import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.support.uppercaseFirstChar import org.gradle.kotlin.dsl.withGroovyBuilder +import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension import java.io.File @@ -24,38 +30,136 @@ import java.io.File * Configure deployment tasks and properties for a project using the provided [deployConfig]. */ fun Project.configureDeployment(deployConfig: Deployed) { - if (this == rootProject) { - throw IllegalStateException("This method can not be called on the root project") - } + check(this != rootProject) { "This method can not be called on the root project" } val credentials = DeployedCredentials(this) - // Configure root project (this only happens once - // and will raise an error on inconsistent data) + // Configure root project (this only happens once and will raise an error on inconsistent data) rootProject.configureRootDeployment(deployConfig, credentials) - val isAndroid = plugins.findPlugin("com.android.library") != null - val isGradlePlugin = plugins.hasPlugin("java-gradle-plugin") + // Apply deployment config for each type of project + plugins.withId("com.android.library") { configureAndroidDeployment(deployConfig, credentials) } + plugins.withId("java-gradle-plugin") { configurePluginDeployment(deployConfig, credentials) } +} + +/* Private */ + +private fun Project.configureRootDeployment( + deployConfig: Deployed, + credentials: DeployedCredentials +) { + check(this == rootProject) { "This method can only be called on the root project" } + + // Validate the integrity of published versions + // (all subprojects must use the same group ID and version number or else an error is raised) + if (version != "unspecified") { + check(version == deployConfig.currentVersion && group == deployConfig.groupId) { + "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 Environment.kt for inconsistencies!" + } + + // Already configured and correct + return + } + + // One-time initialization beyond this point + group = deployConfig.groupId + version = deployConfig.currentVersion + + centralPublishing( + packageGroup = deployConfig.groupId, + stagingProfileId = credentials.sonatypeStagingProfileId, + username = credentials.centralUsername, + password = credentials.centralPassword + ) +} +private fun Project.configureCommonDeployment( + deployConfig: Deployed, + credentials: DeployedCredentials +) { apply { plugin("maven-publish") plugin("signing") plugin("org.jetbrains.dokka") } + // Setup publication details + group = deployConfig.groupId + version = deployConfig.currentVersion + + // Setup code signing + ext["signing.keyId"] = credentials.signingKeyId + ext["signing.password"] = credentials.signingPassword + ext["signing.secretKeyRingFile"] = credentials.signingKeyRingFile + signing { + sign(publishing.publications) + } +} + +private fun Project.configureAndroidDeployment( + deployConfig: Deployed, + credentials: DeployedCredentials +) { + val android = AndroidDsl(this) + configureCommonDeployment(deployConfig, credentials) + + // Create a publication for each variant + SupportedJUnit.values().forEach { junit -> + val variantName = "${junit.variant}Release" + + android.publishing.singleVariant(variantName) { + withSourcesJar() + withJavadocJar() + } + + afterEvaluate { + publishing { + publications { + // Declare an empty 'main' publication and mark the actual publication + // for each variant as an 'alias'. This is done to work around limitations + // of the Maven publication process + maybeCreate("main") + + register(junit.variant) { + from(components.getByName(variantName)) + + applyPublicationDetails( + project = this@afterEvaluate, + deployConfig = deployConfig, + junit = junit + ) + + (this as PublicationInternal).isAlias = true + + // We have to write parts of the POM file ourselves, so that + // project dependencies between instrumentation libraries use the correct + // coordinates for each variant (e.g. "junit5" vs "junit6") + configurePom(deployConfig) + } + } + } + } + } +} + +private fun Project.configurePluginDeployment( + deployConfig: Deployed, + credentials: DeployedCredentials +) { + configureCommonDeployment(deployConfig, credentials) + // Create artifact tasks - val androidSourcesJar = tasks.register("androidSourcesJar") { + val sourcesJar = tasks.register("sourcesJar") { archiveClassifier.set("sources") - if (isAndroid) { - // This declaration includes Java source directories - from(android.sourceSets.main.kotlin.srcDirs) - } else { - // This declaration includes Kotlin & Groovy source directories - from(sourceSets.main.allJava.srcDirs) - } + // This declaration includes Kotlin & Groovy source directories + from(sourceSets.main.allJava.srcDirs) } + // Create javadoc artifact val javadocJar = tasks.register("javadocJar") { archiveClassifier.set("javadoc") @@ -65,187 +169,135 @@ fun Project.configureDeployment(deployConfig: Deployed) { } artifacts { - add("archives", androidSourcesJar) + add("archives", sourcesJar) add("archives", javadocJar) } - // Setup publication details - group = deployConfig.groupId - version = deployConfig.currentVersion + // Connect signing task to the JAR produced by the artifact-producing task + tasks.withType().configureEach { + dependsOn("assemble") + } publishing { publications { - // For Gradle Plugin projects, there already is a 'pluginMaven' publication - // pre-configured by the Java Gradle plugin, which we will extend with more properties and details. - // For other projects, a new publication must be created instead - if (isGradlePlugin) { - all { - if (this !is MavenPublication) return@all - + all { + if (this is MavenPublication) { if (name == "pluginMaven") { + // Attach artifacts + artifacts.clear() + artifact(layout.buildDirectory.file("libs/${project.name}-$version.jar")) + artifact(sourcesJar) + artifact(javadocJar) + applyPublicationDetails( - project = this@configureDeployment, - deployConfig = deployConfig, - isAndroid = isAndroid, - androidSourcesJar = androidSourcesJar, - javadocJar = javadocJar + project = this@configurePluginDeployment, + deployConfig = deployConfig ) } // Always extend POM details to satisfy Maven Central's POM validation - // (they require a bunch of metadata for each POM, which isn't filled out by default) + // (they require a bunch of metadata that isn't filled out by default) configurePom(deployConfig) } - } else { - create("release", MavenPublication::class.java) - .applyPublicationDetails( - project = this@configureDeployment, - deployConfig = deployConfig, - isAndroid = isAndroid, - androidSourcesJar = androidSourcesJar, - javadocJar = javadocJar - ) - .configurePom(deployConfig) } } } - - // Setup code signing - ext["signing.keyId"] = credentials.signingKeyId - ext["signing.password"] = credentials.signingPassword - ext["signing.secretKeyRingFile"] = credentials.signingKeyRingFile - signing { - sign(publishing.publications) - } - - // Connect signing task to artifact-producing task - // (build an AAR for Android modules, assemble a JAR for other modules) - tasks.withType(Sign::class.java).configureEach { - dependsOn(if (isAndroid) "bundleReleaseAar" else "assemble") - } -} - -/* Private */ - -private fun Project.configureRootDeployment(deployConfig: Deployed, credentials: DeployedCredentials) { - if (this != rootProject) { - throw IllegalStateException("This method can only be called on the root project") - } - - // Validate the integrity of published versions - // (all subprojects must use the same group ID and 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!") - } else { - // Already configured and correct - return - } - } - - // One-time initialization beyond this point - group = deployConfig.groupId - version = deployConfig.currentVersion - - centralPublishing( - packageGroup = deployConfig.groupId, - stagingProfileId = credentials.sonatypeStagingProfileId, - username = credentials.centralUsername, - password = credentials.centralPassword - ) } private fun MavenPublication.applyPublicationDetails( - project: Project, - deployConfig: Deployed, - isAndroid: Boolean, - androidSourcesJar: TaskProvider, - javadocJar: TaskProvider + project: Project, + deployConfig: Deployed, + junit: SupportedJUnit? = null ) = also { groupId = deployConfig.groupId - artifactId = deployConfig.artifactId + artifactId = suffixedArtifactId(deployConfig.artifactId, junit) version = deployConfig.currentVersion - // Attach artifacts - artifacts.clear() - val buildDir = project.layout.buildDirectory - if (isAndroid) { - artifact(buildDir.file("outputs/aar/${project.name}-release.aar").get().asFile) - } else { - artifact(buildDir.file("libs/${project.name}-$version.jar")) - } - artifact(androidSourcesJar) - artifact(javadocJar) - - // Attach dependency information + // Attach dependency information, rewriting it to work around certain inadequacies + // with the built-in maven publish POM generation pom { withXml { with(asNode()) { - // Only add dependencies manually if there aren't any already - if (children().filterIsInstance() - .none { it.name().toString().endsWith("dependencies") } - ) { - val dependenciesNode = appendNode("dependencies") - - val compileDeps = project.configurations.getByName("api").allDependencies - val runtimeDeps = project.configurations.getByName("implementation").allDependencies + + // Replace an existing node or just append a new one. + val dependenciesNode = replaceNode("dependencies") + + val compileDeps = project.configurations.getByName("api").allDependencies + val runtimeDeps = + project.configurations.getByName("implementation").allDependencies + project.configurations.getByName("runtimeOnly").allDependencies - compileDeps - val dependencies = mapOf( - "runtime" to runtimeDeps, - "compile" to compileDeps - ) + val dependencies = mapOf( + "runtime" to runtimeDeps, + "compile" to compileDeps + ) + + dependencies + .mapValues { entry -> entry.value.filter { it.name != "unspecified" } } + .forEach { (scope, dependencies) -> + dependencies.forEach { dep -> + // Do not allow BOM dependencies for our own packaged libraries, + // instead its artifact versions should be unrolled explicitly + require("-bom" !in dep.name) { + "Found a BOM declaration in the dependencies of project" + + "${project.path}: $dep. Prefer declaring its " + + "transitive artifacts explicitly by " + + "adding a version constraint to them." + } - dependencies - .mapValues { entry -> entry.value.filter { it.name != "unspecified" } } - .forEach { (scope, dependencies) -> - dependencies.forEach { dep -> - // Do not allow BOM dependencies for our own packaged libraries, - // instead its artifact versions should be unrolled explicitly - if ("-bom" in dep.name) { - throw IllegalArgumentException( - "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." - ) + with(dependenciesNode.appendNode("dependency")) { + if (dep is ProjectDependency) { + appendProjectDependencyCoordinates(dep, junit) + } else { + appendExternalDependencyCoordinates(dep) } - with(dependenciesNode.appendNode("dependency")) { - if (dep is ProjectDependency) { - appendProjectDependencyCoordinates(dep) - } else { - appendExternalDependencyCoordinates(dep) - } - - appendNode("scope", scope) - } + appendNode("scope", scope) } } - } + } } } } } -private fun Node.appendProjectDependencyCoordinates(dep: ProjectDependency) { +private fun Node.appendProjectDependencyCoordinates( + dep: ProjectDependency, + junit: SupportedJUnit? +) { // Find the external coordinates for the given project dependency val projectName = dep.name val config = Artifacts.Instrumentation::class.java .getMethod("get${projectName.uppercaseFirstChar()}") .invoke(Artifacts.Instrumentation) - as Deployed + as Deployed appendNode("groupId", config.groupId) - appendNode("artifactId", config.artifactId) + appendNode("artifactId", suffixedArtifactId(config.artifactId, junit)) appendNode("version", config.currentVersion) } +private fun suffixedArtifactId(base: String, junit: SupportedJUnit? = null) = buildString { + append(base) + + // Attach optional suffix to Android artifacts + // to distinguish between different JUnit targets + junit?.artifactIdSuffix?.let { + append('-') + append(it) + } +} + +private fun Node.findNode(name: String): Node? = + // This method uses "endsWith()" to ignore XMLNS prefixes + children().firstOrNull { it is Node && it.name().toString().endsWith(name) } as? Node + +private fun Node.replaceNode(name: String): Node { + findNode(name)?.let { remove(it) } + return appendNode(name) +} + private fun Node.appendExternalDependencyCoordinates(dep: Dependency) { appendNode("groupId", dep.group) appendNode("artifactId", dep.name) @@ -299,38 +351,45 @@ private val Project.ext: ExtraPropertiesExtension get() = extensions.getByName("ext") as ExtraPropertiesExtension /** - * Allows us to retain the untyped Groovy API even in the stricter Kotlin context - * ("android.sourceSets.main.java.srcDirs") + * Allows us to access certain APIs without access to the actual plugins. Brittle, but works. */ private class AndroidDsl(project: Project) { private val delegate = project.extensions.getByName("android") as ExtensionAware - val sourceSets = SourceSetDsl(delegate) - - class SourceSetDsl(android: ExtensionAware) { - private val delegate = android.javaClass.getDeclaredMethod("getSourceSets") - .also { it.isAccessible = true } - .invoke(android) as NamedDomainObjectCollection + val publishing = PublishingDsl(delegate) - val main = MainDsl(delegate) + class PublishingDsl(android: ExtensionAware) { + private val delegate = android.javaClass.getDeclaredMethod("getPublishing") + .also { it.isAccessible = true } + .invoke(android) - class MainDsl(sourceSets: NamedDomainObjectCollection) { - private val delegate = sourceSets.named("main").get() + fun singleVariant(name: String, block: SingleVariantDsl.() -> Unit = {}) { + delegate.javaClass + .getDeclaredMethod("singleVariant", String::class.java, Closure::class.java) + .also { it.isAccessible = true } + .invoke(delegate, name, closureOf { + SingleVariantDsl(this).block() + }) + } - val kotlin = KotlinDsl(delegate) + class SingleVariantDsl(private val delegate: Any) { + fun withSourcesJar() { + delegate.javaClass.declaredMethods + .first { it.name.startsWith("withSourcesJar") } + .also { it.isAccessible = true } + .invoke(delegate, true) + } - class KotlinDsl(main: Any) { - val srcDirs = main.javaClass - .getDeclaredMethod("getKotlinDirectories") - .invoke(main) as Set + fun withJavadocJar() { + delegate.javaClass.declaredMethods + .first { it.name.startsWith("withJavadocJar") } + .also { it.isAccessible = true } + .invoke(delegate, true) } } } } -private val Project.android - get() = AndroidDsl(this) - private class SourceSetDsl(project: Project) { private val delegate = project.extensions.getByName("sourceSets") as SourceSetContainer @@ -372,24 +431,30 @@ private fun Project.centralPublishing( val cls = delegate.javaClass cls.getDeclaredMethod("setStagingProfileId", Any::class.java) - .also { it.isAccessible = true } - .invoke(delegate, stagingProfileId) + .also { it.isAccessible = true } + .invoke(delegate, stagingProfileId) cls.getDeclaredMethod("setUsername", Any::class.java) - .also { it.isAccessible = true } - .invoke(delegate, username) + .also { it.isAccessible = true } + .invoke(delegate, username) cls.getDeclaredMethod("setPassword", Any::class.java) - .also { it.isAccessible = true } - .invoke(delegate, password) + .also { it.isAccessible = true } + .invoke(delegate, password) cls.getDeclaredMethod("setNexusUrl", Any::class.java) .also { it.isAccessible = true } - .invoke(delegate, uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + .invoke( + delegate, + uri("https://ossrh-staging-api.central.sonatype.com/service/local/") + ) cls.getDeclaredMethod("setSnapshotRepositoryUrl", Any::class.java) .also { it.isAccessible = true } - .invoke(delegate, uri("https://central.sonatype.com/repository/maven-snapshots/")) + .invoke( + delegate, + uri("https://central.sonatype.com/repository/maven-snapshots/") + ) } } } diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt index 6ade4345..f3e3c22f 100644 --- a/build-logic/src/main/kotlin/Environment.kt +++ b/build-logic/src/main/kotlin/Environment.kt @@ -4,6 +4,20 @@ import org.gradle.api.Project import java.io.File import java.util.Properties +enum class SupportedJUnit( + val majorVersion: Int, + val variant: String, + val minSdk: Int, + val artifactIdSuffix: String? = null +) { + JUnit5(majorVersion = 5, variant = "five", minSdk = 26), + JUnit6(majorVersion = 6, variant = "six", minSdk = 35, artifactIdSuffix = "junit6"); + + companion object { + fun fromVariant(variant: String): SupportedJUnit = values().first { it.variant == variant } + } +} + enum class SupportedAgp( val version: String, val gradle: String, @@ -21,8 +35,8 @@ enum class SupportedAgp( AGP_8_11("8.11.2", gradle = "8.13"), AGP_8_12("8.12.3", gradle = "8.13"), AGP_8_13("8.13.2", gradle = "8.13"), - AGP_9_0("9.0.0-rc01", gradle = "9.1.0"), - AGP_9_1("9.1.0-alpha01", gradle = "9.1.0"), + AGP_9_0("9.0.0-rc02", gradle = "9.1.0"), + AGP_9_1("9.1.0-alpha02", gradle = "9.1.0"), ; companion object { @@ -33,9 +47,7 @@ enum class SupportedAgp( val shortVersion: String = run { // Extract first two components of the Maven dependency's version string. val components = version.split('.') - if (components.size < 2) { - throw IllegalArgumentException("Cannot derive AGP configuration name from: $this") - } + require(components.size >= 2) { "Cannot derive AGP configuration name from: $this" } "${components[0]}.${components[1]}" } @@ -94,9 +106,9 @@ object Artifacts { platform = Java, groupId = "de.mannodermaus.gradle.plugins", artifactId = "android-junit5", - currentVersion = "1.14.0.1-SNAPSHOT", + currentVersion = "2.0.0-SNAPSHOT", latestStableVersion = "1.14.0.0", - description = "Unit Testing with JUnit 5 for Android." + description = "Unit Testing with the JUnit Framework for Android." ) /** @@ -104,7 +116,7 @@ object Artifacts { */ object Instrumentation { const val groupId = "de.mannodermaus.junit5" - private const val currentVersion = "1.9.1-SNAPSHOT" + private const val currentVersion = "2.0.0-SNAPSHOT" private const val latestStableVersion = "1.9.0" val Core = Deployed( @@ -113,7 +125,7 @@ object Artifacts { artifactId = "android-test-core", currentVersion = currentVersion, latestStableVersion = latestStableVersion, - description = "Extensions for instrumented Android tests with JUnit 5." + description = "Extensions for instrumented Android tests with the JUnit Framework." ) val Extensions = Deployed( @@ -122,7 +134,7 @@ object Artifacts { artifactId = "android-test-extensions", currentVersion = currentVersion, latestStableVersion = latestStableVersion, - description = "Optional extensions for instrumented Android tests with JUnit 5." + description = "Optional extensions for instrumented Android tests with the JUnit Framework." ) val Runner = Deployed( @@ -131,7 +143,7 @@ object Artifacts { artifactId = "android-test-runner", currentVersion = currentVersion, latestStableVersion = latestStableVersion, - description = "Runner for integration of instrumented Android tests with JUnit 5." + description = "Runner for integration of instrumented Android tests with the JUnit Framework." ) val Compose = Deployed( @@ -140,13 +152,12 @@ object Artifacts { artifactId = "android-test-compose", currentVersion = currentVersion, latestStableVersion = latestStableVersion, - description = "Extensions for Jetpack Compose tests with JUnit 5." + description = "Extensions for Jetpack Compose tests with the JUnit Framework." ) } } class DeployedCredentials(private val project: Project) { - var signingKeyId: String? var signingPassword: String? var signingKeyRingFile: String? diff --git a/build-logic/src/main/kotlin/Tasks.kt b/build-logic/src/main/kotlin/Tasks.kt index 1b747a71..e84f6b1d 100644 --- a/build-logic/src/main/kotlin/Tasks.kt +++ b/build-logic/src/main/kotlin/Tasks.kt @@ -1,154 +1,17 @@ +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 -import org.gradle.api.artifacts.ExternalModuleDependency -import org.gradle.api.attributes.Usage -import org.gradle.api.attributes.Usage.JAVA_RUNTIME -import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE -import org.gradle.api.attributes.java.TargetJvmEnvironment -import org.gradle.api.attributes.java.TargetJvmEnvironment.STANDARD_JVM -import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE -import org.gradle.api.attributes.plugin.GradlePluginApiVersion -import org.gradle.api.attributes.plugin.GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE -import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.Copy import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import java.io.File import java.time.ZonedDateTime -import java.util.Locale - -private const val minimumGradleVersion = "8.2" - -@Suppress("DEPRECATION") -fun Project.configureTestResources() { - // Create a test resource task which will power the instrumented tests - // for different versions of the Android Gradle Plugin - tasks.named("processTestResources", Copy::class.java).configure { - val tokens = mapOf( - "COMPILE_SDK_VERSION" to Android.compileSdkVersion.toString(), - "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, - "JUNIT5_ANDROID_LIBS_VERSION" to Artifacts.Instrumentation.Core.latestStableVersion, - - // Collect all supported AGP versions into a single string. - // This string is delimited with semicolons, and each of the separated values itself is a 4-tuple. - // - // Example: - // AGP_VERSIONS = 3.5|3.5.3|;3.6|3.6.3|6.4;3.7|3.7.0|8.0|33 - // - // Can be parsed into this list of values: - // |___> Short: "3.5" - // Full: "3.5.3" - // Gradle Requirement: "" - // Compile SDK: null - // - // |___> Short: "3.6" - // Full: "3.6.3" - // Gradle Requirement: "6.4" - // Compile SDK: null - // - // |___> Short: "3.7" - // Full: "3.7.0" - // Gradle Requirement: "8.0" - // Compile SDK: 33 - "AGP_VERSIONS" to SupportedAgp.values().joinToString(separator = ";") { plugin -> - "${plugin.shortVersion}|${plugin.version}|${plugin.gradle}|${plugin.compileSdk ?: ""}" - } - ) - - inputs.properties(tokens) - duplicatesStrategy = DuplicatesStrategy.INCLUDE - - // Apply test environment to a resource file - val sourceSets = project.extensions.getByName("sourceSets") as SourceSetContainer - from(sourceSets.getByName("test").resources.srcDirs) { - include("**/testenv.properties") - filter(mapOf("tokens" to tokens), ReplaceTokens::class.java) - } - } - - // Also, create a custom configuration for each of the supported Android Gradle Plugin versions - project.configurations.apply { - SupportedAgp.values().forEach { plugin -> - create(plugin.configurationName) { - 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}") - - // 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 - // (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 { - with(this as ExternalModuleDependency) { - attributes { - attribute( - TARGET_JVM_ENVIRONMENT_ATTRIBUTE, - objects.named(TargetJvmEnvironment::class.java, STANDARD_JVM) - ) - attribute( - USAGE_ATTRIBUTE, - objects.named(Usage::class.java, JAVA_RUNTIME) - ) - attribute( - GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, - objects.named(GradlePluginApiVersion::class.java, minimumGradleVersion) - ) - } - } - } - } - } - } - } - - // Create slim plugin classpath for functional tests, using multiple flavors - tasks.named("pluginUnderTestMetadata").configure { - val defaultDirectory = outputs.files.singleFile - - configurations.filter { it.name.startsWith("testAgp") }.forEach { configuration -> - val strippedName = configuration.name.substring(4).toLowerCase(Locale.ROOT) - val prunedFile = File(defaultDirectory, "pruned-plugin-metadata-$strippedName.properties") - outputs.file(prunedFile) - - doLast { - prunedFile.writer().use { writer -> - // 1) Use output classes from the plugin itself - // 2) Use resources from the plugin (i.e. plugin IDs etc.) - // 3) Use AGP-specific dependencies - val classesDirs = layout.buildDirectory.dir("classes").get().asFile.listFiles() - ?.filter { it.isDirectory } - ?.map { File(it, "main") } - ?.filter { it.exists() && it.isDirectory && it.list()?.isEmpty() == false } - ?: emptyList() - val resourcesDirs = layout.buildDirectory.dir("resources").get().asFile.listFiles() - ?.filter { it.isDirectory } - ?: emptyList() - - writer.write("implementation-classpath=") - writer.write( - (classesDirs + resourcesDirs + configuration) - .joinToString(separator = "\\:") - ) - } - } - } - } -} +import javax.inject.Inject fun findInstrumentationVersion( pluginVersion: String = Artifacts.Plugin.currentVersion, @@ -184,7 +47,30 @@ fun Copy.configureCreateVersionClassTask( // 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 + "JUNIT_PLATFORM_LAUNCHER" to project.libs.library("junit-platform-launcher").get().toString(), + + // Communicate all supported JUnit versions so the plugin can use them + "SUPPORTED_JUNIT_VERSIONS" to SupportedJUnit.values().joinToString { junit -> + val fullVersion = when (junit) { + SupportedJUnit.JUnit5 -> project.libs.version("junit5") + SupportedJUnit.JUnit6 -> project.libs.version("junit6") + } + + buildString { + append(junit.name) + append("(majorVersion = ") + append(junit.majorVersion) + append(", fullVersion = \"") + append(fullVersion) + append("\"") + junit.artifactIdSuffix?.let { + append(", artifactIdSuffix = \"") + append(it) + append('\"') + } + append(')') + } + } ) ), ReplaceTokens::class.java ) @@ -196,7 +82,9 @@ fun Copy.configureCreateVersionClassTask( * Using a template file, the plugin's version constants & other dependency versions * are automatically injected into the README. */ -abstract class GenerateReadme : DefaultTask() { +abstract class GenerateReadme @Inject constructor( + private val project: Project +) : DefaultTask() { companion object { private val PLACEHOLDER_REGEX = Regex("\\\$\\{(.+)}") private val EXTERNAL_DEP_REGEX = Regex("libs\\.(.+)") @@ -246,7 +134,7 @@ abstract class GenerateReadme : DefaultTask() { // Apply placeholders in the template with data from Versions.kt & Environment.kt: // ${pluginVersion} Artifacts.Plugin.currentVersion // ${instrumentationVersion} Artifacts.Instrumentation.Core.currentVersion - // ${Libs.} (A constant value taken from Dependencies.kt) + // ${libs.} (A constant value taken from the version catalog) val allPlaceholders = mutableMapOf() PLACEHOLDER_REGEX.findAll(templateText).forEach { match -> @@ -267,11 +155,12 @@ abstract class GenerateReadme : DefaultTask() { } else { val match3 = EXTERNAL_DEP_REGEX.find(placeholder) ?: throw InvalidPlaceholder(match) - val externalDependency = match3.groups.last()?.value + val externalDependency = match3.groups.last() + ?.value + ?.replace('.', '-') ?: throw InvalidPlaceholder(match3) - val field = libs.javaClass.getField(externalDependency) - field.get(null) as String + project.libs.library(externalDependency).get().toString() } } } diff --git a/build-logic/src/main/kotlin/extensions/DependencyExtensions.kt b/build-logic/src/main/kotlin/extensions/DependencyExtensions.kt new file mode 100644 index 00000000..a5c24496 --- /dev/null +++ b/build-logic/src/main/kotlin/extensions/DependencyExtensions.kt @@ -0,0 +1,8 @@ +package extensions + +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.provider.Provider + +fun Provider.getWithVersion(version: String): String { + return this.get().toString().replace("+", version) +} 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 new file mode 100644 index 00000000..10d70521 --- /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 + +val Project.libs: VersionCatalog + get() = extensions.getByType().named("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/.idea/runConfigurations/Extensions__Run_Unit_Tests__Gradle_.xml b/instrumentation/.idea/runConfigurations/Extensions__Run_Unit_Tests__Gradle_.xml deleted file mode 100644 index 23830d08..00000000 --- a/instrumentation/.idea/runConfigurations/Extensions__Run_Unit_Tests__Gradle_.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/instrumentation/.idea/runConfigurations/Instrumentation__Update_Public_API_File.xml b/instrumentation/.idea/runConfigurations/Instrumentation__Update_Public_API_File.xml deleted file mode 100644 index b70a5699..00000000 --- a/instrumentation/.idea/runConfigurations/Instrumentation__Update_Public_API_File.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - true - true - false - - - \ No newline at end of file diff --git a/instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_5_.xml b/instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_5_.xml new file mode 100644 index 00000000..41b61666 --- /dev/null +++ b/instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_5_.xml @@ -0,0 +1,31 @@ + + + + + + + true + true + false + + false + false + + false + false + false + false + + + \ No newline at end of file diff --git a/instrumentation/.idea/runConfigurations/Instrumentation__Publish_Release_Manually.xml b/instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_6_.xml similarity index 71% rename from instrumentation/.idea/runConfigurations/Instrumentation__Publish_Release_Manually.xml rename to instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_6_.xml index 27cd0301..f3dc6727 100644 --- a/instrumentation/.idea/runConfigurations/Instrumentation__Publish_Release_Manually.xml +++ b/instrumentation/.idea/runConfigurations/Run_Unit_Tests__JUnit_6_.xml @@ -1,29 +1,31 @@ - + - true true + false false false false false + false + false \ No newline at end of file diff --git a/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml b/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml deleted file mode 100644 index c9edea7d..00000000 --- a/instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml b/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml deleted file mode 100644 index 9dabb6df..00000000 --- a/instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/instrumentation/CHANGELOG.md b/instrumentation/CHANGELOG.md index d929bd72..3b65fd95 100644 --- a/instrumentation/CHANGELOG.md +++ b/instrumentation/CHANGELOG.md @@ -6,6 +6,8 @@ Change Log - Removed deprecated `runComposeTest` API from `ComposeExtension` - Update to Kotlin 2.3 - Update to Compose 1.10 +- Support instrumentation with JUnit 5 and 6 (the plugin will choose the correct runtime accordingly) +- Avoid error when a client doesn't include junit-jupiter-params on the runtime classpath ## 1.9.0 (2025-10-10) diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index a0331297..c6732449 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -1,21 +1,153 @@ +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 +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) + + 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) -buildscript { - dependencies { - classpath(libs.plugins.kotlin) - classpath(libs.plugins.dokka) - classpath(libs.plugins.composeCompiler) - classpath(libs.plugins.android(SupportedAgp.newestStable)) - } + 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.toVersion(jvmTarget.target) + + // Configure Kotlin + plugins.withType { + tasks.withType>().configureEach { + compilerOptions { + this.progressiveMode.set(true) + if (this is KotlinJvmCompilerOptions) { + this.jvmTarget.set(jvmTarget) + } + } + } + } + + // Configure Java + plugins.withId("java") { + configure { + toolchain { languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) } + } + } + + // Configure Android + plugins.withId("com.android.base") { + configure { + val supportedTargets = SupportedJUnit.values() + + compileSdkVersion(Android.compileSdkVersion) + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + minSdk = supportedTargets.minOf(SupportedJUnit::minSdk) + } + + compileOptions { + sourceCompatibility(javaVersion) + targetCompatibility(javaVersion) + } + + with(buildFeatures) { + buildConfig = false + resValues = false + } + + testOptions { + 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 + flavorDimensions("target") + productFlavors { + supportedTargets.forEachIndexed { index, junit -> + register(junit.variant) { + dimension = "target" + isDefault = index == supportedTargets.lastIndex + } + } + } + + dependencies { + supportedTargets.forEach { junit -> + val configNames = listOf( + "${junit.variant}Api", + "${junit.variant}Implementation", + "${junit.variant}CompileOnly", + "${junit.variant}RuntimeOnly", + "test${junit.variant.capitalized()}Implementation", + "test${junit.variant.capitalized()}CompileOnly", + "test${junit.variant.capitalized()}RuntimeOnly", + "androidTest${junit.variant.capitalized()}Implementation", + "androidTest${junit.variant.capitalized()}CompileOnly", + "androidTest${junit.variant.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. + 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..650b0ca4 100644 --- a/instrumentation/compose/build.gradle.kts +++ b/instrumentation/compose/build.gradle.kts @@ -1,101 +1,58 @@ -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" - } + namespace = "de.mannodermaus.junit5.compose" - buildFeatures { - compose = true - buildConfig = false - resValues = false - } + defaultConfig { + minSdk = Android.testComposeMinSdkVersion - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - testOptions { - unitTests.isReturnDefaultValues = true - targetSdk = Android.targetSdkVersion - } - - lint { - targetSdk = Android.targetSdkVersion - } - - packaging { - resources.excludes.add("META-INF/AL2.0") - resources.excludes.add("META-INF/LGPL2.1") - } -} + testInstrumentationRunnerArguments["runnerBuilder"] = + "de.mannodermaus.junit5.AndroidJUnitFrameworkBuilder" + } -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) diff --git a/instrumentation/core/build.gradle.kts b/instrumentation/core/build.gradle.kts index 4dbc036e..df4cfb17 100644 --- a/instrumentation/core/build.gradle.kts +++ b/instrumentation/core/build.gradle.kts @@ -1,76 +1,25 @@ -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 +import java.util.concurrent.atomic.AtomicBoolean 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 +27,37 @@ 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.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, + // 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) diff --git a/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/CoreConstants.kt b/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/CoreConstants.kt new file mode 100644 index 00000000..b55eb448 --- /dev/null +++ b/instrumentation/core/src/five/kotlin/de/mannodermaus/junit5/CoreConstants.kt @@ -0,0 +1,7 @@ +package de.mannodermaus.junit5 + +/** + * The minimum Android API level on which JUnit Framework tests may be executed. + * Trying to launch a test on an older device will simply mark it as 'skipped'. + */ +public const val JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION: Int = 26 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..a4936c32 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/ActivityScenarioExtension.kt @@ -1,13 +1,13 @@ package de.mannodermaus.junit5 -import android.annotation.TargetApi import android.app.Activity import android.content.Intent -import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi 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 @@ -110,7 +110,7 @@ import java.util.concurrent.locks.ReentrantLock * ``` * */ -@TargetApi(Build.VERSION_CODES.O) +@RequiresApi(26) public class ActivityScenarioExtension private constructor(private val scenarioSupplier: () -> ActivityScenario) : BeforeEachCallback, AfterEachCallback, ParameterResolver { @@ -194,7 +194,6 @@ private constructor(private val scenarioSupplier: () -> ActivityScenario) : B /* Private */ - @Suppress("InconsistentCommentForJavaParameter") private fun ExtensionContext.acquireLock(state: Boolean) { // No need to do anything unless parallelism is enabled if (executionMode != ExecutionMode.CONCURRENT) { @@ -209,10 +208,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 +222,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/main/java/de/mannodermaus/junit5/CoreConstants.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/CoreConstants.kt deleted file mode 100644 index 0127049e..00000000 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/CoreConstants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.mannodermaus.junit5 - -/** - * The minimum Android API level on which JUnit 5 tests may be executed. - * Trying to launch a test on an older device will simply mark it as 'skipped'. - */ -public const val JUNIT5_MINIMUM_SDK_VERSION: Int = 26 diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/DeprecatedCoreConstants.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/DeprecatedCoreConstants.kt new file mode 100644 index 00000000..0efc4a31 --- /dev/null +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/DeprecatedCoreConstants.kt @@ -0,0 +1,9 @@ +@file:JvmName("DeprecatedCoreConstants") + +package de.mannodermaus.junit5 + +@Deprecated( + message = "Renamed to JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION", + replaceWith = ReplaceWith("JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION") +) +public const val JUNIT5_MINIMUM_SDK_VERSION: Int = JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersion.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersion.kt index 51dd7989..0434575a 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersion.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersion.kt @@ -1,7 +1,7 @@ package de.mannodermaus.junit5.condition import androidx.annotation.IntRange -import de.mannodermaus.junit5.JUNIT5_MINIMUM_SDK_VERSION +import de.mannodermaus.junit5.JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION import de.mannodermaus.junit5.internal.DisabledOnSdkVersionCondition import de.mannodermaus.junit5.internal.NOT_SET import org.junit.jupiter.api.extension.ExtendWith @@ -10,6 +10,6 @@ import org.junit.jupiter.api.extension.ExtendWith @Retention(AnnotationRetention.RUNTIME) @ExtendWith(DisabledOnSdkVersionCondition::class) public annotation class DisabledOnSdkVersion( - @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, - @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET + @IntRange(from = JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, + @IntRange(from = JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET ) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersion.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersion.kt index e22cfc15..09878c9c 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersion.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersion.kt @@ -1,7 +1,7 @@ package de.mannodermaus.junit5.condition import androidx.annotation.IntRange -import de.mannodermaus.junit5.JUNIT5_MINIMUM_SDK_VERSION +import de.mannodermaus.junit5.JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION import de.mannodermaus.junit5.internal.EnabledOnSdkVersionCondition import de.mannodermaus.junit5.internal.NOT_SET import org.junit.jupiter.api.extension.ExtendWith @@ -10,6 +10,6 @@ import org.junit.jupiter.api.extension.ExtendWith @Retention(AnnotationRetention.RUNTIME) @ExtendWith(EnabledOnSdkVersionCondition::class) public annotation class EnabledOnSdkVersion( - @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, - @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET + @IntRange(from = JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, + @IntRange(from = JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET ) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/CoreInternalConstants.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/CoreInternalConstants.kt index 8cf65fc5..fc1037cc 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/CoreInternalConstants.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/CoreInternalConstants.kt @@ -1,4 +1,4 @@ package de.mannodermaus.junit5.internal internal const val NOT_SET = -1 -internal const val LOG_TAG = "AndroidJUnit5" +internal const val LOG_TAG = "AndroidJUnitFramework" diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledIfBuildConfigValueCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledIfBuildConfigValueCondition.kt index dfb165ce..12dd30c3 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledIfBuildConfigValueCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledIfBuildConfigValueCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import de.mannodermaus.junit5.internal.utils.BuildConfigValueUtils import de.mannodermaus.junit5.condition.DisabledIfBuildConfigValue import org.junit.jupiter.api.extension.ConditionEvaluationResult @@ -18,7 +18,7 @@ internal class DisabledIfBuildConfigValueCondition : ExecutionCondition { enabled("@DisabledIfBuildConfigValue is not present") } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, DisabledIfBuildConfigValue::class.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnManufacturerCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnManufacturerCondition.kt index 4778dec9..dd7a1ffb 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnManufacturerCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnManufacturerCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import android.os.Build import de.mannodermaus.junit5.condition.DisabledOnManufacturer import de.mannodermaus.junit5.internal.EnabledOnManufacturerCondition.Companion.disabled @@ -18,7 +18,7 @@ internal class DisabledOnManufacturerCondition : ExecutionCondition { ConditionEvaluationResult.enabled("@DisabledOnManufacturer is not present") } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, DisabledOnManufacturer::class.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnSdkVersionCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnSdkVersionCondition.kt index c77deccd..649a0093 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnSdkVersionCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnSdkVersionCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import android.os.Build import de.mannodermaus.junit5.condition.DisabledOnSdkVersion import de.mannodermaus.junit5.internal.EnabledOnSdkVersionCondition.Companion.disabled @@ -18,7 +18,7 @@ internal class DisabledOnSdkVersionCondition : ExecutionCondition { ConditionEvaluationResult.enabled("@DisabledOnSdkVersion is not present") } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, DisabledOnSdkVersion::class.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledIfBuildConfigValueCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledIfBuildConfigValueCondition.kt index 837dd1c5..07e15f35 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledIfBuildConfigValueCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledIfBuildConfigValueCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import de.mannodermaus.junit5.internal.utils.BuildConfigValueUtils import de.mannodermaus.junit5.condition.EnabledIfBuildConfigValue import org.junit.jupiter.api.extension.ConditionEvaluationResult @@ -18,7 +18,7 @@ internal class EnabledIfBuildConfigValueCondition : ExecutionCondition { enabled("@EnabledIfBuildConfigValue is not present") } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, EnabledIfBuildConfigValue::class.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnManufacturerCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnManufacturerCondition.kt index 5ef9a4e0..5ab019bb 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnManufacturerCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnManufacturerCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import android.os.Build import de.mannodermaus.junit5.condition.EnabledOnManufacturer import org.junit.jupiter.api.extension.ConditionEvaluationResult @@ -24,7 +24,7 @@ internal class EnabledOnManufacturerCondition : ExecutionCondition { } } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, EnabledOnManufacturer::class.java) diff --git a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnSdkVersionCondition.kt b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnSdkVersionCondition.kt index 895df623..6d0a6839 100644 --- a/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnSdkVersionCondition.kt +++ b/instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnSdkVersionCondition.kt @@ -1,6 +1,6 @@ package de.mannodermaus.junit5.internal -import android.annotation.TargetApi +import androidx.annotation.RequiresApi import android.os.Build import de.mannodermaus.junit5.condition.EnabledOnSdkVersion import org.junit.jupiter.api.extension.ConditionEvaluationResult @@ -24,7 +24,7 @@ internal class EnabledOnSdkVersionCondition : ExecutionCondition { } } - @TargetApi(24) + @RequiresApi(24) override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val optional = findAnnotation(context.element, EnabledOnSdkVersion::class.java) diff --git a/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/CoreConstants.kt b/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/CoreConstants.kt new file mode 100644 index 00000000..bf6f6e8c --- /dev/null +++ b/instrumentation/core/src/six/kotlin/de/mannodermaus/junit5/CoreConstants.kt @@ -0,0 +1,7 @@ +package de.mannodermaus.junit5 + +/** + * The minimum Android API level on which JUnit Framework tests may be executed. + * Trying to launch a test on an older device will simply mark it as 'skipped'. + */ +public const val JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION: Int = 35 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/build.gradle.kts b/instrumentation/extensions/build.gradle.kts index 0759bd5d..78f5642f 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) 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..ef65f627 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 @@ -12,17 +12,16 @@ import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext /** - * The [GrantPermissionExtension] allows granting of runtime permissions on Android M (API 23) - * and above. Use this extension when a test requires a runtime permission to do its work. + * The [GrantPermissionExtension] allows granting of runtime permissions before a test. + * Use this extension when a test requires a runtime permission to do its work. * * This is a port of JUnit 4's GrantPermissionRule for JUnit 5. * *

When applied to a test class it attempts to grant all requested runtime permissions. * The requested permissions will then be granted on the device and will take immediate effect. - * Permissions can only be requested on Android M (API 23) or above and will be ignored on all other - * API levels. Once a permission is granted it will apply for all tests running in the current - * Instrumentation. There is no way of revoking a permission after it was granted. Attempting to do - * so will crash the Instrumentation process. + * Once a permission is granted it will apply for all tests running in the current Instrumentation. + * There is no way of revoking a permission after it was granted. + * Attempting to do so will crash the Instrumentation process. */ @SuppressLint("RestrictedApi") public class GrantPermissionExtension @@ -37,7 +36,6 @@ internal constructor(private val permissionGranter: PermissionGranter) : BeforeE * * @see android.Manifest.permission */ - @ExperimentalTestApi @JvmStatic public fun grant(vararg permissions: String): GrantPermissionExtension { val granter = loadSingleService(PermissionGranter::class.java, ::PermissionRequester) @@ -51,7 +49,7 @@ internal constructor(private val permissionGranter: PermissionGranter) : BeforeE val set = LinkedHashSet(permissions.size + 1).also { it.addAll(permissions) } // Grant READ_EXTERNAL_STORAGE implicitly if its counterpart is present - if (Build.VERSION.SDK_INT >= 16 && Manifest.permission.WRITE_EXTERNAL_STORAGE in set) { + if (Manifest.permission.WRITE_EXTERNAL_STORAGE in set) { set.add(Manifest.permission.READ_EXTERNAL_STORAGE) } @@ -66,7 +64,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..5b28b015 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 { @@ -40,27 +42,18 @@ class GrantPermissionExtensionTests { @TestFactory fun `implicit addition of READ_EXTERNAL_STORAGE`(): List { // Run this test for every available Android OS version. - // For each version below API 16, no implicit addition of permissions should be done val latestApi = findLatestAndroidApiLevel() - val thresholdApi = 16 - - return (1..latestApi).map { api -> - val shouldAddPermission = api >= thresholdApi + return (26..latestApi).map { api -> dynamicTest("API $api") { withApiLevel(api) { runExtension(Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (shouldAddPermission) { - assertThat(granter.grantedPermissions) - .containsExactly( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE, - ).inOrder() - } else { - assertThat(granter.grantedPermissions) - .containsExactly(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } + assertThat(granter.grantedPermissions) + .containsExactly( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + ).inOrder() } } } @@ -81,8 +74,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 dfc8bb50..3f7f3aa7 100644 --- a/instrumentation/runner/build.gradle.kts +++ b/instrumentation/runner/build.gradle.kts @@ -1,109 +1,45 @@ -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.runner" - compileSdk = Android.compileSdkVersion - - defaultConfig { - minSdk = Android.testRunnerMinSdkVersion - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } + namespace = "de.mannodermaus.junit5.runner" - 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 - } + 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) + 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.launcher) + compileOnly(libs.junit.platform.suiteapi) + + testImplementation(project(":testutil")) + testImplementation(libs.robolectric) + testRuntimeOnly(libs.junit.jupiter.engine) } project.configureDeployment(Artifacts.Instrumentation.Runner) diff --git a/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt new file mode 100644 index 00000000..4cbe229f --- /dev/null +++ b/instrumentation/runner/src/five/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt @@ -0,0 +1,7 @@ +package de.mannodermaus.junit5.internal + +/** + * The minimum Android API level on which JUnit Framework tests may be executed. + * Trying to launch a test on an older device will simply mark it as 'skipped'. + */ +internal const val JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION: Int = 26 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/AndroidJUnit5Builder.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilder.kt similarity index 66% rename from instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnit5Builder.kt rename to instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilder.kt index dcb05d09..d5834aec 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnit5Builder.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilder.kt @@ -3,8 +3,8 @@ package de.mannodermaus.junit5 import android.util.Log import de.mannodermaus.junit5.internal.LOG_TAG import de.mannodermaus.junit5.internal.LibcoreAccess -import de.mannodermaus.junit5.internal.runners.AndroidJUnit5RunnerParams -import de.mannodermaus.junit5.internal.runners.tryCreateJUnit5Runner +import de.mannodermaus.junit5.internal.runners.JUnitFrameworkRunnerParams +import de.mannodermaus.junit5.internal.runners.tryCreateJUnitFrameworkRunner import org.junit.runner.Runner import org.junit.runners.model.RunnerBuilder @@ -22,7 +22,7 @@ import org.junit.runners.model.RunnerBuilder * android { * defaultConfig { * testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - * testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder") + * testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnitFrameworkBuilder") * } * } * @@ -30,10 +30,8 @@ import org.junit.runners.model.RunnerBuilder * (Suppressing unused, since this is hooked into the * project configuration via a Test Instrumentation Runner Argument.) */ -@Suppress("unused") -public class AndroidJUnit5Builder : RunnerBuilder() { - - private val junit5Available by lazy { +public open class AndroidJUnitFrameworkBuilder internal constructor() : RunnerBuilder() { + private val junitFrameworkAvailable by lazy { try { // The verification order of this block is quite important. // Do not change it without thorough testing of potential consequences! @@ -42,18 +40,18 @@ public class AndroidJUnit5Builder : RunnerBuilder() { // AND that integration with applications NOT using JUnit 5 for UI tests still works. // // First, verify the existence of junit-jupiter-api on the classpath. - // Then, verify that the Android JUnit 5 Runner is available. + // Then, verify that the Android JUnit Framework Runner is available. Class.forName("org.junit.jupiter.api.Test") - Class.forName("de.mannodermaus.junit5.internal.runners.AndroidJUnit5") + Class.forName("de.mannodermaus.junit5.internal.runners.AndroidJUnitFramework") true - } catch (e: Throwable) { + } catch (_: Throwable) { false } } // One-time parsing setup for runner params, taken from instrumentation arguments private val params by lazy { - AndroidJUnit5RunnerParams.create().also { params -> + JUnitFrameworkRunnerParams.create().also { params -> // Apply all environment variables & system properties to the running process params.registerEnvironmentVariables() params.registerSystemProperties() @@ -66,8 +64,8 @@ public class AndroidJUnit5Builder : RunnerBuilder() { if (testClass.isInIgnorablePackage) return null try { - return if (junit5Available) { - tryCreateJUnit5Runner(testClass) { params } + return if (junitFrameworkAvailable) { + tryCreateJUnitFrameworkRunner(testClass) { params } } else { null } @@ -99,7 +97,7 @@ public class AndroidJUnit5Builder : RunnerBuilder() { return ignorablePackages.any { name.startsWith(it) } } - private fun AndroidJUnit5RunnerParams.registerEnvironmentVariables() { + private fun JUnitFrameworkRunnerParams.registerEnvironmentVariables() { environmentVariables.forEach { (key, value) -> try { LibcoreAccess.setenv(key, value) @@ -109,7 +107,7 @@ public class AndroidJUnit5Builder : RunnerBuilder() { } } - private fun AndroidJUnit5RunnerParams.registerSystemProperties() { + private fun JUnitFrameworkRunnerParams.registerSystemProperties() { systemProperties.forEach { (key, value) -> try { System.setProperty(key, value) @@ -119,3 +117,31 @@ public class AndroidJUnit5Builder : RunnerBuilder() { } } } + +/** + * Custom RunnerBuilder hooked into the main Test Instrumentation Runner + * provided by the Android Test Support Library, which allows to run + * the JUnit Platform for instrumented tests. With this, + * the default JUnit 4-based Runner for Android instrumented tests is, + * in a way, tricked into detecting JUnit Jupiter tests as well. + * + * The RunnerBuilder is added to the instrumentation runner + * through a custom "testInstrumentationRunnerArgument" in the build.gradle script: + * + *

+ *   android {
+ *     defaultConfig {
+ *       testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ *       testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnitFrameworkBuilder")
+ *     }
+ *   }
+ * 
+ * + * (Suppressing unused, since this is hooked into the + * project configuration via a Test Instrumentation Runner Argument.) + */ +@Deprecated( + message = "Renamed to AndroidJUnitFrameworkBuilder", + replaceWith = ReplaceWith("AndroidJUnitFrameworkBuilder") +) +public class AndroidJUnit5Builder : AndroidJUnitFrameworkBuilder() diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/RunnerInternalConstants.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/RunnerInternalConstants.kt index 4eb0f23f..dbb1e3d1 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/RunnerInternalConstants.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/RunnerInternalConstants.kt @@ -1,3 +1,3 @@ package de.mannodermaus.junit5.internal -internal const val LOG_TAG = "AndroidJUnit5" +internal const val LOG_TAG = "AndroidJUnitFramework" 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..f3d44c2d 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,38 +1,26 @@ 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. - * Used by [de.mannodermaus.junit5.internal.runners.AndroidJUnit5] whenever a class + * Used by [de.mannodermaus.junit5.internal.runners.AndroidJUnitFramework] whenever a class * is not loadable through the JUnit Platform and should be discarded. */ @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/discovery/GeneratedFilters.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/GeneratedFilters.kt index d8d14293..2104f9b7 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/GeneratedFilters.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/GeneratedFilters.kt @@ -2,7 +2,7 @@ package de.mannodermaus.junit5.internal.discovery import android.content.Context import android.content.res.Resources -import de.mannodermaus.junit5.internal.runners.AndroidJUnit5 +import de.mannodermaus.junit5.internal.runners.AndroidJUnitFramework import org.junit.platform.engine.Filter import org.junit.platform.launcher.TagFilter @@ -11,11 +11,11 @@ private const val INSTRUMENTATION_FILTER_RES_FILE_NAME = "de_mannodermaus_junit5 /** * Holder object for the filters of a test plan. * It converts the contents of a resource file into JUnit Platform [Filter] objects - * for the [AndroidJUnit5] runner. + * for the [AndroidJUnitFramework] runner. */ internal object GeneratedFilters { - @Suppress("FoldInitializerAndIfToElvis", "DiscouragedApi") + @Suppress("DiscouragedApi") @JvmStatic fun fromContext(context: Context): List> { // Look up the resource file written by the Gradle plugin @@ -29,7 +29,7 @@ internal object GeneratedFilters { val inputStream = if (identifier != 0) { try { context.resources.openRawResource(identifier) - } catch (ignored: Resources.NotFoundException) { + } catch (_: Resources.NotFoundException) { // Ignore null } diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ParsedSelectors.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ParsedSelectors.kt index d0b4684c..3afbe5f3 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ParsedSelectors.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ParsedSelectors.kt @@ -1,7 +1,7 @@ package de.mannodermaus.junit5.internal.discovery import android.os.Bundle -import de.mannodermaus.junit5.internal.runners.AndroidJUnit5 +import de.mannodermaus.junit5.internal.runners.AndroidJUnitFramework import org.junit.platform.engine.DiscoverySelector import org.junit.platform.engine.discovery.DiscoverySelectors @@ -9,11 +9,9 @@ import org.junit.platform.engine.discovery.DiscoverySelectors * Holder object for the selectors of a test plan. * It converts the arguments handed to the Runner by the * Android instrumentation into JUnit Platform [DiscoverySelector] objects - * for the [AndroidJUnit5] runner. + * for the [AndroidJUnitFramework] runner. */ internal object ParsedSelectors { - - @JvmStatic fun fromBundle(testClass: Class<*>, arguments: Bundle): List { // Check if specific class arguments were given to the Runner arguments.getString("class", null)?.let { classArg -> diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParser.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParser.kt index f36cf2de..e69ab49c 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParser.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParser.kt @@ -1,7 +1,6 @@ package de.mannodermaus.junit5.internal.discovery internal object PropertiesParser { - @JvmStatic fun fromString(string: String) = string.split(",") .map { keyValuePair -> keyValuePair.split("=") } diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinder.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinder.kt index 69daac7c..8109c8ef 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinder.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinder.kt @@ -12,20 +12,31 @@ import java.lang.reflect.Modifier /** * Algorithm to find all methods annotated with a JUnit Jupiter annotation - * for devices running below API level 26 (i.e. those that cannot run Jupiter). - * We're unable to rely on JUnit Platform's own reflection utilities since they rely on Java 8 stuff + * for devices running below the API level requirement of the JUnit Framework. + * As they rely on Java 8 stuff, we're unable to rely on JUnit Platform's own reflection utilities. */ internal object JupiterTestMethodFinder { - private val jupiterTestAnnotations = listOf( - Test::class.java, - TestFactory::class.java, - RepeatedTest::class.java, - TestTemplate::class.java, - ParameterizedTest::class.java, - ) + // Carefully access the Jupiter annotations, since it's possible that they aren't on + // the runtime classpath (esp. "ParameterizedTest" could be absent if the consumer + // didn't include a dependency on junit-jupiter-params) + private val jupiterTestAnnotations = buildList { + addSafely { Test::class.java } + addSafely { TestFactory::class.java } + addSafely { RepeatedTest::class.java } + addSafely { TestTemplate::class.java } + addSafely { ParameterizedTest::class.java } + } fun find(cls: Class<*>): Set = cls.doFind(includeInherited = true) + private fun MutableList.addSafely(valueCreator: () -> T) { + try { + add(valueCreator()) + } catch (_: NoClassDefFoundError) { + // No-op + } + } + private fun Class<*>.doFind(includeInherited: Boolean): Set = buildSet { try { // Check each method in the Class for the presence 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/AndroidJUnit5.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFramework.kt similarity index 89% rename from instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5.kt rename to instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFramework.kt index b5d2b5ae..be6aef8e 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFramework.kt @@ -5,26 +5,21 @@ import androidx.annotation.VisibleForTesting import de.mannodermaus.junit5.internal.discovery.EmptyTestPlan import de.mannodermaus.junit5.internal.runners.notification.ParallelRunNotifier import org.junit.platform.commons.JUnitException -import org.junit.platform.engine.ConfigurationParameters import org.junit.platform.engine.discovery.MethodSelector -import org.junit.platform.launcher.TestPlan import org.junit.platform.launcher.core.LauncherFactory import org.junit.runner.Runner import org.junit.runner.notification.RunNotifier -import java.util.Optional /** * JUnit Runner implementation using the JUnit Platform as its backbone. * Serves as an intermediate solution to writing JUnit 5-based instrumentation tests * until official support arrives for this. - * - * @see org.junit.platform.runner.JUnitPlatform */ @RequiresApi(26) @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) -internal class AndroidJUnit5( +internal class AndroidJUnitFramework( private val testClass: Class<*>, - paramsSupplier: () -> AndroidJUnit5RunnerParams = AndroidJUnit5RunnerParams.Companion::create, + paramsSupplier: () -> JUnitFrameworkRunnerParams = JUnitFrameworkRunnerParams::create, ) : Runner() { private val launcher = LauncherFactory.create() private val testTree by lazy { generateTestTree(paramsSupplier()) } @@ -41,7 +36,7 @@ internal class AndroidJUnit5( /* Private */ - private fun generateTestTree(params: AndroidJUnit5RunnerParams): AndroidJUnitPlatformTestTree { + private fun generateTestTree(params: JUnitFrameworkRunnerParams): AndroidJUnitPlatformTestTree { val selectors = params.createSelectors(testClass) val isIsolatedMethodRun = selectors.size == 1 && selectors.first() is MethodSelector val isUsingOrchestrator = params.isUsingOrchestrator 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/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnit5.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnitFramework.kt similarity index 72% rename from instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnit5.kt rename to instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnitFramework.kt index bca9548f..c1261dee 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnit5.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnitFramework.kt @@ -2,6 +2,7 @@ package de.mannodermaus.junit5.internal.runners import android.os.Build import android.util.Log +import de.mannodermaus.junit5.internal.JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION import de.mannodermaus.junit5.internal.LOG_TAG import de.mannodermaus.junit5.internal.dummy.JupiterTestMethodFinder import org.junit.runner.Description @@ -10,18 +11,19 @@ import org.junit.runner.notification.RunNotifier import java.lang.reflect.Method /** - * Fake Runner that marks all JUnit 5 methods as ignored, - * used for old devices without Java 8 capabilities. + * Fake Runner that marks all JUnit Framework methods as ignored, + * used for old devices without the required Java capabilities. */ -internal class DummyJUnit5(private val testClass: Class<*>) : Runner() { +internal class DummyJUnitFramework(private val testClass: Class<*>) : Runner() { private val testMethods: Set = JupiterTestMethodFinder.find(testClass) override fun run(notifier: RunNotifier) { Log.w( LOG_TAG, - "JUnit 5 is not supported on this device: " + - "API level ${Build.VERSION.SDK_INT} is less than 26, the minimum requirement. " + + "JUnit Framework is not supported on this device: " + + "API level ${Build.VERSION.SDK_INT} is less than " + + "${JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION}, the minimum requirement. " + "All Jupiter tests for ${testClass.name} will be disabled." ) diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnit5RunnerFactory.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnit5RunnerFactory.kt deleted file mode 100644 index 4e63c672..00000000 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnit5RunnerFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -package de.mannodermaus.junit5.internal.runners - -import android.os.Build -import org.junit.runner.Runner - -/** - * Since we can't reference AndroidJUnit5 directly, use this factory for instantiation. - * - * On API 26 and above, delegate to the real implementation to drive JUnit 5 tests. - * Below that however, they wouldn't work; for this case, delegate a dummy runner - * which will highlight these tests as ignored. - */ -internal fun tryCreateJUnit5Runner( - klass: Class<*>, - paramsSupplier: () -> AndroidJUnit5RunnerParams -): Runner? { - val runner = if (Build.VERSION.SDK_INT >= 26) { - AndroidJUnit5(klass, paramsSupplier) - } else { - DummyJUnit5(klass) - } - - // It's still possible for the runner to not be relevant to the test run, - // which is related to how further filters are applied (e.g. via @Tag). - // Only return the runner to the instrumentation if it has any tests to contribute, - // otherwise there would be a mismatch between the number of test classes reported - // to Android, and the number of test classes actually tested with JUnit 5 (ref #298) - return runner.takeIf(Runner::hasExecutableTests) -} - -private fun Runner.hasExecutableTests() = - this.description.children.isNotEmpty() diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerFactory.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerFactory.kt new file mode 100644 index 00000000..2dd7067f --- /dev/null +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerFactory.kt @@ -0,0 +1,33 @@ +package de.mannodermaus.junit5.internal.runners + +import android.os.Build +import de.mannodermaus.junit5.internal.JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION +import org.junit.runner.Runner + +/** + * Since we can't reference [AndroidJUnitFramework] directly, use this factory for instantiation. + * + * On devices with sufficient API levels, delegate to the real implementation to drive + * the execution of JUnit Framework tests. Below this threshold, they wouldn't work, however; + * for this case, delegate to a dummy runner which will highlight these tests as ignored. + */ +internal fun tryCreateJUnitFrameworkRunner( + klass: Class<*>, + paramsSupplier: () -> JUnitFrameworkRunnerParams +): Runner? { + val runner = if (Build.VERSION.SDK_INT >= JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION) { + AndroidJUnitFramework(klass, paramsSupplier) + } else { + DummyJUnitFramework(klass) + } + + // It's still possible for the runner to not be relevant to the test run, + // which is related to how further filters are applied (e.g. via @Tag). + // Only return the runner to the instrumentation if it has any tests to contribute, + // otherwise there would be a mismatch between the number of test classes reported + // to Android, and the number of test classes actually tested with JUnit (ref #298) + return runner.takeIf(Runner::hasExecutableTests) +} + +private fun Runner.hasExecutableTests() = + this.description.children.isNotEmpty() diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5RunnerParams.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerParams.kt similarity index 95% rename from instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5RunnerParams.kt rename to instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerParams.kt index 64d8fe6a..0d9cfb7d 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5RunnerParams.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnitFrameworkRunnerParams.kt @@ -11,7 +11,7 @@ import org.junit.platform.engine.Filter import org.junit.platform.launcher.LauncherDiscoveryRequest import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder -internal data class AndroidJUnit5RunnerParams( +internal data class JUnitFrameworkRunnerParams( private val arguments: Bundle = Bundle(), private val filters: List> = emptyList(), val environmentVariables: Map = emptyMap(), @@ -37,7 +37,7 @@ internal data class AndroidJUnit5RunnerParams( get() = arguments.getString("orchestratorService") != null internal companion object { - fun create(): AndroidJUnit5RunnerParams { + fun create(): JUnitFrameworkRunnerParams { val instrumentation = InstrumentationRegistry.getInstrumentation() val arguments = InstrumentationRegistry.getArguments() @@ -63,7 +63,7 @@ internal data class AndroidJUnit5RunnerParams( val filters = GeneratedFilters.fromContext(instrumentation.context) + listOfNotNull(ShardingFilter.fromArguments(arguments)) - return AndroidJUnit5RunnerParams( + return JUnitFrameworkRunnerParams( arguments, filters, environmentVariables, diff --git a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/notification/ParallelRunNotifier.kt b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/notification/ParallelRunNotifier.kt index f27a5332..4229a76c 100644 --- a/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/notification/ParallelRunNotifier.kt +++ b/instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/notification/ParallelRunNotifier.kt @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit */ internal class ParallelRunNotifier(private val delegate: RunNotifier) : RunNotifier() { private companion object { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") private val doneLock = Object() private val nopPrinter = InstrumentationResultPrinter() private val nopTestState = TestState("", Bundle(), 0) @@ -258,7 +259,7 @@ internal class ParallelRunNotifier(private val delegate: RunNotifier) : RunNotif ignoreQueue.forEach(::sendEvent) ignoreQueue.clear() } - } catch (ignored: InterruptedException) { + } catch (_: InterruptedException) { // OK while (startQueue.isNotEmpty()) { val startEvent = startQueue.take() diff --git a/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt new file mode 100644 index 00000000..8ef01d5c --- /dev/null +++ b/instrumentation/runner/src/six/kotlin/de/mannodermaus/junit5/internal/RunnerConstants.kt @@ -0,0 +1,7 @@ +package de.mannodermaus.junit5.internal + +/** + * The minimum Android API level on which JUnit Framework tests may be executed. + * Trying to launch a test on an older device will simply mark it as 'skipped'. + */ +internal const val JUNIT_FRAMEWORK_MINIMUM_SDK_VERSION: Int = 35 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/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnit5BuilderTests.kt b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilderTests.kt similarity index 96% rename from instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnit5BuilderTests.kt rename to instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilderTests.kt index 407293d3..6fc73aa0 100644 --- a/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnit5BuilderTests.kt +++ b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnitFrameworkBuilderTests.kt @@ -9,9 +9,9 @@ import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.TestFactory -class AndroidJUnit5BuilderTests { +class AndroidJUnitFrameworkBuilderTests { - private val builder = AndroidJUnit5Builder() + private val builder = AndroidJUnitFrameworkBuilder() @TestFactory fun `no runner is created if class only contains top-level test methods`() = runTest( diff --git a/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinderTests.kt b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinderTests.kt index 3207e8b6..b3d684e1 100644 --- a/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinderTests.kt +++ b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinderTests.kt @@ -16,8 +16,8 @@ import de.mannodermaus.junit5.HasTaggedTest import de.mannodermaus.junit5.HasTest import de.mannodermaus.junit5.HasTestFactory import de.mannodermaus.junit5.HasTestTemplate -import de.mannodermaus.junit5.internal.runners.AndroidJUnit5 -import de.mannodermaus.junit5.internal.runners.AndroidJUnit5RunnerParams +import de.mannodermaus.junit5.internal.runners.AndroidJUnitFramework +import de.mannodermaus.junit5.internal.runners.JUnitFrameworkRunnerParams import org.junit.jupiter.api.DynamicContainer.dynamicContainer import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest.dynamicTest @@ -99,8 +99,8 @@ class JupiterTestMethodFinderTests { val listener = CountingRunListener() notifier.addListener(listener) - val params = AndroidJUnit5RunnerParams(filters = listOfNotNull(filter)) - AndroidJUnit5(cls) { params }.run(notifier) + val params = JUnitFrameworkRunnerParams(filters = listOfNotNull(filter)) + AndroidJUnitFramework(cls) { params }.run(notifier) return listener } diff --git a/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5Tests.kt b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFrameworkTests.kt similarity index 97% rename from instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5Tests.kt rename to instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFrameworkTests.kt index 3ce19bac..6f571535 100644 --- a/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnit5Tests.kt +++ b/instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/runners/AndroidJUnitFrameworkTests.kt @@ -18,7 +18,7 @@ import org.robolectric.RobolectricTestRunner import java.util.concurrent.atomic.AtomicReference @RunWith(RobolectricTestRunner::class) -class AndroidJUnit5Tests { +class AndroidJUnitFrameworkTests { @org.junit.Test fun `successful tests are reported correctly`() { @@ -80,7 +80,7 @@ class AndroidJUnit5Tests { val resultRef = AtomicReference() val args = buildArgs(shardingConfig) withMockedInstrumentation(args) { - val runner = AndroidJUnit5(Sample_NormalTests::class.java) + val runner = AndroidJUnitFramework(Sample_NormalTests::class.java) val listener = CollectingRunListener() val notifier = RunNotifier().also { it.addListener(listener) } runner.run(notifier) diff --git a/instrumentation/sample/build.gradle.kts b/instrumentation/sample/build.gradle.kts index ae01ce8d..64d2201d 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.AndroidJUnitFrameworkBuilder" + 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/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