├── .circleci └── config.yml ├── .github └── workflows │ └── gradle-wrapper-validation.yml ├── .gitignore ├── .images ├── logo.png ├── logo.psd └── wiki │ ├── instrumentation-disabled.png │ └── instrumentation-enabled.png ├── LICENSE ├── README.md ├── README.md.template ├── build-logic ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── Dependencies.kt │ │ ├── Deployment.kt │ │ ├── Environment.kt │ │ ├── Tasks.kt │ │ └── Utilities.kt │ └── test │ └── java │ └── FindInstrumentationVersionTests.java ├── instrumentation ├── .idea │ ├── codeStyles │ │ └── Project.xml │ ├── icon.png │ └── runConfigurations │ │ ├── Check_Dependency_Updates.xml │ │ ├── Compose__Run_Instrumentation_Tests.xml │ │ ├── Core__Run_Instrumentation_Tests.xml │ │ ├── Extensions__Run_Unit_Tests__Gradle_.xml │ │ ├── Instrumentation__Publish_Release_Manually.xml │ │ ├── Instrumentation__Update_Public_API_File.xml │ │ ├── Runner__Run_Unit_Tests__Gradle_.xml │ │ ├── Sample__Run_Instrumentation_Tests.xml │ │ └── Sample__Run_Unit_Tests__Gradle_.xml ├── CHANGELOG.md ├── build.gradle.kts ├── buildSrc │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── ExplicitApiModePlugin.kt ├── compose │ ├── api │ │ └── compose.api │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ └── compose │ │ │ ├── ClassComposeExtensionTests.kt │ │ │ ├── ExistingActivityComposeExtensionTests.kt │ │ │ └── FieldComposeExtensionTests.kt │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ └── compose │ │ │ └── ExistingActivity.kt │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ └── compose │ │ │ ├── AndroidComposeExtension.kt │ │ │ ├── ComposeContext.kt │ │ │ └── ComposeExtension.kt │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── junit5 │ │ └── compose │ │ └── ComposeContextTests.kt ├── core │ ├── api │ │ └── core.api │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ ├── BaselineJUnit4Test.kt │ │ │ ├── JavaInstrumentationTests.java │ │ │ ├── KotlinInstrumentationTests.kt │ │ │ ├── TaggedTests.kt │ │ │ ├── inheritance │ │ │ ├── JavaAbstractClass.java │ │ │ ├── JavaAbstractClassTest.java │ │ │ ├── JavaInterface.java │ │ │ ├── JavaInterfaceTest.java │ │ │ ├── JavaMixedInterfaceTest.java │ │ │ └── KotlinInheritanceTests.kt │ │ │ └── otherpackage │ │ │ └── AnotherTest.kt │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── de │ │ │ │ └── mannodermaus │ │ │ │ └── junit5 │ │ │ │ └── TestActivity.kt │ │ └── res │ │ │ └── layout │ │ │ └── activity_test.xml │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ ├── ActivityScenarioExtension.kt │ │ │ ├── CoreConstants.kt │ │ │ ├── condition │ │ │ ├── DisabledIfBuildConfigValue.kt │ │ │ ├── DisabledOnManufacturer.kt │ │ │ ├── DisabledOnSdkVersion.kt │ │ │ ├── EnabledIfBuildConfigValue.kt │ │ │ ├── EnabledOnManufacturer.kt │ │ │ └── EnabledOnSdkVersion.kt │ │ │ └── internal │ │ │ ├── CoreInternalConstants.kt │ │ │ ├── DisabledIfBuildConfigValueCondition.kt │ │ │ ├── DisabledOnManufacturerCondition.kt │ │ │ ├── DisabledOnSdkVersionCondition.kt │ │ │ ├── EnabledIfBuildConfigValueCondition.kt │ │ │ ├── EnabledOnManufacturerCondition.kt │ │ │ ├── EnabledOnSdkVersionCondition.kt │ │ │ └── utils │ │ │ └── BuildConfigValueUtils.kt │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── junit5 │ │ ├── condition │ │ ├── AbstractExecutionConditionTests.kt │ │ ├── DisabledIfBuildConfigValueConditionTests.kt │ │ ├── DisabledIfBuildConfigValueIntegrationTests.kt │ │ ├── DisabledOnManufacturerConditionTests.kt │ │ ├── DisabledOnManufacturerIntegrationTests.kt │ │ ├── DisabledOnSdkVersionConditionTests.kt │ │ ├── DisabledOnSdkVersionIntegrationTests.kt │ │ ├── EnabledIfBuildConfigValueConditionTests.kt │ │ ├── EnabledIfBuildConfigValueIntegrationTests.kt │ │ ├── EnabledOnManufacturerConditionTests.kt │ │ ├── EnabledOnManufacturerIntegrationTests.kt │ │ ├── EnabledOnSdkVersionConditionTests.kt │ │ └── EnabledOnSdkVersionIntegrationTests.kt │ │ └── util │ │ ├── BuildConfig.java │ │ └── ResourceLocks.kt ├── extensions │ ├── api │ │ └── extensions.api │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ └── extensions │ │ │ └── GrantPermissionExtension.kt │ │ └── test │ │ └── kotlin │ │ └── de │ │ └── mannodermaus │ │ └── junit5 │ │ └── extensions │ │ └── GrantPermissionExtensionTests.kt ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── runner │ ├── api │ │ └── runner.api │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── junit5 │ │ │ ├── AndroidJUnit5Builder.kt │ │ │ └── internal │ │ │ ├── LibcoreAccess.kt │ │ │ ├── RunnerInternalConstants.kt │ │ │ ├── discovery │ │ │ ├── EmptyTestPlan.kt │ │ │ ├── GeneratedFilters.kt │ │ │ ├── ParsedSelectors.kt │ │ │ ├── PropertiesParser.kt │ │ │ └── ShardingFilter.kt │ │ │ ├── dummy │ │ │ └── JupiterTestMethodFinder.kt │ │ │ ├── extensions │ │ │ └── TestIdentifierExt.kt │ │ │ ├── formatters │ │ │ └── TestNameFormatter.kt │ │ │ └── runners │ │ │ ├── AndroidJUnit5.kt │ │ │ ├── AndroidJUnit5RunnerParams.kt │ │ │ ├── AndroidJUnitPlatformRunnerListener.kt │ │ │ ├── AndroidJUnitPlatformTestTree.kt │ │ │ ├── DummyJUnit5.kt │ │ │ ├── JUnit5RunnerFactory.kt │ │ │ └── notification │ │ │ ├── FilteredRunListener.kt │ │ │ └── ParallelRunNotifier.kt │ │ └── test │ │ └── kotlin │ │ └── de │ │ └── mannodermaus │ │ └── junit5 │ │ ├── AndroidJUnit5BuilderTests.kt │ │ ├── TestClasses.kt │ │ ├── TestHelpers.kt │ │ └── internal │ │ ├── discovery │ │ └── PropertiesParserTests.kt │ │ ├── dummy │ │ └── JupiterTestMethodFinderTests.kt │ │ ├── formatters │ │ └── TestNameFormatterTests.kt │ │ └── runners │ │ ├── AndroidJUnit5Tests.kt │ │ └── AndroidJUnitPlatformTestTreeTests.kt ├── sample │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── sample │ │ │ ├── ActivityOneTest.kt │ │ │ ├── TestRunningOnJUnit4.kt │ │ │ ├── TestRunningOnJUnit5.kt │ │ │ └── TestTemplateExampleTests.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── de │ │ │ │ └── mannodermaus │ │ │ │ └── junit5 │ │ │ │ └── sample │ │ │ │ └── ActivityOne.kt │ │ └── res │ │ │ └── layout │ │ │ └── activity_one.xml │ │ └── test │ │ ├── java │ │ └── de │ │ │ └── mannodermaus │ │ │ └── sample │ │ │ └── ExampleJavaTest.java │ │ └── kotlin │ │ └── de │ │ └── mannodermaus │ │ └── sample │ │ └── ExampleKotlinTest.kt ├── settings.gradle.kts ├── testutil-reflect │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── de │ │ └── mannodermaus │ │ └── junit5 │ │ └── testutil │ │ └── reflect │ │ └── Reflections.kt └── testutil │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── de │ └── mannodermaus │ └── junit5 │ └── testutil │ ├── AndroidBuildUtils.kt │ ├── CollectingRunListener.kt │ └── StubInstrumentation.kt └── plugin ├── .idea ├── codeStyles │ └── codeStyleConfig.xml ├── icon.png └── runConfigurations │ ├── Check_Dependency_Updates.xml │ ├── Generate_README_md.xml │ ├── Plugin__Build_Fat_JAR.xml │ ├── Plugin__Publish_Release_Manually.xml │ ├── Plugin__Run_Unit_Tests.xml │ └── Plugin__Update_Public_API_File.xml ├── CHANGELOG.md ├── android-junit5 ├── api │ └── android-junit5.api ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── de │ │ │ └── mannodermaus │ │ │ └── gradle │ │ │ └── plugins │ │ │ └── junit5 │ │ │ ├── AndroidJUnitPlatformPlugin.kt │ │ │ ├── dsl │ │ │ ├── AndroidJUnitPlatformExtension.kt │ │ │ ├── FiltersExtension.kt │ │ │ ├── InstrumentationTestOptions.kt │ │ │ └── JacocoOptions.kt │ │ │ ├── internal │ │ │ ├── config │ │ │ │ ├── Constants.kt │ │ │ │ ├── JUnit5TaskConfig.kt │ │ │ │ └── PluginConfig.kt │ │ │ ├── configureJUnit5.kt │ │ │ ├── extensions │ │ │ │ ├── BaseVariantExt.kt │ │ │ │ ├── ConfigurableReportExt.kt │ │ │ │ ├── ExtensionAwareExt.kt │ │ │ │ ├── LoggerExt.kt │ │ │ │ ├── MapExt.kt │ │ │ │ ├── ProjectExt.kt │ │ │ │ ├── StringExt.kt │ │ │ │ ├── TaskContainerExt.kt │ │ │ │ └── VariantExt.kt │ │ │ ├── providers │ │ │ │ ├── DirectoryProvider.kt │ │ │ │ ├── JavaDirectoryProvider.kt │ │ │ │ └── KotlinDirectoryProvider.kt │ │ │ └── utils │ │ │ │ ├── Functions.kt │ │ │ │ └── IncludeExcludeContainer.kt │ │ │ └── tasks │ │ │ ├── AndroidJUnit5JacocoReport.kt │ │ │ └── AndroidJUnit5WriteFilters.kt │ └── templates │ │ └── Libraries.kt │ └── test │ ├── groovy │ └── de │ │ └── mannodermaus │ │ └── gradle │ │ └── plugins │ │ └── junit5 │ │ └── DslGroovyTests.groovy │ ├── kotlin │ └── de │ │ └── mannodermaus │ │ └── gradle │ │ └── plugins │ │ └── junit5 │ │ ├── ConfigurationCacheTests.kt │ │ ├── FunctionalTests.kt │ │ ├── IncludeExcludeContainerTests.kt │ │ ├── VersionCheckerTests.kt │ │ ├── annotations │ │ └── DisabledOnCI.kt │ │ ├── plugin │ │ ├── AbstractProjectTests.kt │ │ ├── AgpConfigurationParameterTests.kt │ │ ├── AgpFilterTests.kt │ │ ├── AgpInstrumentationSupportTests.kt │ │ ├── AgpJacocoBaseTests.kt │ │ ├── AgpJacocoExclusionRuleTests.kt │ │ ├── AgpJacocoVariantTests.kt │ │ ├── AgpProjectTests.kt │ │ ├── AgpTests.kt │ │ ├── AgpVariantTests.kt │ │ ├── InstrumentationSupportTests.kt │ │ ├── TestProjectProviderExtension.kt │ │ └── WrongPluginUsageTests.kt │ │ ├── tasks │ │ └── AndroidJUnit5WriteFiltersTests.kt │ │ └── util │ │ ├── GradleTruth.kt │ │ ├── TestEnvironment.kt │ │ ├── TestExtensions.kt │ │ ├── TestKitTruth.kt │ │ ├── TestedAgp.kt │ │ └── projects │ │ ├── BuildScriptTemplateProcessor.kt │ │ ├── BuildScriptTemplateProcessorTests.kt │ │ ├── FunctionalTestProjectCreator.kt │ │ ├── PluginSpecProjectCreator.kt │ │ ├── SemanticVersion.kt │ │ └── SemanticVersionTests.kt │ └── resources │ ├── de │ └── mannodermaus │ │ └── gradle │ │ └── plugins │ │ └── junit5 │ │ └── testenv.properties │ └── test-projects │ ├── build.gradle.kts.template │ ├── custom-build-type │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaTest.java │ │ ├── testRelease │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaReleaseTest.java │ │ └── testStaging │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── JavaStagingTest.java │ ├── default-values │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ ├── AndroidTest.java │ │ └── JavaTest.java │ ├── include-android-resources │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── AndroidTest.java │ ├── instrumentation-tests │ ├── config.toml │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── InstrumentationTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── AndroidTest.java │ ├── jacoco │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ └── test │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── AdderTest.java │ ├── new-variant-api │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaTest.java │ │ ├── testFreeRelease │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaFreeReleaseTest.java │ │ ├── testPaidDebug │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── KotlinPaidDebugTest.kt │ │ └── testRelease │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── KotlinReleaseTest.kt │ ├── product-flavors │ ├── config.toml │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── Adder.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaTest.java │ │ ├── testFreeRelease │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── JavaFreeReleaseTest.java │ │ ├── testPaidDebug │ │ └── java │ │ │ └── de │ │ │ └── mannodermaus │ │ │ └── app │ │ │ └── KotlinPaidDebugTest.kt │ │ └── testRelease │ │ └── java │ │ └── de │ │ └── mannodermaus │ │ └── app │ │ └── KotlinReleaseTest.kt │ └── settings.gradle.kts.template ├── build.gradle.kts ├── buildSrc └── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | validation: 6 | name: "Validation" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: gradle/wrapper-validation-action@v1 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | */.idea/* 5 | !*/.idea/codeStyles/ 6 | !*/.idea/runConfigurations 7 | !*/.idea/scopes/ 8 | !*/.idea/fileColors.xml 9 | !*/.idea/icon.* 10 | .DS_Store 11 | build/ 12 | out/ 13 | captures/ 14 | -------------------------------------------------------------------------------- /.images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/.images/logo.png -------------------------------------------------------------------------------- /.images/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/.images/logo.psd -------------------------------------------------------------------------------- /.images/wiki/instrumentation-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/.images/wiki/instrumentation-disabled.png -------------------------------------------------------------------------------- /.images/wiki/instrumentation-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/.images/wiki/instrumentation-enabled.png -------------------------------------------------------------------------------- /build-logic/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .gradle/ 3 | build/ 4 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | java 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation(gradleApi()) 12 | testImplementation("junit:junit:+") 13 | } 14 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/build-logic/settings.gradle.kts -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/Utilities.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | import org.gradle.api.artifacts.dsl.RepositoryHandler 3 | import org.gradle.api.file.DirectoryProperty 4 | import org.gradle.api.tasks.compile.AbstractCompile 5 | import org.gradle.kotlin.dsl.withGroovyBuilder 6 | import java.io.File 7 | import java.time.Instant 8 | import java.time.ZoneId 9 | import java.time.format.DateTimeFormatter 10 | 11 | /* RepositoryHandler */ 12 | 13 | fun RepositoryHandler.jitpack() = maven { 14 | setUrl("https://jitpack.io") 15 | } 16 | 17 | fun RepositoryHandler.sonatypeSnapshots() = maven { 18 | setUrl("https://oss.sonatype.org/content/repositories/snapshots") 19 | } 20 | 21 | /* Project */ 22 | 23 | fun Project.fixCompileTaskChain() { 24 | setupCompileChain( 25 | sourceCompileName = "compileKotlin", 26 | targetCompileName = "compileGroovy" 27 | ) 28 | 29 | setupCompileChain( 30 | sourceCompileName = "compileTestKotlin", 31 | targetCompileName = "compileTestGroovy" 32 | ) 33 | } 34 | 35 | /** 36 | * @param sourceCompileName The sources in this task may call into the target 37 | * @param targetCompileName The sources in this task must not call into the source 38 | */ 39 | private fun Project.setupCompileChain( 40 | sourceCompileName: String, 41 | targetCompileName: String 42 | ) { 43 | val targetCompile = tasks.getByName(targetCompileName) as AbstractCompile 44 | val sourceCompile = tasks.getByName(sourceCompileName) 45 | 46 | // Allow calling the source language's classes from the target language. 47 | // In this case, we allow calling Kotlin from Groovy - it has to be noted however, 48 | // that the other way does not work! 49 | val sourceDir = sourceCompile.withGroovyBuilder { getProperty("destinationDirectory") } as DirectoryProperty 50 | targetCompile.classpath += project.files(sourceDir.get().asFile) 51 | } 52 | -------------------------------------------------------------------------------- /build-logic/src/test/java/FindInstrumentationVersionTests.java: -------------------------------------------------------------------------------- 1 | import org.junit.Test; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | public class FindInstrumentationVersionTests { 6 | @Test 7 | public void findCorrectVersionForSnapshotPlugin() { 8 | String actual = TasksKt.findInstrumentationVersion( 9 | "plugin-1.0-SNAPSHOT", 10 | "instrumentation-2.0-SNAPSHOT", 11 | "instrumentation-1.0" 12 | ); 13 | 14 | assertEquals("instrumentation-2.0-SNAPSHOT", actual); 15 | } 16 | 17 | @Test 18 | public void findCorrectVersionForStablePluginAndStableInstrumentation() { 19 | String actual = TasksKt.findInstrumentationVersion( 20 | "plugin-1.0", 21 | "instrumentation-2.0", 22 | "instrumentation-1.0" 23 | ); 24 | 25 | assertEquals("instrumentation-2.0", actual); 26 | } 27 | 28 | @Test 29 | public void findCorrectVersionForStablePluginAndSnapshotInstrumentation() { 30 | String actual = TasksKt.findInstrumentationVersion( 31 | "plugin-1.0", 32 | "instrumentation-2.0-SNAPSHOT", 33 | "instrumentation-1.0" 34 | ); 35 | 36 | assertEquals("instrumentation-1.0", actual); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /instrumentation/.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/instrumentation/.idea/icon.png -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Check_Dependency_Updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Extensions__Run_Unit_Tests__Gradle_.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Instrumentation__Publish_Release_Manually.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Instrumentation__Update_Public_API_File.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Runner__Run_Unit_Tests__Gradle_.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /instrumentation/.idea/runConfigurations/Sample__Run_Unit_Tests__Gradle_.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /instrumentation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.github.gradle-nexus.publish-plugin").version(libs.versions.nexusPublish) 3 | id("org.jetbrains.kotlinx.binary-compatibility-validator").version(libs.versions.kotlinxBinaryCompatibilityValidator) 4 | } 5 | 6 | buildscript { 7 | dependencies { 8 | classpath(libs.plugins.kotlin) 9 | classpath(libs.plugins.dokka) 10 | classpath(libs.plugins.composeCompiler) 11 | classpath(libs.plugins.android(SupportedAgp.newestStable)) 12 | } 13 | } 14 | 15 | apiValidation { 16 | ignoredPackages.add("de.mannodermaus.junit5.internal") 17 | ignoredPackages.add("de.mannodermaus.junit5.compose.internal") 18 | ignoredProjects.add("sample") 19 | ignoredProjects.add("testutil") 20 | ignoredProjects.add("testutil-reflect") 21 | } 22 | -------------------------------------------------------------------------------- /instrumentation/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | sourceSets { 10 | main { 11 | java.srcDir(file("../../build-logic/src/main/kotlin")) 12 | } 13 | } 14 | 15 | gradlePlugin { 16 | plugins { 17 | register("explicit-api-mode") { 18 | id = "explicit-api-mode" 19 | implementationClass = "ExplicitApiModePlugin" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /instrumentation/buildSrc/src/main/java/ExplicitApiModePlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.Task 4 | import org.gradle.kotlin.dsl.withGroovyBuilder 5 | 6 | // Based on code by Chao Zhang, but updated to avoid direct dependency on Kotlin Gradle plugin: 7 | // https://youtrack.jetbrains.com/issue/KT-37652 8 | 9 | private const val EXPLICIT_API = "-Xexplicit-api=strict" 10 | 11 | class ExplicitApiModePlugin : Plugin { 12 | override fun apply(project: Project) { 13 | project.tasks 14 | .matching { it.isKotlinCompileTask() } 15 | .configureEach { 16 | if (!project.hasProperty("kotlin.optOutExplicitApi")) { 17 | enableExplicitApiMode() 18 | } 19 | } 20 | } 21 | 22 | private fun Task.isKotlinCompileTask(): Boolean { 23 | return "org.jetbrains.kotlin.gradle.tasks.KotlinCompile" in javaClass.name && 24 | !name.contains("test", ignoreCase = true) 25 | } 26 | 27 | @Suppress("UNCHECKED_CAST") 28 | private fun Task.enableExplicitApiMode() { 29 | withGroovyBuilder { 30 | "kotlinOptions" { 31 | val freeCompilerArgs = getProperty("freeCompilerArgs") as Collection 32 | if (EXPLICIT_API !in freeCompilerArgs) { 33 | invokeMethod("setFreeCompilerArgs", freeCompilerArgs + listOf(EXPLICIT_API)) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /instrumentation/compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | 4 | plugins { 5 | id("com.android.library") 6 | kotlin("android") 7 | id("explicit-api-mode") 8 | id("de.mannodermaus.android-junit5").version(Artifacts.Plugin.latestStableVersion) 9 | id("org.jetbrains.kotlin.plugin.compose") 10 | } 11 | 12 | val javaVersion = JavaVersion.VERSION_11 13 | 14 | android { 15 | namespace = "de.mannodermaus.junit5.compose" 16 | compileSdk = Android.compileSdkVersion 17 | 18 | defaultConfig { 19 | minSdk = Android.testComposeMinSdkVersion 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" 23 | } 24 | 25 | buildFeatures { 26 | compose = true 27 | buildConfig = false 28 | resValues = false 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility = javaVersion 33 | targetCompatibility = javaVersion 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = javaVersion.toString() 38 | } 39 | 40 | testOptions { 41 | unitTests.isReturnDefaultValues = true 42 | targetSdk = Android.targetSdkVersion 43 | } 44 | 45 | lint { 46 | targetSdk = Android.targetSdkVersion 47 | } 48 | 49 | packaging { 50 | resources.excludes.add("META-INF/AL2.0") 51 | resources.excludes.add("META-INF/LGPL2.1") 52 | } 53 | } 54 | 55 | junitPlatform { 56 | // Using local dependency instead of Maven coordinates 57 | instrumentationTests.enabled = false 58 | } 59 | 60 | tasks.withType { 61 | failFast = true 62 | testLogging { 63 | events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) 64 | exceptionFormat = TestExceptionFormat.FULL 65 | } 66 | } 67 | 68 | dependencies { 69 | implementation(project(":core")) 70 | implementation(libs.kotlinStdLib) 71 | implementation(libs.kotlinCoroutinesCore) 72 | 73 | implementation(libs.junitJupiterApi) 74 | implementation(libs.junit4) 75 | implementation(libs.espressoCore) 76 | 77 | implementation(platform(libs.composeBom)) 78 | implementation(libs.composeActivity) 79 | implementation(libs.composeUi) 80 | implementation(libs.composeUiTooling) 81 | implementation(libs.composeFoundation) 82 | implementation(libs.composeMaterial) 83 | api(libs.composeUiTest) 84 | api(libs.composeUiTestJUnit4) 85 | implementation(libs.composeUiTestManifest) 86 | 87 | testImplementation(libs.junitJupiterApi) 88 | testImplementation(libs.junitJupiterParams) 89 | testRuntimeOnly(libs.junitJupiterEngine) 90 | 91 | androidTestImplementation(libs.junitJupiterApi) 92 | androidTestImplementation(libs.junitJupiterParams) 93 | androidTestImplementation(libs.espressoCore) 94 | 95 | androidTestRuntimeOnly(project(":runner")) 96 | androidTestRuntimeOnly(libs.androidXTestRunner) 97 | } 98 | 99 | project.configureDeployment(Artifacts.Instrumentation.Compose) 100 | -------------------------------------------------------------------------------- /instrumentation/compose/src/androidTest/java/de/mannodermaus/junit5/compose/ClassComposeExtensionTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material.Button 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.test.assertIsDisplayed 12 | import androidx.compose.ui.test.onNodeWithText 13 | import androidx.compose.ui.test.performClick 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.junit.jupiter.params.ParameterizedTest 17 | import org.junit.jupiter.params.provider.ValueSource 18 | 19 | @ExtendWith(AndroidComposeExtension::class) 20 | class ClassComposeExtensionTests { 21 | 22 | @ValueSource( 23 | strings = [ 24 | "click me", 25 | "touch me", 26 | "jfc it actually works" 27 | ] 28 | ) 29 | @ParameterizedTest 30 | fun test(buttonLabel: String, extension: AndroidComposeExtension) = 31 | extension.use { 32 | setContent { 33 | Column { 34 | var counter by remember { mutableStateOf(0) } 35 | 36 | Text(text = "Clicked: $counter") 37 | Button(onClick = { counter++ }) { 38 | Text(text = buttonLabel) 39 | } 40 | } 41 | } 42 | 43 | onNodeWithText("Clicked: 0").assertIsDisplayed() 44 | onNodeWithText(buttonLabel).performClick() 45 | onNodeWithText("Clicked: 1").assertIsDisplayed() 46 | } 47 | 48 | @Test 49 | fun anotherTest(extension: ComposeExtension) = extension.use { 50 | setContent { 51 | Column { 52 | var showDetails by remember { mutableStateOf(false) } 53 | 54 | Text("Hello world") 55 | if (showDetails) { 56 | Text("Extra details") 57 | } 58 | 59 | Button(onClick = { showDetails = !showDetails }) { 60 | Text("click") 61 | } 62 | } 63 | } 64 | 65 | onNodeWithText("Extra details").assertDoesNotExist() 66 | onNodeWithText("click").performClick() 67 | onNodeWithText("Extra details").assertIsDisplayed() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /instrumentation/compose/src/androidTest/java/de/mannodermaus/junit5/compose/ExistingActivityComposeExtensionTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import androidx.compose.ui.test.ExperimentalTestApi 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.onNodeWithText 6 | import androidx.compose.ui.test.performClick 7 | import org.junit.jupiter.api.BeforeAll 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import org.junit.jupiter.api.TestInstance 11 | import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS 12 | import org.junit.jupiter.api.extension.RegisterExtension 13 | 14 | @TestInstance(PER_CLASS) 15 | class ExistingActivityComposeExtensionTests { 16 | 17 | @JvmField 18 | @RegisterExtension 19 | @OptIn(ExperimentalTestApi::class) 20 | val extension = createAndroidComposeExtension() 21 | 22 | @BeforeAll 23 | fun beforeAll() = extension.use { 24 | onNodeWithText("click").performClick() 25 | } 26 | 27 | @BeforeEach 28 | fun beforeEach() = extension.use { 29 | onNodeWithText("click").performClick() 30 | } 31 | 32 | @Test 33 | fun test() = extension.use { 34 | onNodeWithText("Clicked: 2").assertIsDisplayed() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /instrumentation/compose/src/androidTest/java/de/mannodermaus/junit5/compose/FieldComposeExtensionTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.test.ExperimentalTestApi 11 | import androidx.compose.ui.test.assertIsDisplayed 12 | import androidx.compose.ui.test.onNodeWithText 13 | import androidx.compose.ui.test.performClick 14 | import org.junit.jupiter.api.extension.RegisterExtension 15 | import org.junit.jupiter.params.ParameterizedTest 16 | import org.junit.jupiter.params.provider.ValueSource 17 | 18 | class FieldComposeExtensionTests { 19 | 20 | @JvmField 21 | @RegisterExtension 22 | @OptIn(ExperimentalTestApi::class) 23 | val extension = createComposeExtension() 24 | 25 | @ValueSource( 26 | strings = [ 27 | "click me", 28 | "touch me", 29 | "jfc it actually works" 30 | ] 31 | ) 32 | @ParameterizedTest 33 | fun test(buttonLabel: String) = extension.use { 34 | setContent { 35 | Column { 36 | var counter by remember { mutableStateOf(0) } 37 | 38 | Text(text = "Clicked: $counter") 39 | Button(onClick = { counter++ }) { 40 | Text(text = buttonLabel) 41 | } 42 | } 43 | } 44 | 45 | onNodeWithText("Clicked: 0").assertIsDisplayed() 46 | onNodeWithText(buttonLabel).performClick() 47 | onNodeWithText("Clicked: 1").assertIsDisplayed() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /instrumentation/compose/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /instrumentation/compose/src/debug/java/de/mannodermaus/junit5/compose/ExistingActivity.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableIntStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | 14 | public class ExistingActivity : ComponentActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | setContent { 19 | Column { 20 | var counter by remember { mutableIntStateOf(0) } 21 | 22 | Text(text = "Clicked: $counter") 23 | Button(onClick = { counter++ }) { 24 | Text("click") 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /instrumentation/compose/src/main/java/de/mannodermaus/junit5/compose/ComposeExtension.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.junit.jupiter.api.extension.Extension 6 | 7 | /** 8 | * A JUnit 5 [Extension] that allows you to test and control [Composable]s and application using Compose. 9 | * The functionality of testing Compose is provided by means of the [runComposeTest] method, 10 | * which receives a [ComposeContext] from which the test can be orchestrated. The test will block 11 | * until the app or composable is idle, to ensure the tests are deterministic. 12 | * 13 | * This extension can be added to any JUnit 5 class using the [ExtendWith] annotation, 14 | * or registered globally through a configuration file. Alternatively, you can instantiate the extension 15 | * in a field within the test class using any of the [createComposeExtension] or 16 | * [createAndroidComposeExtension] factory methods. 17 | */ 18 | public interface ComposeExtension : Extension { 19 | /** 20 | * Set up and drive the execution of a Compose test within the provided [block]. 21 | * Depending on the time this is called, it will either queue up a preparatory action for the test 22 | * (e.g. in @BeforeEach) 23 | * The receive of this block is a [ComposeContext], through which you can access all sorts of 24 | * utilities to drive the execution of the test, such as driving the clock or executing actions 25 | * on the UI thread. The main purpose is provided through [ComposeContext.setContent], however: 26 | * With this function, you can pass an arbitrary composable tree to the extension and evaluate it afterwards. 27 | */ 28 | public fun use(block: ComposeContext.() -> Unit) 29 | 30 | @Deprecated(message = "Change to use()", replaceWith = ReplaceWith("use(block)")) 31 | public fun runComposeTest(block: ComposeContext.() -> Unit) { 32 | use(block) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /instrumentation/compose/src/test/java/de/mannodermaus/junit5/compose/ComposeContextTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.compose 2 | 3 | import androidx.compose.ui.test.junit4.ComposeContentTestRule 4 | import androidx.compose.ui.test.junit4.ComposeTestRule 5 | import org.junit.jupiter.api.Assertions.fail 6 | import org.junit.jupiter.params.ParameterizedTest 7 | import org.junit.jupiter.params.provider.MethodSource 8 | import java.lang.reflect.Method 9 | 10 | class ComposeContextTests { 11 | companion object { 12 | @JvmStatic 13 | fun relevantMethods() = buildList { 14 | addAll(ComposeTestRule::class.java.relevantMethods) 15 | addAll(ComposeContentTestRule::class.java.relevantMethods) 16 | } 17 | 18 | private val Class.relevantMethods 19 | get() = declaredMethods.filter { '$' !in it.name } 20 | } 21 | 22 | @MethodSource("relevantMethods") 23 | @ParameterizedTest(name = "ComposeContext defines {0} correctly") 24 | fun test(method: Method) { 25 | try { 26 | ComposeContext::class.java.getDeclaredMethod(method.name, *method.parameterTypes) 27 | } catch (ignored: NoSuchMethodException) { 28 | fail("ComposeContext does not define method $method") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /instrumentation/core/api/core.api: -------------------------------------------------------------------------------- 1 | public final class de/mannodermaus/junit5/ActivityScenarioExtension : org/junit/jupiter/api/extension/AfterEachCallback, org/junit/jupiter/api/extension/BeforeEachCallback, org/junit/jupiter/api/extension/ParameterResolver { 2 | public static final field Companion Lde/mannodermaus/junit5/ActivityScenarioExtension$Companion; 3 | public synthetic fun (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V 4 | public fun afterEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V 5 | public fun beforeEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V 6 | public final fun getScenario ()Landroidx/test/core/app/ActivityScenario; 7 | public static final fun launch (Landroid/content/Intent;)Lde/mannodermaus/junit5/ActivityScenarioExtension; 8 | public static final fun launch (Ljava/lang/Class;)Lde/mannodermaus/junit5/ActivityScenarioExtension; 9 | public fun resolveParameter (Lorg/junit/jupiter/api/extension/ParameterContext;Lorg/junit/jupiter/api/extension/ExtensionContext;)Ljava/lang/Object; 10 | public fun supportsParameter (Lorg/junit/jupiter/api/extension/ParameterContext;Lorg/junit/jupiter/api/extension/ExtensionContext;)Z 11 | } 12 | 13 | public final class de/mannodermaus/junit5/ActivityScenarioExtension$Companion { 14 | public final fun launch (Landroid/content/Intent;)Lde/mannodermaus/junit5/ActivityScenarioExtension; 15 | public final fun launch (Ljava/lang/Class;)Lde/mannodermaus/junit5/ActivityScenarioExtension; 16 | } 17 | 18 | public final class de/mannodermaus/junit5/CoreConstantsKt { 19 | public static final field JUNIT5_MINIMUM_SDK_VERSION I 20 | } 21 | 22 | public abstract interface annotation class de/mannodermaus/junit5/condition/DisabledIfBuildConfigValue : java/lang/annotation/Annotation { 23 | public abstract fun matches ()Ljava/lang/String; 24 | public abstract fun named ()Ljava/lang/String; 25 | } 26 | 27 | public abstract interface annotation class de/mannodermaus/junit5/condition/DisabledOnManufacturer : java/lang/annotation/Annotation { 28 | public abstract fun ignoreCase ()Z 29 | public abstract fun value ()[Ljava/lang/String; 30 | } 31 | 32 | public abstract interface annotation class de/mannodermaus/junit5/condition/DisabledOnSdkVersion : java/lang/annotation/Annotation { 33 | public abstract fun from ()I 34 | public abstract fun until ()I 35 | } 36 | 37 | public abstract interface annotation class de/mannodermaus/junit5/condition/EnabledIfBuildConfigValue : java/lang/annotation/Annotation { 38 | public abstract fun matches ()Ljava/lang/String; 39 | public abstract fun named ()Ljava/lang/String; 40 | } 41 | 42 | public abstract interface annotation class de/mannodermaus/junit5/condition/EnabledOnManufacturer : java/lang/annotation/Annotation { 43 | public abstract fun ignoreCase ()Z 44 | public abstract fun value ()[Ljava/lang/String; 45 | } 46 | 47 | public abstract interface annotation class de/mannodermaus/junit5/condition/EnabledOnSdkVersion : java/lang/annotation/Annotation { 48 | public abstract fun from ()I 49 | public abstract fun until ()I 50 | } 51 | 52 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/BaselineJUnit4Test.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * This JUnit 4 test class is here to validate 8 | * that older device, which ignore JUnit 5 tests, 9 | * still execute older ones. 10 | */ 11 | class BaselineJUnit4Test { 12 | 13 | @Test 14 | fun run() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/KotlinInstrumentationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import androidx.test.core.app.ActivityScenario 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.assertion.ViewAssertions.matches 6 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 7 | import androidx.test.espresso.matcher.ViewMatchers.withText 8 | import de.mannodermaus.junit5.condition.EnabledOnManufacturer 9 | import org.junit.jupiter.api.DisplayName 10 | import org.junit.jupiter.api.DynamicTest.dynamicTest 11 | import org.junit.jupiter.api.RepeatedTest 12 | import org.junit.jupiter.api.RepetitionInfo 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.TestFactory 15 | import org.junit.jupiter.api.extension.RegisterExtension 16 | import org.junit.jupiter.params.ParameterizedTest 17 | import org.junit.jupiter.params.provider.ValueSource 18 | 19 | class KotlinInstrumentationTests { 20 | 21 | @JvmField 22 | @RegisterExtension 23 | val scenarioExtension = ActivityScenarioExtension.launch() 24 | 25 | @Test 26 | fun testUsingGetScenario() { 27 | val scenario = scenarioExtension.scenario 28 | onView(withText("TestActivity")).check(matches(isDisplayed())) 29 | scenario.onActivity { it.changeText("New Text") } 30 | onView(withText("New Text")).check(matches(isDisplayed())) 31 | } 32 | 33 | @DisplayName("cool display name") 34 | @Test 35 | fun testWithDisplayName() { 36 | } 37 | 38 | @Test 39 | fun testUsingMethodParameter(scenario: ActivityScenario) { 40 | onView(withText("TestActivity")).check(matches(isDisplayed())) 41 | scenario.onActivity { it.changeText("New Text") } 42 | onView(withText("New Text")).check(matches(isDisplayed())) 43 | } 44 | 45 | @ParameterizedTest 46 | @ValueSource(ints = [1, 4, 6, 7]) 47 | fun kotlinTestWithParameters(value: Int) { 48 | 49 | } 50 | 51 | @RepeatedTest(3) 52 | fun kotlinRepeatedTest(info: RepetitionInfo) { 53 | 54 | } 55 | 56 | @TestFactory 57 | fun kotlinTestFactory() = listOf( 58 | dynamicTest("Dynamic 1") {}, 59 | dynamicTest("Dynamic 2") {} 60 | ) 61 | 62 | @EnabledOnManufacturer(["Samsung"]) 63 | @Test 64 | fun onlyOnSamsung() { 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/TaggedTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Tag 5 | import org.junit.jupiter.api.Test 6 | 7 | class TaggedTests { 8 | @Test 9 | fun includedTest() { 10 | } 11 | 12 | @Tag("nope") 13 | @Test 14 | fun taggedTestDisabledOnMethodLevel() { 15 | assertEquals(5, 2 + 2) 16 | } 17 | } 18 | 19 | @Tag("nope") 20 | class TaggedTestsDisabledOnClassLevel { 21 | @Test 22 | fun excludedTest() { 23 | assertEquals(5, 2 + 2) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/JavaAbstractClass.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertNotNull; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | abstract class JavaAbstractClass { 8 | @Test 9 | void javaTest() { 10 | assertNotNull(getJavaFileName()); 11 | } 12 | 13 | abstract String getJavaFileName(); 14 | } 15 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/JavaAbstractClassTest.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | public class JavaAbstractClassTest extends JavaAbstractClass { 6 | @Nullable 7 | @Override 8 | public String getJavaFileName() { 9 | return "hello world"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/JavaInterface.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | interface JavaInterface { 6 | @Test 7 | default void javaTest() { 8 | assert(getJavaValue() > 0L); 9 | } 10 | 11 | long getJavaValue(); 12 | } 13 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/JavaInterfaceTest.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance; 2 | 3 | public class JavaInterfaceTest implements JavaInterface { 4 | @Override 5 | public long getJavaValue() { 6 | return 4815162342L; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/JavaMixedInterfaceTest.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance; 2 | 3 | public class JavaMixedInterfaceTest implements JavaInterface, KotlinInterface { 4 | @Override 5 | public long getJavaValue() { 6 | return 4815162342L; 7 | } 8 | 9 | @Override 10 | public int getKotlinValue() { 11 | return 10101010; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/inheritance/KotlinInheritanceTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.inheritance 2 | 3 | import org.junit.jupiter.api.Assertions.assertNotNull 4 | import org.junit.jupiter.api.Test 5 | 6 | abstract class KotlinAbstractClass { 7 | @Test 8 | fun kotlinTest() { 9 | assertNotNull(getKotlinFileName()) 10 | } 11 | 12 | abstract fun getKotlinFileName(): String? 13 | } 14 | 15 | interface KotlinInterface { 16 | @Test 17 | fun kotlinTest() { 18 | assert(kotlinValue > 0) 19 | } 20 | 21 | val kotlinValue: Int 22 | } 23 | 24 | class KotlinAbstractClassTest : KotlinAbstractClass() { 25 | override fun getKotlinFileName() = "hello world" 26 | } 27 | 28 | class KotlinInterfaceTest : KotlinInterface { 29 | override val kotlinValue: Int = 1337 30 | } 31 | 32 | class KotlinMixedInterfaceTest : KotlinInterface, JavaInterface { 33 | override val kotlinValue: Int = 1337 34 | override fun getJavaValue(): Long = 1234L 35 | } 36 | -------------------------------------------------------------------------------- /instrumentation/core/src/androidTest/java/de/mannodermaus/junit5/otherpackage/AnotherTest.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.otherpackage 2 | 3 | import org.junit.jupiter.api.Test 4 | 5 | class AnotherTest { 6 | @Test 7 | fun anotherTest() { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /instrumentation/core/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /instrumentation/core/src/debug/java/de/mannodermaus/junit5/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.widget.TextView 6 | 7 | class TestActivity : Activity() { 8 | 9 | private val textView by lazy { findViewById(R.id.textView) } 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_test) 14 | } 15 | 16 | fun changeText(label: String) { 17 | textView.text = label 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /instrumentation/core/src/debug/res/layout/activity_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/CoreConstants.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | /** 4 | * The minimum Android API level on which JUnit 5 tests may be executed. 5 | * Trying to launch a test on an older device will simply mark it as 'skipped'. 6 | */ 7 | public const val JUNIT5_MINIMUM_SDK_VERSION: Int = 26 8 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledIfBuildConfigValue.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import de.mannodermaus.junit5.internal.DisabledIfBuildConfigValueCondition 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | @ExtendWith(DisabledIfBuildConfigValueCondition::class) 9 | public annotation class DisabledIfBuildConfigValue( 10 | val named: String, 11 | val matches: String 12 | ) 13 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnManufacturer.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import de.mannodermaus.junit5.internal.DisabledOnManufacturerCondition 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | @ExtendWith(DisabledOnManufacturerCondition::class) 9 | public annotation class DisabledOnManufacturer( 10 | val value: Array, 11 | val ignoreCase: Boolean = true 12 | ) 13 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersion.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import androidx.annotation.IntRange 4 | import de.mannodermaus.junit5.JUNIT5_MINIMUM_SDK_VERSION 5 | import de.mannodermaus.junit5.internal.DisabledOnSdkVersionCondition 6 | import de.mannodermaus.junit5.internal.NOT_SET 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | 9 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 10 | @Retention(AnnotationRetention.RUNTIME) 11 | @ExtendWith(DisabledOnSdkVersionCondition::class) 12 | public annotation class DisabledOnSdkVersion( 13 | @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, 14 | @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET 15 | ) 16 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledIfBuildConfigValue.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import de.mannodermaus.junit5.internal.EnabledIfBuildConfigValueCondition 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | @ExtendWith(EnabledIfBuildConfigValueCondition::class) 9 | public annotation class EnabledIfBuildConfigValue( 10 | val named: String, 11 | val matches: String 12 | ) 13 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnManufacturer.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import de.mannodermaus.junit5.internal.EnabledOnManufacturerCondition 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | 6 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | @ExtendWith(EnabledOnManufacturerCondition::class) 9 | public annotation class EnabledOnManufacturer( 10 | val value: Array, 11 | val ignoreCase: Boolean = true 12 | ) 13 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersion.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import androidx.annotation.IntRange 4 | import de.mannodermaus.junit5.JUNIT5_MINIMUM_SDK_VERSION 5 | import de.mannodermaus.junit5.internal.EnabledOnSdkVersionCondition 6 | import de.mannodermaus.junit5.internal.NOT_SET 7 | import org.junit.jupiter.api.extension.ExtendWith 8 | 9 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 10 | @Retention(AnnotationRetention.RUNTIME) 11 | @ExtendWith(EnabledOnSdkVersionCondition::class) 12 | public annotation class EnabledOnSdkVersion( 13 | @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val from: Int = NOT_SET, 14 | @IntRange(from = JUNIT5_MINIMUM_SDK_VERSION.toLong()) val until: Int = NOT_SET 15 | ) 16 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/CoreInternalConstants.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | internal const val NOT_SET = -1 4 | internal const val LOG_TAG = "AndroidJUnit5" 5 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledIfBuildConfigValueCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import de.mannodermaus.junit5.internal.utils.BuildConfigValueUtils 5 | import de.mannodermaus.junit5.condition.DisabledIfBuildConfigValue 6 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 7 | import org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled 8 | import org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled 9 | import org.junit.jupiter.api.extension.ExecutionCondition 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 12 | import org.junit.platform.commons.util.Preconditions 13 | 14 | internal class DisabledIfBuildConfigValueCondition : ExecutionCondition { 15 | 16 | companion object { 17 | private val ENABLED_BY_DEFAULT = 18 | enabled("@DisabledIfBuildConfigValue is not present") 19 | } 20 | 21 | @TargetApi(24) 22 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 23 | val optional = findAnnotation(context.element, DisabledIfBuildConfigValue::class.java) 24 | 25 | if (optional.isPresent) { 26 | val annotation = optional.get() 27 | val name = annotation.named.trim() 28 | val regexString = annotation.matches 29 | 30 | Preconditions.notBlank(name) { "The 'named' attribute must not be blank in $annotation" } 31 | Preconditions.notBlank(regexString) { "The 'matches' attribute must not be blank in $annotation" } 32 | 33 | val actual = runCatching { BuildConfigValueUtils.getAsString(name) }.getOrNull() 34 | ?: return enabled("BuildConfig key [$name] does not exist") 35 | 36 | return if (actual.matches(regexString.toRegex())) { 37 | disabled("BuildConfig key [$name] with value [$actual] matches regular expression [$regexString]") 38 | } else { 39 | enabled("BuildConfig key [$name] with value [$actual] does not match regular expression [$regexString]") 40 | } 41 | } 42 | 43 | return ENABLED_BY_DEFAULT 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnManufacturerCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import de.mannodermaus.junit5.condition.DisabledOnManufacturer 6 | import de.mannodermaus.junit5.internal.EnabledOnManufacturerCondition.Companion.disabled 7 | import de.mannodermaus.junit5.internal.EnabledOnManufacturerCondition.Companion.enabled 8 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 9 | import org.junit.jupiter.api.extension.ExecutionCondition 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 12 | import org.junit.platform.commons.util.Preconditions 13 | 14 | internal class DisabledOnManufacturerCondition : ExecutionCondition { 15 | 16 | companion object { 17 | private val ENABLED_BY_DEFAULT = 18 | ConditionEvaluationResult.enabled("@DisabledOnManufacturer is not present") 19 | } 20 | 21 | @TargetApi(24) 22 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 23 | val optional = findAnnotation(context.element, DisabledOnManufacturer::class.java) 24 | 25 | if (optional.isPresent) { 26 | val annotation = optional.get() 27 | val patterns = annotation.value 28 | val ignoreCase = annotation.ignoreCase 29 | 30 | Preconditions.condition( 31 | patterns.isNotEmpty(), 32 | "You must declare at least one Manufacturer in @DisabledOnManufacturer" 33 | ) 34 | 35 | return if (patterns.any { Build.MANUFACTURER.equals(it, ignoreCase = ignoreCase) }) { 36 | disabled() 37 | } else { 38 | enabled() 39 | } 40 | } 41 | return ENABLED_BY_DEFAULT 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/DisabledOnSdkVersionCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import de.mannodermaus.junit5.condition.DisabledOnSdkVersion 6 | import de.mannodermaus.junit5.internal.EnabledOnSdkVersionCondition.Companion.disabled 7 | import de.mannodermaus.junit5.internal.EnabledOnSdkVersionCondition.Companion.enabled 8 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 9 | import org.junit.jupiter.api.extension.ExecutionCondition 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 12 | import org.junit.platform.commons.util.Preconditions 13 | 14 | internal class DisabledOnSdkVersionCondition : ExecutionCondition { 15 | 16 | companion object { 17 | private val ENABLED_BY_DEFAULT = 18 | ConditionEvaluationResult.enabled("@DisabledOnSdkVersion is not present") 19 | } 20 | 21 | @TargetApi(24) 22 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 23 | val optional = findAnnotation(context.element, DisabledOnSdkVersion::class.java) 24 | 25 | if (optional.isPresent) { 26 | val annotation = optional.get() 27 | val fromApi = annotation.from 28 | val untilApi = annotation.until 29 | val hasLowerBound = fromApi != NOT_SET 30 | val hasUpperBound = untilApi != NOT_SET 31 | Preconditions.condition( 32 | hasLowerBound || hasUpperBound, 33 | "At least one value must be provided in @DisabledOnSdkVersion" 34 | ) 35 | 36 | // Constrain the current API Level based on the presence of "fromApi" & "untilApi": 37 | // If either one is not set at all, that part of the conditional becomes true automatically 38 | val lowerCheck = !hasLowerBound || Build.VERSION.SDK_INT >= fromApi 39 | val upperCheck = !hasUpperBound || Build.VERSION.SDK_INT <= untilApi 40 | return if (lowerCheck && upperCheck) { 41 | disabled() 42 | } else { 43 | enabled() 44 | } 45 | } 46 | 47 | return ENABLED_BY_DEFAULT 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledIfBuildConfigValueCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import de.mannodermaus.junit5.internal.utils.BuildConfigValueUtils 5 | import de.mannodermaus.junit5.condition.EnabledIfBuildConfigValue 6 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 7 | import org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled 8 | import org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled 9 | import org.junit.jupiter.api.extension.ExecutionCondition 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 12 | import org.junit.platform.commons.util.Preconditions 13 | 14 | internal class EnabledIfBuildConfigValueCondition : ExecutionCondition { 15 | 16 | companion object { 17 | private val ENABLED_BY_DEFAULT = 18 | enabled("@EnabledIfBuildConfigValue is not present") 19 | } 20 | 21 | @TargetApi(24) 22 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 23 | val optional = findAnnotation(context.element, EnabledIfBuildConfigValue::class.java) 24 | 25 | if (optional.isPresent) { 26 | val annotation = optional.get() 27 | val name = annotation.named.trim() 28 | val regexString = annotation.matches 29 | 30 | Preconditions.notBlank(name) { "The 'named' attribute must not be blank in $annotation" } 31 | Preconditions.notBlank(regexString) { "The 'matches' attribute must not be blank in $annotation" } 32 | 33 | val actual = runCatching { BuildConfigValueUtils.getAsString(name) }.getOrNull() 34 | ?: return disabled("BuildConfig key [$name] does not exist") 35 | 36 | return if (actual.matches(regexString.toRegex())) { 37 | enabled("BuildConfig key [$name] with value [$actual] matches regular expression [$regexString]") 38 | } else { 39 | disabled("BuildConfig key [$name] with value [$actual] does not match regular expression [$regexString]") 40 | } 41 | } 42 | 43 | return ENABLED_BY_DEFAULT 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnManufacturerCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import de.mannodermaus.junit5.condition.EnabledOnManufacturer 6 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 7 | import org.junit.jupiter.api.extension.ExecutionCondition 8 | import org.junit.jupiter.api.extension.ExtensionContext 9 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 10 | import org.junit.platform.commons.util.Preconditions 11 | 12 | internal class EnabledOnManufacturerCondition : ExecutionCondition { 13 | 14 | companion object { 15 | private val ENABLED_BY_DEFAULT = 16 | ConditionEvaluationResult.enabled("@EnabledOnManufacturer is not present") 17 | 18 | fun enabled(): ConditionEvaluationResult { 19 | return ConditionEvaluationResult.enabled("Enabled on Manufacturer: " + Build.MANUFACTURER) 20 | } 21 | 22 | fun disabled(): ConditionEvaluationResult { 23 | return ConditionEvaluationResult.disabled("Disabled on Manufacturer: " + Build.MANUFACTURER) 24 | } 25 | } 26 | 27 | @TargetApi(24) 28 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 29 | val optional = findAnnotation(context.element, EnabledOnManufacturer::class.java) 30 | 31 | if (optional.isPresent) { 32 | val annotation = optional.get() 33 | val patterns = annotation.value 34 | val ignoreCase = annotation.ignoreCase 35 | 36 | Preconditions.condition( 37 | patterns.isNotEmpty(), 38 | "You must declare at least one Manufacturer in @EnabledOnManufacturer" 39 | ) 40 | 41 | return if (patterns.any { Build.MANUFACTURER.equals(it, ignoreCase = ignoreCase) }) { 42 | enabled() 43 | } else { 44 | disabled() 45 | } 46 | } 47 | 48 | return ENABLED_BY_DEFAULT 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/EnabledOnSdkVersionCondition.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import de.mannodermaus.junit5.condition.EnabledOnSdkVersion 6 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 7 | import org.junit.jupiter.api.extension.ExecutionCondition 8 | import org.junit.jupiter.api.extension.ExtensionContext 9 | import org.junit.platform.commons.util.AnnotationUtils.findAnnotation 10 | import org.junit.platform.commons.util.Preconditions 11 | 12 | internal class EnabledOnSdkVersionCondition : ExecutionCondition { 13 | 14 | companion object { 15 | private val ENABLED_BY_DEFAULT = 16 | ConditionEvaluationResult.enabled("@EnabledOnSdkVersion is not present") 17 | 18 | fun enabled(): ConditionEvaluationResult { 19 | return ConditionEvaluationResult.enabled("Enabled on API " + Build.VERSION.SDK_INT) 20 | } 21 | 22 | fun disabled(): ConditionEvaluationResult { 23 | return ConditionEvaluationResult.disabled("Disabled on API " + Build.VERSION.SDK_INT) 24 | } 25 | } 26 | 27 | @TargetApi(24) 28 | override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { 29 | val optional = findAnnotation(context.element, EnabledOnSdkVersion::class.java) 30 | 31 | if (optional.isPresent) { 32 | val annotation = optional.get() 33 | val fromApi = annotation.from 34 | val untilApi = annotation.until 35 | val hasLowerBound = fromApi != NOT_SET 36 | val hasUpperBound = untilApi != NOT_SET 37 | Preconditions.condition( 38 | hasLowerBound || hasUpperBound, 39 | "At least one value must be provided in @EnabledOnSdkVersion" 40 | ) 41 | 42 | // Constrain the current API Level based on the presence of "fromApi" & "untilApi": 43 | // If either one is not set at all, that part of the conditional becomes true automatically 44 | val lowerCheck = !hasLowerBound || Build.VERSION.SDK_INT >= fromApi 45 | val upperCheck = !hasUpperBound || Build.VERSION.SDK_INT <= untilApi 46 | return if (lowerCheck && upperCheck) { 47 | enabled() 48 | } else { 49 | disabled() 50 | } 51 | } 52 | 53 | return ENABLED_BY_DEFAULT 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /instrumentation/core/src/main/java/de/mannodermaus/junit5/internal/utils/BuildConfigValueUtils.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.util.Log 5 | import androidx.test.platform.app.InstrumentationRegistry 6 | import de.mannodermaus.junit5.internal.LOG_TAG 7 | import java.lang.reflect.Field 8 | 9 | @SuppressLint("NewApi") 10 | internal object BuildConfigValueUtils { 11 | 12 | private class Wrapper { 13 | private val fieldCache = mutableMapOf() 14 | private val buildConfigClass = run { 15 | val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName 16 | val buildConfigClassName = "$packageName.BuildConfig" 17 | Class.forName(buildConfigClassName) 18 | } 19 | 20 | fun getValue(key: String): String? { 21 | return try { 22 | fieldCache.getOrPut(key) { 23 | buildConfigClass.getField(key).also { 24 | it.isAccessible = true 25 | } 26 | }.get(null)?.toString() 27 | } catch (ignored: Throwable) { 28 | throw IllegalAccessException("Cannot access BuildConfig field '$key'") 29 | } 30 | } 31 | } 32 | 33 | private val wrapper by lazy { 34 | try { 35 | Wrapper() 36 | } catch (t: Throwable) { 37 | Log.e(LOG_TAG, "Cannot initialize access to BuildConfig", t) 38 | null 39 | } 40 | } 41 | 42 | /** 43 | * Reflectively look up a BuildConfig field's value. 44 | * This caches previous lookups to maximize performance. 45 | * @param key Key of the entry to obtain 46 | * @return The value of this entry, if any 47 | */ 48 | @Throws(IllegalAccessException::class) 49 | fun getAsString(key: String): String? { 50 | val buildConfigWrapper = this.wrapper 51 | if (buildConfigWrapper != null) { 52 | return buildConfigWrapper.getValue(key) 53 | } else { 54 | throw IllegalAccessException("Cannot access BuildConfig field'$key'") 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/AbstractExecutionConditionTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Assertions.assertTrue 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.TestInfo 7 | import org.junit.jupiter.api.extension.ConditionEvaluationResult 8 | import org.junit.jupiter.api.extension.ExecutionCondition 9 | import org.junit.jupiter.api.extension.ExtensionContext 10 | import org.junit.platform.commons.util.ReflectionUtils 11 | import org.mockito.kotlin.mock 12 | import org.mockito.kotlin.whenever 13 | import java.lang.reflect.AnnotatedElement 14 | import java.util.* 15 | 16 | abstract class AbstractExecutionConditionTests { 17 | 18 | private val context = mock() 19 | private var result: ConditionEvaluationResult? = null 20 | 21 | /* Lifecycle */ 22 | 23 | @BeforeEach 24 | fun beforeEach(testInfo: TestInfo) { 25 | whenever(context.element).thenReturn(method(testInfo)) 26 | } 27 | 28 | /* Abstract */ 29 | 30 | abstract fun getTestClass(): Class<*> 31 | 32 | abstract fun getExecutionCondition(): ExecutionCondition 33 | 34 | /* Protected */ 35 | 36 | protected fun evaluateCondition() { 37 | this.result = getExecutionCondition().evaluateExecutionCondition(context) 38 | } 39 | 40 | protected fun assertEnabled() { 41 | assertTrue(!result!!.isDisabled, "Should be enabled") 42 | } 43 | 44 | protected fun assertDisabled() { 45 | assertTrue(result!!.isDisabled, "Should be disabled") 46 | } 47 | 48 | protected fun assertReasonEquals(text: String) { 49 | assertThat(result!!.reason).hasValue(text) 50 | } 51 | 52 | /* Private */ 53 | 54 | private fun method(testInfo: TestInfo) = 55 | method(getTestClass(), testInfo.testMethod.get().name) 56 | 57 | private fun method(clazz: Class<*>, methodName: String): Optional = 58 | Optional.of(ReflectionUtils.findMethod(clazz, methodName).get()) 59 | } 60 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/DisabledIfBuildConfigValueIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [DisabledIfBuildConfigValueConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class DisabledIfBuildConfigValueIntegrationTests { 12 | 13 | @Disabled("Used by DisabledIfBuildConfigValueConditionTests only") 14 | @DisabledIfBuildConfigValue(named = "", matches = ".*") 15 | @Test 16 | fun invalidBecauseNameIsEmpty() { 17 | } 18 | 19 | @Disabled("Used by DisabledIfBuildConfigValueConditionTests only") 20 | @DisabledIfBuildConfigValue(named = "DEBUG", matches = "") 21 | @Test 22 | fun invalidBecauseRegexIsEmpty() { 23 | } 24 | 25 | @Disabled("Used by DisabledIfBuildConfigValueConditionTests only") 26 | @DisabledIfBuildConfigValue(named = "DEBUG", matches = "\\w{4}") 27 | @Test 28 | fun disabledBecauseValueMatchesRegex() { 29 | } 30 | 31 | @Disabled("Used by DisabledIfBuildConfigValueConditionTests only") 32 | @DisabledIfBuildConfigValue(named = "VERSION_NAME", matches = "0.1.234") 33 | @Test 34 | fun enabledBecauseValueDoesNotMatchRegex() { 35 | } 36 | 37 | @Disabled("Used by DisabledIfBuildConfigValueConditionTests only") 38 | @DisabledIfBuildConfigValue(named = "NOT_EXISTENT_KEY", matches = "whatever") 39 | @Test 40 | fun enabledBecauseKeyDoesNotExist() { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/DisabledOnManufacturerIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [DisabledOnManufacturerConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class DisabledOnManufacturerIntegrationTests { 12 | 13 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 14 | @DisabledOnManufacturer([]) 15 | @Test 16 | fun invalidBecauseArrayIsEmpty() { 17 | } 18 | 19 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 20 | @DisabledOnManufacturer(["Samsung"]) 21 | @Test 22 | fun disabledBecauseValueMatchesExactly() { 23 | } 24 | 25 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 26 | @DisabledOnManufacturer(["Samsung", "Huawei"]) 27 | @Test 28 | fun disabledBecauseValueIsAmongTheValues() { 29 | } 30 | 31 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 32 | @DisabledOnManufacturer(["sAmSuNg"]) 33 | @Test 34 | fun disabledBecauseValueMatchesWithOfIgnoreCase() { 35 | } 36 | 37 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 38 | @DisabledOnManufacturer(["sAmSuNg"], ignoreCase = false) 39 | @Test 40 | fun enabledBecauseValueDoesntMatchDueToIgnoreCase() { 41 | } 42 | 43 | @Disabled("Used by DisabledOnManufacturerConditionTests only") 44 | @DisabledOnManufacturer(["Samsung", "Huawei"]) 45 | @Test 46 | fun enabledBecauseValueDoesntMatchAnyValue() { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/DisabledOnSdkVersionIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [DisabledOnSdkVersionConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class DisabledOnSdkVersionIntegrationTests { 12 | 13 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 14 | @DisabledOnSdkVersion 15 | @Test 16 | fun invalidBecauseNoValueGiven() { 17 | } 18 | 19 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 20 | @DisabledOnSdkVersion(from = 24) 21 | @Test 22 | fun disabledBecauseMinApiIsMatched() { 23 | } 24 | 25 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 26 | @DisabledOnSdkVersion(until = 26) 27 | @Test 28 | fun disabledBecauseMaxApiIsMatched() { 29 | } 30 | 31 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 32 | @DisabledOnSdkVersion(from = 24, until = 29) 33 | @Test 34 | fun disabledBecauseApiIsInValidRange() { 35 | } 36 | 37 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 38 | @DisabledOnSdkVersion(from = 27) 39 | @Test 40 | fun enabledBecauseMinApiLowEnough() { 41 | } 42 | 43 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 44 | @DisabledOnSdkVersion(until = 27) 45 | @Test 46 | fun disabledBecauseMaxApiHighEnough() { 47 | } 48 | 49 | @Disabled("Used by DisabledOnSdkVersionConditionTests only") 50 | @DisabledOnSdkVersion(from = 27, until = 29) 51 | @Test 52 | fun disabledBecauseApiIsInsideValidRange() { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/EnabledIfBuildConfigValueIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [EnabledIfBuildConfigValueConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class EnabledIfBuildConfigValueIntegrationTests { 12 | 13 | @Disabled("Used by EnabledIfBuildConfigValueConditionTests only") 14 | @EnabledIfBuildConfigValue(named = "", matches = ".*") 15 | @Test 16 | fun invalidBecauseNameIsEmpty() { 17 | } 18 | 19 | @Disabled("Used by EnabledIfBuildConfigValueConditionTests only") 20 | @EnabledIfBuildConfigValue(named = "DEBUG", matches = "") 21 | @Test 22 | fun invalidBecauseRegexIsEmpty() { 23 | } 24 | 25 | @Disabled("Used by EnabledIfBuildConfigValueConditionTests only") 26 | @EnabledIfBuildConfigValue(named = "DEBUG", matches = "\\w{4}") 27 | @Test 28 | fun enabledBecauseValueMatchesRegex() { 29 | } 30 | 31 | @Disabled("Used by EnabledIfBuildConfigValueConditionTests only") 32 | @EnabledIfBuildConfigValue(named = "VERSION_NAME", matches = "0.1.234") 33 | @Test 34 | fun disabledBecauseValueDoesNotMatchRegex() { 35 | } 36 | 37 | @Disabled("Used by EnabledIfBuildConfigValueConditionTests only") 38 | @EnabledIfBuildConfigValue(named = "NOT_EXISTENT_KEY", matches = "whatever") 39 | @Test 40 | fun disabledBecauseKeyDoesNotExist() { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/EnabledOnManufacturerConditionTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import de.mannodermaus.junit5.internal.EnabledOnManufacturerCondition 5 | import de.mannodermaus.junit5.testutil.AndroidBuildUtils.withManufacturer 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.assertThrows 8 | import org.junit.jupiter.api.extension.ExecutionCondition 9 | import org.junit.platform.commons.PreconditionViolationException 10 | 11 | /** 12 | * Unit tests for [EnabledOnManufacturerCondition]. 13 | * 14 | * This works together with [EnabledOnManufacturerIntegrationTests]: The test methods 15 | * in both classes MUST be named identical. 16 | */ 17 | class EnabledOnManufacturerConditionTests : AbstractExecutionConditionTests() { 18 | 19 | override fun getExecutionCondition(): ExecutionCondition = 20 | EnabledOnManufacturerCondition() 21 | 22 | override fun getTestClass(): Class<*> = EnabledOnManufacturerIntegrationTests::class.java 23 | 24 | /** 25 | * @see [EnabledOnManufacturerIntegrationTests.invalidBecauseArrayIsEmpty] 26 | */ 27 | @Test 28 | fun invalidBecauseArrayIsEmpty() { 29 | val expected = assertThrows { 30 | evaluateCondition() 31 | } 32 | 33 | assertThat(expected).hasMessageThat().contains("You must declare at least one Manufacturer in @EnabledOnManufacturer") 34 | } 35 | 36 | /** 37 | * @see [EnabledOnManufacturerIntegrationTests.enabledBecauseValueMatchesExactly] 38 | */ 39 | @Test 40 | fun enabledBecauseValueMatchesExactly() { 41 | withManufacturer("Samsung") { 42 | evaluateCondition() 43 | assertEnabled() 44 | assertReasonEquals("Enabled on Manufacturer: Samsung") 45 | } 46 | } 47 | 48 | /** 49 | * @see [EnabledOnManufacturerIntegrationTests.enabledBecauseValueIsAmongTheValues] 50 | */ 51 | @Test 52 | fun enabledBecauseValueIsAmongTheValues() { 53 | withManufacturer("Huawei") { 54 | evaluateCondition() 55 | assertEnabled() 56 | assertReasonEquals("Enabled on Manufacturer: Huawei") 57 | } 58 | } 59 | 60 | /** 61 | * @see [EnabledOnManufacturerIntegrationTests.enabledBecauseValueMatchesWithOfIgnoreCase] 62 | */ 63 | @Test 64 | fun enabledBecauseValueMatchesWithOfIgnoreCase() { 65 | withManufacturer("Samsung") { 66 | evaluateCondition() 67 | assertEnabled() 68 | assertReasonEquals("Enabled on Manufacturer: Samsung") 69 | } 70 | } 71 | 72 | /** 73 | * @see [EnabledOnManufacturerIntegrationTests.disabledBecauseValueDoesntMatchDueToIgnoreCase] 74 | */ 75 | @Test 76 | fun disabledBecauseValueDoesntMatchDueToIgnoreCase() { 77 | withManufacturer("Samsung") { 78 | evaluateCondition() 79 | assertDisabled() 80 | assertReasonEquals("Disabled on Manufacturer: Samsung") 81 | } 82 | } 83 | 84 | /** 85 | * @see [EnabledOnManufacturerIntegrationTests.disabledBecauseValueDoesntMatchAnyValue] 86 | */ 87 | @Test 88 | fun disabledBecauseValueDoesntMatchAnyValue() { 89 | withManufacturer("Google") { 90 | evaluateCondition() 91 | assertDisabled() 92 | assertReasonEquals("Disabled on Manufacturer: Google") 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/EnabledOnManufacturerIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [EnabledOnManufacturerConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class EnabledOnManufacturerIntegrationTests { 12 | 13 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 14 | @EnabledOnManufacturer([]) 15 | @Test 16 | fun invalidBecauseArrayIsEmpty() { 17 | } 18 | 19 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 20 | @EnabledOnManufacturer(["Samsung"]) 21 | @Test 22 | fun enabledBecauseValueMatchesExactly() { 23 | } 24 | 25 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 26 | @EnabledOnManufacturer(["Samsung", "Huawei"]) 27 | @Test 28 | fun enabledBecauseValueIsAmongTheValues() { 29 | } 30 | 31 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 32 | @EnabledOnManufacturer(["sAmSuNg"]) 33 | @Test 34 | fun enabledBecauseValueMatchesWithOfIgnoreCase() { 35 | } 36 | 37 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 38 | @EnabledOnManufacturer(["sAmSuNg"], ignoreCase = false) 39 | @Test 40 | fun disabledBecauseValueDoesntMatchDueToIgnoreCase() { 41 | } 42 | 43 | @Disabled("Used by EnabledOnManufacturerConditionTests only") 44 | @EnabledOnManufacturer(["Samsung", "Huawei"]) 45 | @Test 46 | fun disabledBecauseValueDoesntMatchAnyValue() { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/condition/EnabledOnSdkVersionIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.condition 2 | 3 | import org.junit.jupiter.api.Disabled 4 | import org.junit.jupiter.api.Test 5 | 6 | /** 7 | * Companion class for [EnabledOnSdkVersionConditionTests]. 8 | * The tests in here are intentionally disabled; the partner class will 9 | * drive them through reflection in order to assert the behavior of the condition 10 | */ 11 | class EnabledOnSdkVersionIntegrationTests { 12 | 13 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 14 | @EnabledOnSdkVersion 15 | @Test 16 | fun invalidBecauseNoValueGiven() { 17 | } 18 | 19 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 20 | @EnabledOnSdkVersion(from = 24) 21 | @Test 22 | fun enabledBecauseMinApiIsMatched() { 23 | } 24 | 25 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 26 | @EnabledOnSdkVersion(until = 26) 27 | @Test 28 | fun enabledBecauseMaxApiIsMatched() { 29 | } 30 | 31 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 32 | @EnabledOnSdkVersion(from = 24, until = 29) 33 | @Test 34 | fun enabledBecauseApiIsInValidRange() { 35 | } 36 | 37 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 38 | @EnabledOnSdkVersion(from = 27) 39 | @Test 40 | fun disabledBecauseMinApiTooLow() { 41 | } 42 | 43 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 44 | @EnabledOnSdkVersion(until = 27) 45 | @Test 46 | fun disabledBecauseMaxApiTooHigh() { 47 | } 48 | 49 | @Disabled("Used by EnabledOnSdkVersionConditionTests only") 50 | @EnabledOnSdkVersion(from = 27, until = 29) 51 | @Test 52 | fun disabledBecauseApiIsOutsideValidRange() { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/util/BuildConfig.java: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.util; 2 | 3 | // Used reflectively by the integration tests for BuildConfig-related conditions 4 | @SuppressWarnings("unused") 5 | public final class BuildConfig { 6 | public static final boolean DEBUG = Boolean.parseBoolean("true"); 7 | public static final String VERSION_NAME = "1.0"; 8 | } 9 | -------------------------------------------------------------------------------- /instrumentation/core/src/test/java/de/mannodermaus/junit5/util/ResourceLocks.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.util 2 | 3 | /** 4 | * JUnit Jupiter resource locks, restricting parallelism of the test suite. 5 | */ 6 | const val RESOURCE_LOCK_INSTRUMENTATION = "instrumentation" 7 | -------------------------------------------------------------------------------- /instrumentation/extensions/api/extensions.api: -------------------------------------------------------------------------------- 1 | public final class de/mannodermaus/junit5/extensions/GrantPermissionExtension : org/junit/jupiter/api/extension/BeforeEachCallback { 2 | public static final field Companion Lde/mannodermaus/junit5/extensions/GrantPermissionExtension$Companion; 3 | public fun beforeEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V 4 | public static final fun grant ([Ljava/lang/String;)Lde/mannodermaus/junit5/extensions/GrantPermissionExtension; 5 | } 6 | 7 | public final class de/mannodermaus/junit5/extensions/GrantPermissionExtension$Companion { 8 | public final fun grant ([Ljava/lang/String;)Lde/mannodermaus/junit5/extensions/GrantPermissionExtension; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /instrumentation/extensions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import libs.plugins.android 2 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 3 | import org.gradle.api.tasks.testing.logging.TestLogEvent 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | 6 | buildscript { 7 | repositories { 8 | google() 9 | mavenCentral() 10 | sonatypeSnapshots() 11 | } 12 | 13 | dependencies { 14 | val latest = Artifacts.Plugin.latestStableVersion 15 | classpath("de.mannodermaus.gradle.plugins:android-junit5:$latest") 16 | } 17 | } 18 | 19 | plugins { 20 | id("com.android.library") 21 | kotlin("android") 22 | id("explicit-api-mode") 23 | } 24 | 25 | apply { 26 | plugin("de.mannodermaus.android-junit5") 27 | } 28 | 29 | val javaVersion = JavaVersion.VERSION_11 30 | 31 | android { 32 | namespace = "de.mannodermaus.junit5.extensions" 33 | compileSdk = Android.compileSdkVersion 34 | 35 | defaultConfig { 36 | minSdk = Android.testRunnerMinSdkVersion 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility = javaVersion 41 | targetCompatibility = javaVersion 42 | } 43 | 44 | buildFeatures { 45 | buildConfig = false 46 | resValues = false 47 | } 48 | 49 | lint { 50 | // JUnit 4 refers to java.lang.management APIs, which are absent on Android. 51 | warning.add("InvalidPackage") 52 | targetSdk = Android.targetSdkVersion 53 | } 54 | 55 | packaging { 56 | resources.excludes.add("META-INF/LICENSE.md") 57 | resources.excludes.add("META-INF/LICENSE-notice.md") 58 | } 59 | 60 | testOptions { 61 | unitTests.isReturnDefaultValues = true 62 | targetSdk = Android.targetSdkVersion 63 | } 64 | } 65 | 66 | tasks.withType { 67 | kotlinOptions.jvmTarget = javaVersion.toString() 68 | } 69 | 70 | tasks.withType { 71 | failFast = true 72 | testLogging { 73 | events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) 74 | exceptionFormat = TestExceptionFormat.FULL 75 | } 76 | } 77 | 78 | configurations.all { 79 | // The Instrumentation Test Runner uses the plugin, 80 | // which in turn provides the Instrumentation Test Runner again - 81 | // that's kind of deep. 82 | // To avoid conflicts, prefer using the local classes 83 | // and exclude the dependency from being pulled in externally. 84 | exclude(module = Artifacts.Instrumentation.Extensions.artifactId) 85 | } 86 | 87 | dependencies { 88 | implementation(libs.androidXTestAnnotation) 89 | implementation(libs.androidXTestRunner) 90 | implementation(libs.junitJupiterApi) 91 | 92 | testImplementation(project(":testutil")) 93 | testRuntimeOnly(libs.junitJupiterEngine) 94 | } 95 | 96 | project.configureDeployment(Artifacts.Instrumentation.Extensions) 97 | -------------------------------------------------------------------------------- /instrumentation/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX = true 2 | org.gradle.jvmargs = -XX:MetaspaceSize=1g -XX:MaxMetaspaceSize=1g 3 | 4 | # Dokka V2 (https://kotlinlang.org/docs/dokka-migration.html) 5 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 6 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 7 | -------------------------------------------------------------------------------- /instrumentation/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mannodermaus/android-junit5/f761c0f15619cb8caa9ac44daa50864329153d30/instrumentation/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /instrumentation/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /instrumentation/runner/api/runner.api: -------------------------------------------------------------------------------- 1 | public final class de/mannodermaus/junit5/AndroidJUnit5Builder : org/junit/runners/model/RunnerBuilder { 2 | public fun ()V 3 | public fun runnerForClass (Ljava/lang/Class;)Lorg/junit/runner/Runner; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/LibcoreAccess.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | import android.util.Log 4 | 5 | internal object LibcoreAccess { 6 | 7 | private class Wrapper { 8 | private val libcoreClass = Class.forName("libcore.io.Libcore") 9 | private val libcoreOsObject = libcoreClass.getField("os").get(null) 10 | private val setEnvMethod = libcoreOsObject.javaClass.getMethod( 11 | "setenv", 12 | String::class.java, 13 | String::class.java, 14 | Boolean::class.java 15 | ) 16 | 17 | fun setenv(key: String, value: String, overwrite: Boolean) { 18 | setEnvMethod.invoke(libcoreOsObject, key, value, overwrite) 19 | } 20 | } 21 | 22 | private val wrapper by lazy { 23 | try { 24 | Wrapper() 25 | } catch (t: Throwable) { 26 | Log.e(LOG_TAG, "FATAL: Cannot initialize access to Libcore", t) 27 | null 28 | } 29 | } 30 | 31 | /** 32 | * Invokes the method "libcore.io.Libcore.os.setenv(String, String)" with the provided key/value pair. 33 | * This effectively adds a custom environment variable to the running process, 34 | * allowing instrumentation tests to honor JUnit 5's @EnabledIfEnvironmentVariable and @DisabledIfEnvironmentVariable annotations. 35 | * 36 | * @param key Key of the variable 37 | * @param value Value of the variable 38 | * @throws IllegalAccessException If Libcore is not available 39 | */ 40 | @Throws(IllegalAccessException::class) 41 | fun setenv(key: String, value: String) { 42 | val libcoreWrapper = this.wrapper 43 | if (libcoreWrapper != null) { 44 | libcoreWrapper.setenv(key, value, true) 45 | } else { 46 | throw IllegalAccessException("Cannot access Libcore.os.setenv()") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/RunnerInternalConstants.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal 2 | 3 | internal const val LOG_TAG = "AndroidJUnit5" 4 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/EmptyTestPlan.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | import androidx.annotation.RequiresApi 4 | import org.junit.platform.engine.ConfigurationParameters 5 | import org.junit.platform.engine.TestDescriptor 6 | import org.junit.platform.engine.reporting.OutputDirectoryProvider 7 | import org.junit.platform.launcher.TestPlan 8 | import java.io.File 9 | import java.util.Optional 10 | 11 | /** 12 | * A JUnit TestPlan that does absolutely nothing. 13 | * Used by [de.mannodermaus.junit5.internal.runners.AndroidJUnit5] whenever a class 14 | * is not loadable through the JUnit Platform and should be discarded. 15 | */ 16 | @RequiresApi(26) 17 | internal object EmptyTestPlan : TestPlan( 18 | false, 19 | emptyConfigurationParameters, 20 | emptyOutputDirectoryProvider 21 | ) 22 | 23 | @RequiresApi(26) 24 | private val emptyConfigurationParameters = object : ConfigurationParameters { 25 | override fun get(key: String?) = Optional.empty() 26 | override fun getBoolean(key: String?) = Optional.empty() 27 | override fun keySet() = emptySet() 28 | 29 | @Deprecated("Deprecated in Java", ReplaceWith("keySet().size")) 30 | override fun size() = 0 31 | } 32 | 33 | @RequiresApi(26) 34 | private val emptyOutputDirectoryProvider = object : OutputDirectoryProvider { 35 | private val path = File.createTempFile("empty-output", ".nop").toPath() 36 | override fun getRootDirectory() = path 37 | override fun createOutputDirectory(testDescriptor: TestDescriptor?) = path 38 | } 39 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/GeneratedFilters.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import de.mannodermaus.junit5.internal.runners.AndroidJUnit5 6 | import org.junit.platform.engine.Filter 7 | import org.junit.platform.launcher.TagFilter 8 | 9 | private const val INSTRUMENTATION_FILTER_RES_FILE_NAME = "de_mannodermaus_junit5_filters" 10 | 11 | /** 12 | * Holder object for the filters of a test plan. 13 | * It converts the contents of a resource file into JUnit Platform [Filter] objects 14 | * for the [AndroidJUnit5] runner. 15 | */ 16 | internal object GeneratedFilters { 17 | 18 | @Suppress("FoldInitializerAndIfToElvis", "DiscouragedApi") 19 | @JvmStatic 20 | fun fromContext(context: Context): List> { 21 | // Look up the resource file written by the Gradle plugin 22 | // and open it. 23 | // (See Constants.kt inside the plugin's repository for the value used here) 24 | val identifier = context.resources.getIdentifier( 25 | INSTRUMENTATION_FILTER_RES_FILE_NAME, 26 | "raw", 27 | context.packageName 28 | ) 29 | val inputStream = if (identifier != 0) { 30 | try { 31 | context.resources.openRawResource(identifier) 32 | } catch (ignored: Resources.NotFoundException) { 33 | // Ignore 34 | null 35 | } 36 | } else { 37 | null 38 | } 39 | 40 | if (inputStream == null) { 41 | // File doesn't exist, or couldn't be located; return 42 | return emptyList() 43 | } 44 | 45 | // Try parsing the contents of the resource file 46 | // based on the expected format: 47 | // -t Include Tag 48 | // -T Exclude Tag 49 | val contents = inputStream.bufferedReader().readLines() 50 | val filters = mutableListOf>() 51 | 52 | contents.forEach { line -> 53 | when { 54 | line.startsWith("-t ") -> filters += TagFilter.includeTags(line.substring(3)) 55 | line.startsWith("-T ") -> filters += TagFilter.excludeTags(line.substring(3)) 56 | } 57 | } 58 | 59 | return filters 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ParsedSelectors.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | import android.os.Bundle 4 | import de.mannodermaus.junit5.internal.runners.AndroidJUnit5 5 | import org.junit.platform.engine.DiscoverySelector 6 | import org.junit.platform.engine.discovery.DiscoverySelectors 7 | 8 | /** 9 | * Holder object for the selectors of a test plan. 10 | * It converts the arguments handed to the Runner by the 11 | * Android instrumentation into JUnit Platform [DiscoverySelector] objects 12 | * for the [AndroidJUnit5] runner. 13 | */ 14 | internal object ParsedSelectors { 15 | 16 | @JvmStatic 17 | fun fromBundle(testClass: Class<*>, arguments: Bundle): List { 18 | // Check if specific class arguments were given to the Runner 19 | arguments.getString("class", null)?.let { classArg -> 20 | val testClassName = testClass.name 21 | val methods = testClass.declaredMethods 22 | 23 | val selectors = mutableListOf() 24 | 25 | // Separate the provided argument into methods, if any are given 26 | // (Format: class=com.package1.FirstTest#method1,com.package1.SecondTest#method2). 27 | // For each component in this string that applies to the test class at hand, 28 | // consider it a method filter if the name is appended to the component, using a pound sign (#). 29 | // Finally, if at least one of these method filters can be found, construct JUnit selectors from it 30 | classArg.split(",") 31 | .forEach { component -> 32 | if (!component.startsWith(testClassName)) { 33 | // Not the desired class 34 | return@forEach 35 | } 36 | 37 | // Try extracting an appended method name 38 | var methodName = component.replace(testClassName, "") 39 | if (!methodName.startsWith("#")) { 40 | return@forEach 41 | } 42 | methodName = methodName.substring(1) 43 | 44 | // Find all methods with the given name 45 | val eligibleMethods = methods 46 | .filter { it.name == methodName } 47 | .map { method -> DiscoverySelectors.selectMethod(testClass, method) } 48 | 49 | selectors += eligibleMethods 50 | } 51 | 52 | if (selectors.isNotEmpty()) { 53 | // Restrictions to specific methods apply 54 | return selectors 55 | } 56 | } 57 | 58 | // If nothing else was specified to the runner, assume that all classes should be run 59 | return listOf(DiscoverySelectors.selectClass(testClass)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParser.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | internal object PropertiesParser { 4 | @JvmStatic 5 | fun fromString(string: String) = 6 | string.split(",") 7 | .map { keyValuePair -> keyValuePair.split("=") } 8 | .filter { keyValueList -> keyValueList.size == 2 } 9 | .associate { it[0] to it[1] } 10 | } 11 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/discovery/ShardingFilter.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | import android.os.Bundle 4 | import de.mannodermaus.junit5.internal.extensions.isDynamicTest 5 | import org.junit.platform.engine.FilterResult 6 | import org.junit.platform.engine.TestDescriptor 7 | import org.junit.platform.launcher.PostDiscoveryFilter 8 | import org.junit.platform.launcher.TestIdentifier 9 | import kotlin.math.abs 10 | 11 | /** 12 | * JUnit 5 implementation of the default instrumentation's 13 | * `androidx.test.internal.runner.TestRequestBuilder$ShardingFilter`, 14 | * ported to the new API to support dynamic test templates, too. 15 | * 16 | * Based on a draft by KyoungJoo Jeon (@jkj8790). 17 | */ 18 | internal class ShardingFilter( 19 | private val numShards: Int, 20 | private val shardIndex: Int, 21 | ) : PostDiscoveryFilter { 22 | 23 | companion object { 24 | private const val ARG_NUM_SHARDS = "numShards" 25 | private const val ARG_SHARD_INDEX = "shardIndex" 26 | 27 | fun fromArguments(arguments: Bundle): ShardingFilter? { 28 | val numShards = arguments.getString(ARG_NUM_SHARDS)?.toInt() ?: -1 29 | val shardIndex = arguments.getString(ARG_SHARD_INDEX)?.toInt() ?: -1 30 | 31 | return if (numShards > 0 && shardIndex >= 0 && shardIndex < numShards) { 32 | ShardingFilter(numShards, shardIndex) 33 | } else { 34 | null 35 | } 36 | } 37 | } 38 | 39 | override fun apply(descriptor: TestDescriptor): FilterResult { 40 | val identifier = TestIdentifier.from(descriptor) 41 | 42 | if (identifier.isTest || identifier.isDynamicTest) { 43 | val remainder = abs(identifier.hashCode()) % numShards 44 | return if (remainder == shardIndex) { 45 | FilterResult.included(null) 46 | } else { 47 | FilterResult.excluded("excluded") 48 | } 49 | } 50 | 51 | return FilterResult.included(null) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/dummy/JupiterTestMethodFinder.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.dummy 2 | 3 | import android.util.Log 4 | import de.mannodermaus.junit5.internal.LOG_TAG 5 | import org.junit.jupiter.api.RepeatedTest 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.TestFactory 8 | import org.junit.jupiter.api.TestTemplate 9 | import org.junit.jupiter.params.ParameterizedTest 10 | import java.lang.reflect.Method 11 | import java.lang.reflect.Modifier 12 | 13 | /** 14 | * Algorithm to find all methods annotated with a JUnit Jupiter annotation 15 | * for devices running below API level 26 (i.e. those that cannot run Jupiter). 16 | * We're unable to rely on JUnit Platform's own reflection utilities since they rely on Java 8 stuff 17 | */ 18 | internal object JupiterTestMethodFinder { 19 | private val jupiterTestAnnotations = listOf( 20 | Test::class.java, 21 | TestFactory::class.java, 22 | RepeatedTest::class.java, 23 | TestTemplate::class.java, 24 | ParameterizedTest::class.java, 25 | ) 26 | 27 | fun find(cls: Class<*>): Set = cls.doFind(includeInherited = true) 28 | 29 | private fun Class<*>.doFind(includeInherited: Boolean): Set = buildSet { 30 | try { 31 | // Check each method in the Class for the presence 32 | // of the well-known list of JUnit Jupiter annotations. 33 | addAll(declaredMethods.filter(::isApplicableMethod)) 34 | 35 | // Recursively check non-private inner classes as well 36 | declaredClasses.filter(::isApplicableClass).forEach { inner -> 37 | addAll(inner.doFind(includeInherited = false)) 38 | } 39 | 40 | // Attach methods from inherited superclass or (for Java) implemented interfaces, too 41 | if (includeInherited) { 42 | addAll(superclass?.doFind(includeInherited = true).orEmpty()) 43 | interfaces.forEach { i -> addAll(i.doFind(includeInherited = true)) } 44 | } 45 | } catch (t: Throwable) { 46 | Log.w( 47 | LOG_TAG, 48 | "Encountered ${t.javaClass.simpleName} while finding Jupiter test methods for ${this@doFind.name}", 49 | t 50 | ) 51 | } 52 | } 53 | 54 | private fun isApplicableMethod(method: Method): Boolean { 55 | // The method must not be static... 56 | if (Modifier.isStatic(method.modifiers)) return false 57 | 58 | // ...and have at least one of the recognized JUnit 5 annotations 59 | return hasJupiterAnnotation(method) 60 | } 61 | 62 | private fun hasJupiterAnnotation(method: Method): Boolean { 63 | return jupiterTestAnnotations.any { method.getAnnotation(it) != null } 64 | } 65 | 66 | private fun isApplicableClass(cls: Class<*>): Boolean { 67 | // A class must not be private to be considered 68 | return !Modifier.isPrivate(cls.modifiers) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/extensions/TestIdentifierExt.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.extensions 2 | 3 | import de.mannodermaus.junit5.internal.formatters.TestNameFormatter 4 | import org.junit.platform.launcher.TestIdentifier 5 | 6 | private val DYNAMIC_TEST_PREFIXES = listOf( 7 | "[test-template-invocation", 8 | "[dynamic-test", 9 | "[dynamic-container", 10 | "[test-factory", 11 | "[test-template" 12 | ) 13 | 14 | private val TestIdentifier.shortId: String 15 | get() { 16 | var id = this.uniqueId 17 | val lastSlashIndex = id.lastIndexOf('/') 18 | if (lastSlashIndex > -1 && id.length >= lastSlashIndex) { 19 | id = id.substring(lastSlashIndex + 1) 20 | } 21 | return id 22 | } 23 | 24 | /** 25 | * Check if the given TestIdentifier describes a "test template invocation", 26 | * i.e. a dynamic test generated at runtime. 27 | */ 28 | internal val TestIdentifier.isDynamicTest: Boolean 29 | get() { 30 | val shortId = this.shortId 31 | return DYNAMIC_TEST_PREFIXES.any { shortId.startsWith(it) } 32 | } 33 | 34 | /** 35 | * Returns a formatted version of this identifier's name, 36 | * which is compatible with the quirks and limitations 37 | * of the Android Instrumentation, esp. when the [legacyFormat] 38 | * flag is enabled. 39 | */ 40 | internal fun TestIdentifier.format(legacyFormat: Boolean = false): String = 41 | TestNameFormatter.format(this, legacyFormat) 42 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/formatters/TestNameFormatter.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.formatters 2 | 3 | import org.junit.platform.launcher.TestIdentifier 4 | 5 | /** 6 | * A class for naming Jupiter test methods in a compatible manner, 7 | * taking into account several limitations imposed by the 8 | * Android instrumentation (e.g. on isolated test runs). 9 | */ 10 | internal object TestNameFormatter { 11 | fun format(identifier: TestIdentifier, legacyFormat: Boolean = false): String { 12 | // When requesting the legacy format of the formatter, 13 | // construct a technical version of its name for backwards compatibility 14 | // with the JUnit 4-based instrumentation of Android by stripping the brackets of parameterized tests completely. 15 | // If this didn't happen, running them from the IDE will cause "No tests found" errors. 16 | // See AndroidX's TestRequestBuilder$MethodFilter for where this is cross-referenced in the instrumentation! 17 | // 18 | // History: 19 | // - #199 & #207 (the original unearthing of this behavior) 20 | // - #317 (making an exception for dynamic tests) 21 | // - #339 (retain indices of parameterized methods to avoid premature filtering by JUnit 4's test discovery) 22 | if (legacyFormat) { 23 | val reportName = identifier.legacyReportingName 24 | val paramStartIndex = reportName.indexOf('(') 25 | if (paramStartIndex > -1) { 26 | val result = reportName.substring(0, paramStartIndex) 27 | 28 | val paramEndIndex = reportName.lastIndexOf('[') 29 | 30 | return if (paramEndIndex > -1) { 31 | // Retain suffix of parameterized methods (i.e. "[1]", "[2]" etc) 32 | // so that they won't be filtered out by JUnit 4 on isolated method runs 33 | result + reportName.substring(paramEndIndex) 34 | } else { 35 | result 36 | } 37 | } 38 | } 39 | 40 | // Process the display name before handing it out, 41 | // maintaining compatibility with the expectations of Android's instrumentation: 42 | // - Cut off no-parameter brackets '()' 43 | // - Replace any other round brackets with square brackets (for parameterized tests) 44 | // to ensure that logs are displayed in the test results window (ref. #350) 45 | return identifier.displayName 46 | .replace("()", "") 47 | .replace('(', '[') 48 | .replace(')', ']') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/DummyJUnit5.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.runners 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import de.mannodermaus.junit5.internal.LOG_TAG 6 | import de.mannodermaus.junit5.internal.dummy.JupiterTestMethodFinder 7 | import org.junit.runner.Description 8 | import org.junit.runner.Runner 9 | import org.junit.runner.notification.RunNotifier 10 | import java.lang.reflect.Method 11 | 12 | /** 13 | * Fake Runner that marks all JUnit 5 methods as ignored, 14 | * used for old devices without Java 8 capabilities. 15 | */ 16 | internal class DummyJUnit5(private val testClass: Class<*>) : Runner() { 17 | 18 | private val testMethods: Set = JupiterTestMethodFinder.find(testClass) 19 | 20 | override fun run(notifier: RunNotifier) { 21 | Log.w( 22 | LOG_TAG, 23 | "JUnit 5 is not supported on this device: " + 24 | "API level ${Build.VERSION.SDK_INT} is less than 26, the minimum requirement. " + 25 | "All Jupiter tests for ${testClass.name} will be disabled." 26 | ) 27 | 28 | for (testMethod in testMethods) { 29 | val description = Description.createTestDescription(testClass, testMethod.name) 30 | notifier.fireTestIgnored(description) 31 | } 32 | } 33 | 34 | override fun getDescription(): Description = 35 | Description.createSuiteDescription(testClass).also { 36 | testMethods.forEach { method -> 37 | it.addChild(Description.createTestDescription(testClass, method.name)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/JUnit5RunnerFactory.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.runners 2 | 3 | import android.os.Build 4 | import org.junit.runner.Runner 5 | 6 | /** 7 | * Since we can't reference AndroidJUnit5 directly, use this factory for instantiation. 8 | * 9 | * On API 26 and above, delegate to the real implementation to drive JUnit 5 tests. 10 | * Below that however, they wouldn't work; for this case, delegate a dummy runner 11 | * which will highlight these tests as ignored. 12 | */ 13 | internal fun tryCreateJUnit5Runner( 14 | klass: Class<*>, 15 | paramsSupplier: () -> AndroidJUnit5RunnerParams 16 | ): Runner? { 17 | val runner = if (Build.VERSION.SDK_INT >= 26) { 18 | AndroidJUnit5(klass, paramsSupplier) 19 | } else { 20 | DummyJUnit5(klass) 21 | } 22 | 23 | // It's still possible for the runner to not be relevant to the test run, 24 | // which is related to how further filters are applied (e.g. via @Tag). 25 | // Only return the runner to the instrumentation if it has any tests to contribute, 26 | // otherwise there would be a mismatch between the number of test classes reported 27 | // to Android, and the number of test classes actually tested with JUnit 5 (ref #298) 28 | return runner.takeIf(Runner::hasExecutableTests) 29 | } 30 | 31 | private fun Runner.hasExecutableTests() = 32 | this.description.children.isNotEmpty() 33 | -------------------------------------------------------------------------------- /instrumentation/runner/src/main/kotlin/de/mannodermaus/junit5/internal/runners/notification/FilteredRunListener.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.runners.notification 2 | 3 | import org.junit.runner.Description 4 | import org.junit.runner.notification.Failure 5 | import org.junit.runner.notification.RunListener 6 | 7 | /** 8 | * A wrapper implementation around JUnit's [RunListener] class 9 | * which only works selectively. In other words, this implementation only delegates 10 | * to its parameter for test descriptors that pass the given [filter]. 11 | */ 12 | internal class FilteredRunListener( 13 | private val delegate: RunListener, 14 | private val filter: (Description) -> Boolean, 15 | ) : RunListener() { 16 | override fun testStarted(description: Description) { 17 | if (filter(description)) { 18 | delegate.testStarted(description) 19 | } 20 | } 21 | 22 | override fun testIgnored(description: Description) { 23 | if (filter(description)) { 24 | delegate.testIgnored(description) 25 | } 26 | } 27 | 28 | override fun testFailure(failure: Failure) { 29 | if (filter(failure.description)) { 30 | delegate.testFailure(failure) 31 | } 32 | } 33 | 34 | override fun testAssumptionFailure(failure: Failure) { 35 | if (filter(failure.description)) { 36 | delegate.testAssumptionFailure(failure) 37 | } 38 | } 39 | 40 | override fun testFinished(description: Description) { 41 | if (filter(description)) { 42 | delegate.testFinished(description) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/AndroidJUnit5BuilderTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import android.os.Build 4 | import com.google.common.truth.Truth.assertThat 5 | import de.mannodermaus.junit5.testutil.AndroidBuildUtils.withApiLevel 6 | import de.mannodermaus.junit5.testutil.AndroidBuildUtils.withMockedInstrumentation 7 | import org.junit.jupiter.api.DynamicContainer.dynamicContainer 8 | import org.junit.jupiter.api.DynamicNode 9 | import org.junit.jupiter.api.DynamicTest.dynamicTest 10 | import org.junit.jupiter.api.TestFactory 11 | 12 | class AndroidJUnit5BuilderTests { 13 | 14 | private val builder = AndroidJUnit5Builder() 15 | 16 | @TestFactory 17 | fun `no runner is created if class only contains top-level test methods`() = runTest( 18 | expectSuccess = false, 19 | // In Kotlin, a 'Kt'-suffixed class of top-level functions cannot be referenced 20 | // via the ::class syntax, so construct a reference to the class directly 21 | Class.forName(javaClass.packageName + ".TestClassesKt") 22 | ) 23 | 24 | @TestFactory 25 | fun `runner is created correctly for classes with valid jupiter test methods`() = runTest( 26 | expectSuccess = true, 27 | HasTest::class.java, 28 | HasRepeatedTest::class.java, 29 | HasTestFactory::class.java, 30 | HasTestTemplate::class.java, 31 | HasParameterizedTest::class.java, 32 | HasInnerClassWithTest::class.java, 33 | HasTaggedTest::class.java, 34 | HasInheritedTestsFromClass::class.java, 35 | HasInheritedTestsFromInterface::class.java, 36 | HasMultipleInheritancesAndOverrides::class.java, 37 | ) 38 | 39 | @TestFactory 40 | fun `no runner is created if class has no jupiter test methods`() = runTest( 41 | expectSuccess = false, 42 | DoesntHaveTestMethods::class.java, 43 | HasJUnit4Tests::class.java, 44 | kotlin.time.Duration::class.java, 45 | ) 46 | 47 | /* Private */ 48 | 49 | private fun runTest(expectSuccess: Boolean, vararg classes: Class<*>): List { 50 | // Generate a test container for each given class, 51 | // then create two sub-variants for testing both DummyJUnit5 and AndroidJUnit5 52 | return classes.map { cls -> 53 | dynamicContainer( 54 | /* displayName = */ cls.name, 55 | /* dynamicNodes = */ setOf(Build.VERSION_CODES.M, Build.VERSION_CODES.TIRAMISU).map { apiLevel -> 56 | dynamicTest("API Level $apiLevel") { 57 | withMockedInstrumentation { 58 | withApiLevel(apiLevel) { 59 | val runner = builder.runnerForClass(cls) 60 | if (expectSuccess) { 61 | assertThat(runner).isNotNull() 62 | } else { 63 | assertThat(runner).isNull() 64 | } 65 | } 66 | } 67 | } 68 | } 69 | ) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/TestHelpers.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5 2 | 3 | import org.junit.platform.engine.discovery.DiscoverySelectors 4 | import org.junit.platform.launcher.Launcher 5 | import org.junit.platform.launcher.TestPlan 6 | import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder 7 | import org.junit.platform.launcher.core.LauncherFactory 8 | import kotlin.reflect.KClass 9 | 10 | /** 11 | * A quick one-liner for executing a Jupiter discover-and-execute pass 12 | * from inside of a Jupiter test. Useful for testing runner code 13 | * that needs to work with the innards of the [TestPlan], such as 14 | * individual test identifiers and such. 15 | */ 16 | fun discoverTests( 17 | cls: KClass<*>, 18 | launcher: Launcher = LauncherFactory.create(), 19 | executeAsWell: Boolean = true, 20 | ): TestPlan { 21 | return launcher.discover( 22 | LauncherDiscoveryRequestBuilder.request() 23 | .selectors(DiscoverySelectors.selectClass(cls.java)) 24 | .build() 25 | ).also { plan -> 26 | if (executeAsWell) { 27 | launcher.execute(plan) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /instrumentation/runner/src/test/kotlin/de/mannodermaus/junit5/internal/discovery/PropertiesParserTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.internal.discovery 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | class PropertiesParserTests { 7 | 8 | @Test 9 | fun `test valid string, containing one entry`() { 10 | val string = "KEY1=true" 11 | val variables = PropertiesParser.fromString(string) 12 | assertThat(variables).containsExactly( 13 | "KEY1", "true" 14 | ) 15 | } 16 | 17 | @Test 18 | fun `test valid string, containing multiple entries`() { 19 | val string = "KEY1=true,KEY2=123,KEY3=lol" 20 | val variables = PropertiesParser.fromString(string) 21 | assertThat(variables).containsExactly( 22 | "KEY1", "true", 23 | "KEY2", "123", 24 | "KEY3", "lol" 25 | ) 26 | } 27 | 28 | @Test 29 | fun `test invalid string, filter out those entries`() { 30 | val string = "KEY1=true,INVALID1,INVALID2=lol=lolol,1234567" 31 | val variables = PropertiesParser.fromString(string) 32 | assertThat(variables).containsExactly( 33 | "KEY1", "true" 34 | ) 35 | } 36 | 37 | @Test 38 | fun `test invalid string, return empty map`() { 39 | val string = "" 40 | val variables = PropertiesParser.fromString(string) 41 | assertThat(variables).isEmpty() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /instrumentation/sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestLogEvent 2 | 3 | plugins { 4 | id("com.android.application") 5 | kotlin("android") 6 | id("jacoco") 7 | id("de.mannodermaus.android-junit5").version(Artifacts.Plugin.latestStableVersion) 8 | } 9 | 10 | val javaVersion = JavaVersion.VERSION_11 11 | 12 | android { 13 | namespace = "de.mannodermaus.junit5.sample" 14 | compileSdk = Android.compileSdkVersion 15 | 16 | defaultConfig { 17 | applicationId = "de.mannodermaus.junit5.sample" 18 | minSdk = Android.sampleMinSdkVersion 19 | targetSdk = Android.targetSdkVersion 20 | versionCode = 1 21 | versionName = "1.0" 22 | 23 | // Make sure to use the AndroidJUnitRunner (or a sub-class) in order to hook in the JUnit 5 Test Builder 24 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 25 | 26 | // These two lines are not needed for a normal integration; 27 | // this sample project disables the automatic integration, so it must be done manually 28 | testInstrumentationRunnerArguments["runnerBuilder"] = "de.mannodermaus.junit5.AndroidJUnit5Builder" 29 | testInstrumentationRunnerArguments["configurationParameters"] = "junit.jupiter.execution.parallel.enabled=true,junit.jupiter.execution.parallel.mode.default=concurrent" 30 | 31 | buildFeatures { 32 | buildConfig = true 33 | } 34 | 35 | buildConfigField("boolean", "MY_VALUE", "true") 36 | 37 | testOptions { 38 | animationsDisabled = true 39 | } 40 | } 41 | 42 | // Add Kotlin source directory to all source sets 43 | sourceSets.forEach { 44 | it.java.srcDir("src/${it.name}/kotlin") 45 | } 46 | 47 | compileOptions { 48 | sourceCompatibility = javaVersion 49 | targetCompatibility = javaVersion 50 | } 51 | 52 | kotlinOptions { 53 | jvmTarget = javaVersion.toString() 54 | } 55 | } 56 | 57 | junitPlatform { 58 | // Configure JUnit 5 tests here 59 | filters("debug") { 60 | excludeTags("slow") 61 | } 62 | 63 | // Using local dependency instead of Maven coordinates 64 | instrumentationTests.enabled = false 65 | } 66 | 67 | tasks.withType { 68 | testLogging.events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) 69 | } 70 | 71 | dependencies { 72 | implementation(libs.kotlinStdLib) 73 | 74 | testImplementation(libs.junitJupiterApi) 75 | testImplementation(libs.junitJupiterParams) 76 | testRuntimeOnly(libs.junitJupiterEngine) 77 | 78 | androidTestImplementation(libs.junit4) 79 | androidTestImplementation(libs.androidXTestRunner) 80 | 81 | // Android Instrumentation Tests wth JUnit 5 82 | androidTestImplementation(libs.junitJupiterApi) 83 | androidTestImplementation(libs.junitJupiterParams) 84 | androidTestImplementation(libs.espressoCore) 85 | androidTestImplementation(project(":core")) 86 | androidTestRuntimeOnly(project(":runner")) 87 | } 88 | -------------------------------------------------------------------------------- /instrumentation/sample/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /instrumentation/sample/src/androidTest/kotlin/de/mannodermaus/sample/TestRunningOnJUnit4.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.sample 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class TestRunningOnJUnit4 { 7 | @Test 8 | fun junit4() { 9 | Assert.assertEquals(4, 2 + 2) 10 | } 11 | 12 | @Test 13 | fun junit4_2() { 14 | Assert.assertEquals(4, 2 + 2) 15 | } 16 | 17 | @Test 18 | fun junit4_3() { 19 | Assert.assertEquals(4, 2 + 2) 20 | } 21 | 22 | @Test 23 | fun junit4_4() { 24 | Assert.assertEquals(4, 2 + 2) 25 | } 26 | 27 | @Test 28 | fun junit4_5() { 29 | Assert.assertEquals(4, 2 + 2) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /instrumentation/sample/src/androidTest/kotlin/de/mannodermaus/sample/TestRunningOnJUnit5.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.sample 2 | 3 | import org.junit.jupiter.api.Assertions 4 | import org.junit.jupiter.api.Assumptions 5 | import org.junit.jupiter.api.Disabled 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.parallel.Execution 8 | import org.junit.jupiter.api.parallel.ExecutionMode 9 | import org.junit.jupiter.params.ParameterizedTest 10 | import org.junit.jupiter.params.provider.ValueSource 11 | 12 | @Execution(ExecutionMode.CONCURRENT) 13 | class TestRunningOnJUnit5 { 14 | @Test 15 | fun junit5_1() { 16 | Thread.sleep(1000) 17 | Assertions.assertEquals(4, 2 + 2) 18 | } 19 | 20 | @Disabled 21 | @Test 22 | fun junit5_2() { 23 | Thread.sleep(2000) 24 | Assertions.assertEquals(4, 2 + 2) 25 | } 26 | 27 | @Test 28 | fun junit5_3() { 29 | Thread.sleep(3000) 30 | Assertions.assertEquals(4, 2 + 2) 31 | } 32 | 33 | @Test 34 | fun junit5_4() { 35 | Assumptions.assumeTrue(false, "Failed assumption on purpose") 36 | Assertions.assertEquals(4, 2 + 2) 37 | } 38 | 39 | @ValueSource(ints = [1, 2, 3]) 40 | @ParameterizedTest 41 | fun junit5_parameterized(value: Int) { 42 | Thread.sleep(value * 1000L) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /instrumentation/sample/src/androidTest/kotlin/de/mannodermaus/sample/TestTemplateExampleTests.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.sample 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.TestTemplate 5 | import org.junit.jupiter.api.extension.ExtendWith 6 | import org.junit.jupiter.api.extension.Extension 7 | import org.junit.jupiter.api.extension.ExtensionContext 8 | import org.junit.jupiter.api.extension.ParameterContext 9 | import org.junit.jupiter.api.extension.TestTemplateInvocationContext 10 | import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider 11 | import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver 12 | import java.util.stream.Stream 13 | 14 | class TestTemplateExampleTests { 15 | @TestTemplate 16 | @ExtendWith(NameAndLengthTemplateContextProvider::class) 17 | fun testTemplate(testCase: TemplateTestCase) { 18 | assertEquals(testCase.expectedLength, testCase.name.length) 19 | } 20 | } 21 | 22 | data class TemplateTestCase( 23 | val name: String, 24 | val expectedLength: Int, 25 | ) 26 | 27 | class NameAndLengthTemplateContextProvider : TestTemplateInvocationContextProvider { 28 | override fun supportsTestTemplate(context: ExtensionContext): Boolean { 29 | return true 30 | } 31 | 32 | override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream { 33 | return Stream.of( 34 | createCase("Alice", 5), 35 | createCase("Bob", 3) 36 | ) 37 | } 38 | 39 | private fun createCase(name: String, expected: Int): TestTemplateInvocationContext { 40 | val testCase = TemplateTestCase(name, expected) 41 | 42 | return object : TestTemplateInvocationContext { 43 | override fun getDisplayName(invocationIndex: Int): String { 44 | return "${testCase.name} has ${testCase.expectedLength} letters" 45 | } 46 | 47 | override fun getAdditionalExtensions(): List { 48 | return listOf(object : TypeBasedParameterResolver() { 49 | override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): TemplateTestCase { 50 | return testCase 51 | } 52 | }) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /instrumentation/sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /instrumentation/sample/src/main/kotlin/de/mannodermaus/junit5/sample/ActivityOne.kt: -------------------------------------------------------------------------------- 1 | package de.mannodermaus.junit5.sample 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.widget.Button 6 | import android.widget.TextView 7 | 8 | class ActivityOne : Activity() { 9 | 10 | private val textView by lazy { findViewById(R.id.textView) } 11 | private val button by lazy { findViewById