From 9863427e97988d2aae4232494c660b066e1a916f Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sat, 11 May 2024 12:20:00 +0900 Subject: [PATCH 1/7] Add roborazzi-junit5 artifact to bridge the gap for all kinds of JUnit 5 tests --- gradle/libs.versions.toml | 5 ++ .../roborazzi/DefaultFileNameGenerator.kt | 55 ++++---------- .../takahirom/roborazzi/RoborazziOptions.kt | 20 +++++ .../StackTraceTestNameExtractionStrategy.kt | 38 ++++++++++ .../roborazzi/TestNameExtractionStrategy.kt | 5 ++ .../takahirom/roborazzi/roboOutputName.kt | 2 +- roborazzi-junit5/.gitignore | 1 + roborazzi-junit5/build.gradle | 29 ++++++++ roborazzi-junit5/consumer-rules.pro | 0 roborazzi-junit5/proguard-rules.pro | 21 ++++++ .../roborazzi/junit5/CurrentTestInfo.kt | 25 +++++++ .../JUnit5TestNameExtractionStrategy.kt | 35 +++++++++ .../roborazzi/junit5/RoborazziExtension.kt | 54 ++++++++++++++ .../org.junit.jupiter.api.extension.Extension | 1 + .../takahirom/roborazzi/junit5/CustomTest.kt | 56 ++++++++++++++ .../junit5/DefaultFileNameGeneratorTest.kt | 74 +++++++++++++++++++ settings.gradle | 1 + 17 files changed, 380 insertions(+), 42 deletions(-) create mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt create mode 100644 include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/TestNameExtractionStrategy.kt create mode 100644 roborazzi-junit5/.gitignore create mode 100644 roborazzi-junit5/build.gradle create mode 100644 roborazzi-junit5/consumer-rules.pro create mode 100644 roborazzi-junit5/proguard-rules.pro create mode 100644 roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfo.kt create mode 100644 roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/JUnit5TestNameExtractionStrategy.kt create mode 100644 roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt create mode 100644 roborazzi-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CustomTest.kt create mode 100644 roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/DefaultFileNameGeneratorTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6302de32d..6a4da7785 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ kim = "0.17.7" dropbox-differ = "0.0.2" google-android-material = "1.5.0" junit = "4.13.2" +junit5 = "5.10.2" ktor-serialization-kotlinx-xml = "2.3.0" kotlinx-serialization = "1.6.3" squareup-okhttp = "5.0.0-alpha.11" @@ -82,6 +83,10 @@ compose-ui-test-junit4-desktop = { module = "org.jetbrains.compose.ui:ui-test-ju dropbox-differ = { module = "com.dropbox.differ:differ", version.ref = "dropbox-differ" } google-android-material = { module = "com.google.android.material:material", version.ref = "google-android-material" } junit = { module = "junit:junit", version.ref = "junit" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } +junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-serialization-kotlinx-xml = { module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor-serialization-kotlinx-xml" } squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "squareup-okhttp" } diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt index 4abb6f65b..bee5bf83b 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/DefaultFileNameGenerator.kt @@ -1,10 +1,8 @@ package com.github.takahirom.roborazzi -import org.junit.Test import org.junit.runner.Description import java.io.File - object DefaultFileNameGenerator { enum class DefaultNamingStrategy(val optionName: String) { TestPackageAndClassAndMethod("testPackageAndClassAndMethod"), @@ -22,6 +20,9 @@ object DefaultFileNameGenerator { private val defaultNamingStrategy by lazy { roborazziDefaultNamingStrategy() } + private val testNameExtractionStrategies by lazy { + roborazziTestNameExtractionStrategies() + } @InternalRoborazziApi fun generateFilePath(extension: String): String { @@ -38,59 +39,31 @@ object DefaultFileNameGenerator { return when (roborazziRecordFilePathStrategy()) { RoborazziRecordFilePathStrategy.RelativePathFromCurrentDirectory -> { val dir = roborazziContext.outputDirectory - "$dir/${generateCountableOutputNameWithStacktrace()}.$extension" + "$dir/${generateCountableOutputNameWithStrategies()}.$extension" } RoborazziRecordFilePathStrategy.RelativePathFromRoborazziContextOutputDirectory -> { // The directory is specified by [fileWithRecordFilePathStrategy(filePath)] - "${generateCountableOutputNameWithStacktrace()}.$extension" + "${generateCountableOutputNameWithStrategies()}.$extension" } } } - val jupiterTestAnnotationOrNull = try { - Class.forName("org.junit.jupiter.api.Test") as Class - } catch (e: ClassNotFoundException) { - null - } - - private fun generateCountableOutputNameWithStacktrace(): String { + private fun generateCountableOutputNameWithStrategies(): String { val testName = - generateOutputNameWithStackTrace() + generateOutputNameWithStrategies() return countableOutputName(testName) } - internal fun generateOutputNameWithStackTrace(): String { - // Find test method name - val allStackTraces = Thread.getAllStackTraces() - val filteredTracces = allStackTraces - // The Thread Name is come from here - // https://github.com/robolectric/robolectric/blob/40832ada4a0651ecbb0151ebed2c99e9d1d71032/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java#L67 - .filterKeys { - it.name.contains("Main Thread") - || it.name.contains("Test worker") - } - val traceElements = filteredTracces - .flatMap { it.value.toList() } - val stackTraceElement = traceElements - .firstOrNull { - try { - val method = Class.forName(it.className).getMethod(it.methodName) - method - .getAnnotation(Test::class.java) != null || - (jupiterTestAnnotationOrNull != null && (method - .getAnnotation(jupiterTestAnnotationOrNull) as? Annotation) != null) - } catch (e: NoClassDefFoundError) { - false - } catch (e: Exception) { - false - } + internal fun generateOutputNameWithStrategies(): String { + for (strategy in testNameExtractionStrategies) { + strategy.extract()?.let { (className, methodName) -> + return generateOutputName(className, methodName) } - ?: throw IllegalArgumentException("Roborazzi can't find method of test. Please specify file name or use Rule") - val testName = - generateOutputName(stackTraceElement.className, stackTraceElement.methodName) - return testName + } + + throw IllegalArgumentException("Roborazzi can't find method of test. Please specify file name or use Rule") } private fun countableOutputName(testName: String): String { diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt index 1458cd038..2bacffb3d 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/RoborazziOptions.kt @@ -68,6 +68,26 @@ fun roborazziDefaultNamingStrategy(): DefaultFileNameGenerator.DefaultNamingStra ) } +fun roborazziTestNameExtractionStrategies(): List { + return buildList { + // Always use the default strategy with stack traces, + // then add the JUnit 5 integration as well (if present on the classpath) + add(StackTraceTestNameExtractionStrategy) + junit5TestNameExtractionStrategy?.let(::add) + } +} + +private val junit5TestNameExtractionStrategy by lazy { + try { + Class.forName("com.github.takahirom.roborazzi.junit5.JUnit5TestNameExtractionStrategy") + .getConstructor() + .newInstance() + as TestNameExtractionStrategy + } catch (ignored: ClassNotFoundException) { + null + } +} + data class RoborazziOptions( /** * This option, taskType, is experimental. So the API may change. diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt new file mode 100644 index 000000000..c741d7c90 --- /dev/null +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt @@ -0,0 +1,38 @@ +package com.github.takahirom.roborazzi + +import org.junit.Test + +/** + * Default strategy for finding a suitable output name for [DefaultFileNameGenerator]. + * This implementation looks up the test class and method from the current stack trace. + */ +internal object StackTraceTestNameExtractionStrategy : TestNameExtractionStrategy { + override fun extract(): Pair? { + // Find test method name + val allStackTraces = Thread.getAllStackTraces() + val filteredTracces = allStackTraces + // The Thread Name is come from here + // https://github.com/robolectric/robolectric/blob/40832ada4a0651ecbb0151ebed2c99e9d1d71032/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java#L67 + .filterKeys { + it.name.contains("Main Thread") + || it.name.contains("Test worker") + } + val traceElements = filteredTracces + .flatMap { it.value.toList() } + val stackTraceElement = traceElements + .firstOrNull { + try { + val method = Class.forName(it.className).getMethod(it.methodName) + method.getAnnotation(Test::class.java) != null + } catch (e: NoClassDefFoundError) { + false + } catch (e: Exception) { + false + } + } + + return stackTraceElement?.let { + it.className to it.methodName + } + } +} diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/TestNameExtractionStrategy.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/TestNameExtractionStrategy.kt new file mode 100644 index 000000000..7ca5aa1db --- /dev/null +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/TestNameExtractionStrategy.kt @@ -0,0 +1,5 @@ +package com.github.takahirom.roborazzi + +interface TestNameExtractionStrategy { + fun extract(): Pair? +} diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/roboOutputName.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/roboOutputName.kt index c11bc3560..331e2e16e 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/roboOutputName.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/roboOutputName.kt @@ -11,5 +11,5 @@ fun roboOutputName(): String { if (description != null) { return DefaultFileNameGenerator.generateOutputNameWithDescription(description) } - return DefaultFileNameGenerator.generateOutputNameWithStackTrace() + return DefaultFileNameGenerator.generateOutputNameWithStrategies() } \ No newline at end of file diff --git a/roborazzi-junit5/.gitignore b/roborazzi-junit5/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/roborazzi-junit5/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/roborazzi-junit5/build.gradle b/roborazzi-junit5/build.gradle new file mode 100644 index 000000000..5795ac401 --- /dev/null +++ b/roborazzi-junit5/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' +} +if (System.getenv("INTEGRATION_TEST") != "true") { + pluginManager.apply("com.vanniktech.maven.publish") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +test { + useJUnitPlatform() + systemProperty("junit.jupiter.extensions.autodetection.enabled", true) +} + +dependencies { + // Please see settings.gradle + implementation "io.github.takahirom.roborazzi:roborazzi-core:$VERSION_NAME" + implementation libs.junit.jupiter.api + + testImplementation libs.junit + testImplementation libs.junit.jupiter.api + testImplementation libs.junit.jupiter.params + testRuntimeOnly libs.junit.jupiter.engine + testRuntimeOnly libs.junit.vintage.engine +} \ No newline at end of file diff --git a/roborazzi-junit5/consumer-rules.pro b/roborazzi-junit5/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/roborazzi-junit5/proguard-rules.pro b/roborazzi-junit5/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/roborazzi-junit5/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfo.kt b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfo.kt new file mode 100644 index 000000000..523ee1cfe --- /dev/null +++ b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfo.kt @@ -0,0 +1,25 @@ +package com.github.takahirom.roborazzi.junit5 + +import org.junit.jupiter.api.TestInfo + +/** + * A shared, static data container for storing the currently executed test method. + * This is updated by [RoborazziExtension] and read by [JUnit5TestNameExtractionStrategy] + * from different class loaders, bridging the gap between test definition and their execution. + */ +internal object CurrentTestInfo { + private val concurrentRef = ThreadLocal() + private var sameThreadRef: TestInfo? = null + + fun set(info: TestInfo?, isConcurrent: Boolean) { + if (isConcurrent) { + concurrentRef.set(info) + } else { + sameThreadRef = info + } + } + + fun get(): TestInfo? { + return concurrentRef.get() ?: sameThreadRef + } +} diff --git a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/JUnit5TestNameExtractionStrategy.kt b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/JUnit5TestNameExtractionStrategy.kt new file mode 100644 index 000000000..11d4424f5 --- /dev/null +++ b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/JUnit5TestNameExtractionStrategy.kt @@ -0,0 +1,35 @@ +package com.github.takahirom.roborazzi.junit5 + +import com.github.takahirom.roborazzi.TestNameExtractionStrategy +import org.junit.jupiter.api.TestInfo + +/** + * Implementation of [TestNameExtractionStrategy] for JUnit 5 tests using Roborazzi. + * Since JUnit 5's dynamic test method names cannot be detected reliably via the default strategy, + * utilize the built-in extension model to track the class and method of the currently executed test. + * + * This class is executed from the Robolectric main thread using its sandboxed class loader, + * which is why it has to jump through several hoops to obtain the static knowledge + * stored inside [CurrentTestInfo]. We need to utilize reflection to access its getter method + * to prevent accidentally creating a second object and missing the actual value. + */ +internal class JUnit5TestNameExtractionStrategy : TestNameExtractionStrategy { + private val getCurrentTestInfo by lazy { createCurrentTestInfoGetterWithReflection() } + + override fun extract(): Pair? { + return getCurrentTestInfo()?.let { info -> + info.testClass.get().name to info.testMethod.get().name + } + } + + private fun createCurrentTestInfoGetterWithReflection(): () -> TestInfo? { + // Ensure usage of the system class loader here, + // which is also used by RoborazziExtension + val cl = ClassLoader.getSystemClassLoader() + val cls = cl.loadClass(CurrentTestInfo::class.java.name) + val instance = cls.getDeclaredField("INSTANCE").get(null) + val method = cls.getDeclaredMethod("get") + + return { method.invoke(instance) as? TestInfo } + } +} diff --git a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt new file mode 100644 index 000000000..1e0cf7fd9 --- /dev/null +++ b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt @@ -0,0 +1,54 @@ +package com.github.takahirom.roborazzi.junit5 + +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.parallel.ExecutionMode +import java.lang.reflect.Method +import java.util.Optional + +/** + * A JUnit 5 extension to track the currently executed test methods in a static reference. + * This allows the [JUnit5TestNameExtractionStrategy] to extract the correct file name + * for the default image capture when called from a JUnit 5 test method. + */ +class RoborazziExtension : BeforeEachCallback, AfterEachCallback { + + override fun beforeEach(context: ExtensionContext) { + val isConcurrent = requireNotNull(context.executionMode) == ExecutionMode.CONCURRENT + CurrentTestInfo.set(TestInfoImpl(context), isConcurrent) + } + + override fun afterEach(context: ExtensionContext) { + val isConcurrent = requireNotNull(context.executionMode) == ExecutionMode.CONCURRENT + CurrentTestInfo.set(null, isConcurrent) + } + + private class TestInfoImpl(context: ExtensionContext) : TestInfo { + private val displayName = context.displayName + private val tags = context.tags + private val testClass = context.testClass + private val testMethod = context.testMethod + + override fun getDisplayName(): String { + return displayName + } + + override fun getTags(): MutableSet { + return tags + } + + override fun getTestClass(): Optional> { + return testClass + } + + override fun getTestMethod(): Optional { + return testMethod + } + + override fun toString(): String { + return testMethod.toString() + } + } +} diff --git a/roborazzi-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/roborazzi-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..9233645ee --- /dev/null +++ b/roborazzi-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +com.github.takahirom.roborazzi.junit5.RoborazziExtension diff --git a/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CustomTest.kt b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CustomTest.kt new file mode 100644 index 000000000..9b99034f7 --- /dev/null +++ b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CustomTest.kt @@ -0,0 +1,56 @@ +package com.github.takahirom.roborazzi.junit5 + +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import org.junit.jupiter.api.extension.TestTemplateInvocationContext +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider +import java.util.stream.Stream + +/** + * Example of a custom JUnit 5 test template, ensuring that Roborazzi file name generation + * also works with custom extensions to the Jupiter test model. The idea of [CustomTest] + * is to simply run any annotated test method twice. + */ +@TestTemplate +@ExtendWith(CustomTestTemplateContextProvider::class) +annotation class CustomTest + +private class CustomTestTemplateContextProvider : TestTemplateInvocationContextProvider { + override fun supportsTestTemplate(context: ExtensionContext?): Boolean { + return true + } + + override fun provideTestTemplateInvocationContexts(context: ExtensionContext?): Stream { + return Stream.of(CustomTestTemplateContext(true), CustomTestTemplateContext(false)) + } +} + +private class CustomTestTemplateContext(private val isFirst: Boolean) : + TestTemplateInvocationContext { + override fun getDisplayName(invocationIndex: Int) = buildString { + append(super.getDisplayName(invocationIndex)) + append(if (isFirst) " first" else " second") + append(" invocation") + } + + override fun getAdditionalExtensions() = listOf(CustomTestParameterResolver(isFirst)) +} + +private class CustomTestParameterResolver(private val isFirst: Boolean) : ParameterResolver { + override fun supportsParameter( + parameterContext: ParameterContext, + extensionContext: ExtensionContext + ): Boolean { + return parameterContext.parameter.type == Boolean::class.java + } + + override fun resolveParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Any { + return isFirst + } +} diff --git a/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/DefaultFileNameGeneratorTest.kt b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/DefaultFileNameGeneratorTest.kt new file mode 100644 index 000000000..bd72a0112 --- /dev/null +++ b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/DefaultFileNameGeneratorTest.kt @@ -0,0 +1,74 @@ +package com.github.takahirom.roborazzi.junit5 + +import com.github.takahirom.roborazzi.DefaultFileNameGenerator +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicContainer.dynamicContainer +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.RepetitionInfo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +abstract class DefaultFileNameGeneratorTest { + protected fun runTest(expectedSuffix: String) { + val filePath = DefaultFileNameGenerator.generateFilePath("png") + val expectedFileName = "${javaClass.name}.$expectedSuffix" + + require(filePath.endsWith(expectedFileName)) { + "Expected generated file name to be '$expectedFileName', but actual file path was: $filePath" + } + } +} + +class DefaultFileNameGeneratorTestWithJUnit4 : DefaultFileNameGeneratorTest() { + @org.junit.Test + fun test() { + runTest("test.png") + } +} + +class DefaultFileNameGeneratorTestWithJUnit5 : DefaultFileNameGeneratorTest() { + @Test + fun test() { + runTest("test.png") + } + + @ParameterizedTest + @ValueSource(strings = ["A", "B"]) + fun parameterizedTest(value: String) { + if (value == "A") { + runTest("parameterizedTest.png") + } else { + runTest("parameterizedTest_2.png") + } + } + + @RepeatedTest(3) + fun repeatedTest(info: RepetitionInfo) { + when (info.currentRepetition) { + 1 -> runTest("repeatedTest.png") + 2 -> runTest("repeatedTest_2.png") + 3 -> runTest("repeatedTest_3.png") + } + } + + @TestFactory + fun testFactory(): DynamicContainer = dynamicContainer( + "testFactory", + listOf( + dynamicTest("first test") { runTest("testFactory.png") }, + dynamicTest("second test") { runTest("testFactory_2.png") }, + ) + ) + + @CustomTest + fun customTest(firstExecution: Boolean) { + if (firstExecution) { + runTest("customTest.png") + } else { + runTest("customTest_2.png") + } + } +} diff --git a/settings.gradle b/settings.gradle index c4108a23f..a58dd6852 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ dependencyResolutionManagement { rootProject.name = "roborazzi" include ':roborazzi' include ':roborazzi-junit-rule' +include ':roborazzi-junit5' include ':roborazzi-compose-desktop' include ':roborazzi-compose-ios' include ':roborazzi-compose' From d53280510f0c4dab9c3d290ea13ff8756ec33fe7 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Mon, 13 May 2024 22:22:34 +0900 Subject: [PATCH 2/7] Add concurrent tests for verifying that CurrentTestInfo can handle parallel execution Fire 100 tests at the same time and check that all references are set correctly --- roborazzi-junit5/build.gradle | 1 + .../roborazzi/junit5/RoborazziExtension.kt | 39 ++-------- .../roborazzi/junit5/TestInfoImpl.kt | 32 ++++++++ .../roborazzi/junit5/CurrentTestInfoTest.kt | 75 +++++++++++++++++++ 4 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/TestInfoImpl.kt create mode 100644 roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfoTest.kt diff --git a/roborazzi-junit5/build.gradle b/roborazzi-junit5/build.gradle index 5795ac401..ffea38e2b 100644 --- a/roborazzi-junit5/build.gradle +++ b/roborazzi-junit5/build.gradle @@ -14,6 +14,7 @@ java { test { useJUnitPlatform() systemProperty("junit.jupiter.extensions.autodetection.enabled", true) + systemProperty("junit.jupiter.execution.parallel.enabled", true) } dependencies { diff --git a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt index 1e0cf7fd9..ad5946034 100644 --- a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt +++ b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/RoborazziExtension.kt @@ -1,12 +1,9 @@ package com.github.takahirom.roborazzi.junit5 -import org.junit.jupiter.api.TestInfo import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.parallel.ExecutionMode -import java.lang.reflect.Method -import java.util.Optional /** * A JUnit 5 extension to track the currently executed test methods in a static reference. @@ -16,39 +13,19 @@ import java.util.Optional class RoborazziExtension : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext) { + val info = TestInfoImpl( + displayName = context.displayName, + tags = context.tags, + testClass = context.testClass, + testMethod = context.testMethod, + ) + val isConcurrent = requireNotNull(context.executionMode) == ExecutionMode.CONCURRENT - CurrentTestInfo.set(TestInfoImpl(context), isConcurrent) + CurrentTestInfo.set(info, isConcurrent) } override fun afterEach(context: ExtensionContext) { val isConcurrent = requireNotNull(context.executionMode) == ExecutionMode.CONCURRENT CurrentTestInfo.set(null, isConcurrent) } - - private class TestInfoImpl(context: ExtensionContext) : TestInfo { - private val displayName = context.displayName - private val tags = context.tags - private val testClass = context.testClass - private val testMethod = context.testMethod - - override fun getDisplayName(): String { - return displayName - } - - override fun getTags(): MutableSet { - return tags - } - - override fun getTestClass(): Optional> { - return testClass - } - - override fun getTestMethod(): Optional { - return testMethod - } - - override fun toString(): String { - return testMethod.toString() - } - } } diff --git a/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/TestInfoImpl.kt b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/TestInfoImpl.kt new file mode 100644 index 000000000..29304100a --- /dev/null +++ b/roborazzi-junit5/src/main/kotlin/com/github/takahirom/roborazzi/junit5/TestInfoImpl.kt @@ -0,0 +1,32 @@ +package com.github.takahirom.roborazzi.junit5 + +import org.junit.jupiter.api.TestInfo +import java.lang.reflect.Method +import java.util.Optional + +internal class TestInfoImpl( + private val displayName: String, + private val tags: MutableSet, + private val testClass: Optional>, + private val testMethod: Optional, +) : TestInfo { + override fun getDisplayName(): String { + return displayName + } + + override fun getTags(): MutableSet { + return tags + } + + override fun getTestClass(): Optional> { + return testClass + } + + override fun getTestMethod(): Optional { + return testMethod + } + + override fun toString(): String { + return testMethod.toString() + } +} diff --git a/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfoTest.kt b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfoTest.kt new file mode 100644 index 000000000..3e76db310 --- /dev/null +++ b/roborazzi-junit5/src/test/kotlin/com/github/takahirom/roborazzi/junit5/CurrentTestInfoTest.kt @@ -0,0 +1,75 @@ +package com.github.takahirom.roborazzi.junit5 + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.RepetitionInfo +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import java.util.Optional + +class CurrentTestInfoTest { + private val methods = Methods::class.java.declaredMethods + + @Execution(ExecutionMode.CONCURRENT) + @RepeatedTest(100) + fun concurrentTest(repetitionInfo: RepetitionInfo) { + val index = repetitionInfo.currentRepetition + + // Part 1: Apply a non-null info object + doRoundTrip( + TestInfoImpl( + displayName = "Test $index", + tags = mutableSetOf("test$index"), + testClass = Optional.of(Methods::class.java), + testMethod = Optional.of(methods[index % methods.size]), + ) + ) { expected, actual -> + assertEquals(expected?.displayName, actual?.displayName) + assertEquals(expected?.tags, actual?.tags) + assertEquals(expected?.testClass, actual?.testClass) + assertEquals(expected?.testMethod, actual?.testMethod) + } + + // Part 2: Clear this info object again + doRoundTrip(null) { _, actual -> + assertNull(actual) + } + } + + private fun doRoundTrip( + expected: TestInfo?, + assertionBlock: (TestInfo?, TestInfo?) -> Unit, + ) { + CurrentTestInfo.set(expected, isConcurrent = true) + val actual = CurrentTestInfo.get() + assertionBlock(expected, actual) + } + + // Dummy class for this test, used to assign + // random methods from each invocation + // of the concurrent test declared above + @Suppress("unused") + private class Methods { + fun test1() {} + fun test2() {} + fun test3() {} + fun test4() {} + fun test5() {} + fun test6() {} + fun test7() {} + fun test8() {} + fun test9() {} + fun test10() {} + fun test11() {} + fun test12() {} + fun test13() {} + fun test14() {} + fun test15() {} + fun test16() {} + fun test17() {} + fun test18() {} + fun test19() {} + } +} From 93da9dbd9e1160c95df7e3d28a9c3acce0839a5f Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Mon, 13 May 2024 22:41:45 +0900 Subject: [PATCH 3/7] Reinstate basic detection of JU5 annotation in stack trace strategy Turns out that kotlin.test defaults to JUnit 5 in certain applications (e.g. Compose Desktop or Multiplatform) and the existing strategy does pick up its method names correctly. Add a comment explaining the context --- .../StackTraceTestNameExtractionStrategy.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt index c741d7c90..3e011b22e 100644 --- a/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt +++ b/include-build/roborazzi-core/src/commonJvmMain/kotlin/com/github/takahirom/roborazzi/StackTraceTestNameExtractionStrategy.kt @@ -1,6 +1,7 @@ package com.github.takahirom.roborazzi import org.junit.Test +import java.lang.reflect.Method /** * Default strategy for finding a suitable output name for [DefaultFileNameGenerator]. @@ -10,20 +11,20 @@ internal object StackTraceTestNameExtractionStrategy : TestNameExtractionStrateg override fun extract(): Pair? { // Find test method name val allStackTraces = Thread.getAllStackTraces() - val filteredTracces = allStackTraces + val filteredTraces = allStackTraces // The Thread Name is come from here // https://github.com/robolectric/robolectric/blob/40832ada4a0651ecbb0151ebed2c99e9d1d71032/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java#L67 .filterKeys { it.name.contains("Main Thread") || it.name.contains("Test worker") } - val traceElements = filteredTracces + val traceElements = filteredTraces .flatMap { it.value.toList() } val stackTraceElement = traceElements .firstOrNull { try { val method = Class.forName(it.className).getMethod(it.methodName) - method.getAnnotation(Test::class.java) != null + method.isJUnit4Test() || method.isJUnit5Test() } catch (e: NoClassDefFoundError) { false } catch (e: Exception) { @@ -35,4 +36,24 @@ internal object StackTraceTestNameExtractionStrategy : TestNameExtractionStrateg it.className to it.methodName } } + + private fun Method.isJUnit4Test(): Boolean { + return getAnnotation(Test::class.java) != null + } + + // This JUnit 5 check works for basic usage of kotlin.test.Test with JUnit 5 + // in basic Compose desktop and multiplatform applications. For more complex + // support including dynamic tests, [JUnit5TestNameExtractionStrategy] is required + @Suppress("UNCHECKED_CAST") + private val jupiterTestAnnotationOrNull = try { + Class.forName("org.junit.jupiter.api.Test") as Class + } catch (e: ClassNotFoundException) { + null + } + + @Suppress("USELESS_CAST") + private fun Method.isJUnit5Test(): Boolean { + return (jupiterTestAnnotationOrNull != null && + (getAnnotation(jupiterTestAnnotationOrNull) as? Annotation) != null) + } } From a900982c9c46b16efc60cfc26e520f6b5585de15 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Tue, 14 May 2024 21:30:16 +0900 Subject: [PATCH 4/7] Add more unit tests to Compose Desktop JVM sample --- sample-compose-desktop-jvm/build.gradle.kts | 5 +++ .../src/main/kotlin/Main.kt | 5 +-- .../src/test/kotlin/MainJvmTest.kt | 36 +++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/sample-compose-desktop-jvm/build.gradle.kts b/sample-compose-desktop-jvm/build.gradle.kts index 5203802b2..c0e9b984c 100644 --- a/sample-compose-desktop-jvm/build.gradle.kts +++ b/sample-compose-desktop-jvm/build.gradle.kts @@ -11,6 +11,7 @@ group = "com.github.takahirom.roborazzi.compose.desktop.jvm.sample" version = "1.0-SNAPSHOT" tasks.test { useJUnitPlatform() + systemProperty("junit.jupiter.extensions.autodetection.enabled", true) } dependencies { @@ -20,8 +21,12 @@ dependencies { // With compose.desktop.common you will also lose @Preview functionality implementation(compose.desktop.currentOs) testImplementation(project(":roborazzi-compose-desktop")) + testImplementation(project(":roborazzi-junit5")) testImplementation(kotlin("test")) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testRuntimeOnly(libs.junit.jupiter.engine) } compose.desktop { diff --git a/sample-compose-desktop-jvm/src/main/kotlin/Main.kt b/sample-compose-desktop-jvm/src/main/kotlin/Main.kt index cf5d12524..c889e4a06 100644 --- a/sample-compose-desktop-jvm/src/main/kotlin/Main.kt +++ b/sample-compose-desktop-jvm/src/main/kotlin/Main.kt @@ -11,14 +11,15 @@ import androidx.compose.ui.window.application @Composable @Preview -fun App() { +fun App(value: String = "test") { var text by remember { mutableStateOf("Hello, World!") } + val updatedValue by rememberUpdatedState(value) MaterialTheme { Button( modifier = Modifier.testTag("button"), onClick = { - text = "Hello, Desktop! test" + text = "Hello, Desktop! $updatedValue" }) { Text( style = MaterialTheme.typography.h2, diff --git a/sample-compose-desktop-jvm/src/test/kotlin/MainJvmTest.kt b/sample-compose-desktop-jvm/src/test/kotlin/MainJvmTest.kt index cacebc0f0..5267d132f 100644 --- a/sample-compose-desktop-jvm/src/test/kotlin/MainJvmTest.kt +++ b/sample-compose-desktop-jvm/src/test/kotlin/MainJvmTest.kt @@ -2,16 +2,48 @@ import androidx.compose.ui.test.* import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG import com.github.takahirom.roborazzi.RoborazziOptions import io.github.takahirom.roborazzi.captureRoboImage +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicContainer.dynamicContainer +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.RepetitionInfo +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import kotlin.test.Test class MainJvmTest { - @OptIn(ExperimentalTestApi::class) @Test fun test() { + runTest() + } + + @ParameterizedTest + @ValueSource(strings = ["first", "second"]) + fun parameterizedTest(value: String) { + runTest("parameterized $value") + } + + @RepeatedTest(2) + fun repeatedTest(info: RepetitionInfo) { + runTest("repeated ${info.currentRepetition}") + } + + @TestFactory + fun testFactory(): DynamicContainer = dynamicContainer( + "container", + listOf( + dynamicTest("one") { runTest("dynamic one") }, + dynamicTest("two") { runTest("dynamic two") }, + ) + ) + + @OptIn(ExperimentalTestApi::class) + private fun runTest(value: String = "test") { ROBORAZZI_DEBUG = true runDesktopComposeUiTest { setContent { - App() + App(value) } val roborazziOptions = RoborazziOptions( compareOptions = RoborazziOptions.CompareOptions(changeThreshold = 0F) From 715eea96884b27732c80f5aa729dbc4c8ba95e4a Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Tue, 14 May 2024 23:08:16 +0900 Subject: [PATCH 5/7] Introduce Android sample project for using Roborazzi with JUnit 5 This requires the introduction of the third-party Robolectric extension and the Gradle plugin for JUnit 5 with Android. --- build.gradle | 2 + gradle/libs.versions.toml | 4 + sample-android-junit5/.gitignore | 1 + sample-android-junit5/build.gradle.kts | 69 +++++++++++++++++ sample-android-junit5/proguard-rules.pro | 21 +++++ .../sample/JUnit5InstrumentedTest.kt | 14 ++++ .../src/main/AndroidManifest.xml | 14 ++++ .../roborazzi/sample/MainActivity.kt | 22 ++++++ .../src/main/res/layout/activity_main.xml | 45 +++++++++++ .../src/main/res/values/attrs.xml | 4 + .../src/main/res/values/strings.xml | 8 ++ .../src/main/res/values/styles.xml | 7 ++ .../roborazzi/sample/JUnit5ManualTest.kt | 76 +++++++++++++++++++ settings.gradle | 1 + 14 files changed, 288 insertions(+) create mode 100644 sample-android-junit5/.gitignore create mode 100644 sample-android-junit5/build.gradle.kts create mode 100644 sample-android-junit5/proguard-rules.pro create mode 100644 sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt create mode 100644 sample-android-junit5/src/main/AndroidManifest.xml create mode 100644 sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt create mode 100644 sample-android-junit5/src/main/res/layout/activity_main.xml create mode 100644 sample-android-junit5/src/main/res/values/attrs.xml create mode 100644 sample-android-junit5/src/main/res/values/strings.xml create mode 100644 sample-android-junit5/src/main/res/values/styles.xml create mode 100644 sample-android-junit5/src/test/kotlin/com/github/takahirom/roborazzi/sample/JUnit5ManualTest.kt diff --git a/build.gradle b/build.gradle index d8581a040..44e296e8f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,8 @@ plugins { id 'com.android.library' version libs.versions.agp apply false id 'org.jetbrains.kotlin.multiplatform' version libs.versions.kotlin apply false id 'org.jetbrains.compose' version libs.versions.composeMultiplatform apply false + id 'de.mannodermaus.android-junit5' version libs.versions.junit5Android apply false + id 'tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin' version libs.versions.junit5Robolectric apply false id 'org.jetbrains.kotlin.android' version libs.versions.kotlin apply false id "com.vanniktech.maven.publish" version libs.versions.mavenPublish apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a4da7785..33158f33c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,8 @@ kotlin = "1.9.21" mavenPublish = "0.25.3" composeCompiler = "1.5.6" composeMultiplatform = "1.6.2" +junit5Android = "1.10.0.0" +junit5Robolectric = "0.5.2" robolectric = "4.12.2" robolectric-android-all = "Q-robolectric-5415296" @@ -31,6 +33,7 @@ androidx-lifecycle = "2.6.1" androidx-navigation = "2.7.7" androidx-test-espresso-core = "3.5.1" androidx-test-ext-junit = "1.1.5" +androidx-test-runner = "1.5.2" kim = "0.17.7" dropbox-differ = "0.0.2" @@ -73,6 +76,7 @@ androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } androidx-test-ext-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-ext-junit" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } kim = { module = "com.ashampoo:kim", version.ref = "kim" } android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/sample-android-junit5/.gitignore b/sample-android-junit5/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/sample-android-junit5/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sample-android-junit5/build.gradle.kts b/sample-android-junit5/build.gradle.kts new file mode 100644 index 000000000..e3d674e35 --- /dev/null +++ b/sample-android-junit5/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.library") + kotlin("android") + id("io.github.takahirom.roborazzi") + id("de.mannodermaus.android-junit5") + id("tech.apter.junit5.jupiter.robolectric-extension-gradle-plugin") +} + +val jvmVersion = JavaVersion.VERSION_17 + +android { + namespace = "com.github.takahirom.roborazzi.sample" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + targetSdk = 34 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = jvmVersion + targetCompatibility = jvmVersion + } + + kotlinOptions { + jvmTarget = jvmVersion.toString() + } + + buildFeatures { + viewBinding = true + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(jvmVersion.toString())) + } +} + +junitPlatform { + configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true") +} + +dependencies { + testImplementation(project(":roborazzi")) + testImplementation(project(":roborazzi-junit5")) + + implementation(kotlin("stdlib")) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.google.android.material) + + testImplementation(libs.robolectric) + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.androidx.test.espresso.core) + testRuntimeOnly(libs.junit.jupiter.engine) + + androidTestImplementation(libs.junit.jupiter.api) + androidTestImplementation(libs.androidx.test.runner) +} diff --git a/sample-android-junit5/proguard-rules.pro b/sample-android-junit5/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/sample-android-junit5/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt b/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt new file mode 100644 index 000000000..3b4e67640 --- /dev/null +++ b/sample-android-junit5/src/androidTest/kotlin/com/github/takahirom/roborazzi/sample/JUnit5InstrumentedTest.kt @@ -0,0 +1,14 @@ +package com.github.takahirom.roborazzi.sample + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class JUnit5InstrumentedTest { + @Test + fun useAppContext() { + // The basic example instrumentation test, but with JUnit 5 + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.github.takahirom.roborazzi.sample.test", appContext.packageName) + } +} diff --git a/sample-android-junit5/src/main/AndroidManifest.xml b/sample-android-junit5/src/main/AndroidManifest.xml new file mode 100644 index 000000000..38fc16e91 --- /dev/null +++ b/sample-android-junit5/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt b/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt new file mode 100644 index 000000000..cad333093 --- /dev/null +++ b/sample-android-junit5/src/main/kotlin/com/github/takahirom/roborazzi/sample/MainActivity.kt @@ -0,0 +1,22 @@ +package com.github.takahirom.roborazzi.sample + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.github.takahirom.roborazzi.sample.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.updateButton.setOnClickListener { + val input = binding.inputEditText.text ?: "" + + binding.descriptionText.text = getString(R.string.text_description_2, input) + } + } +} diff --git a/sample-android-junit5/src/main/res/layout/activity_main.xml b/sample-android-junit5/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..55ea50643 --- /dev/null +++ b/sample-android-junit5/src/main/res/layout/activity_main.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + +