From 8611dfb0dc9191d3e4adc3ff73bff7e53b976a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Varga?= Date: Thu, 9 May 2024 15:12:13 +0200 Subject: [PATCH 1/2] feat: Allow LooperMode annotation on nested tests and test methods --- README.md | 5 +- .../JUnit5RobolectricSandboxManager.kt | 2 - .../internal/JUnit5RobolectricTestRunner.kt | 10 +-- .../JUnit5RobolectricTestRunnerHelper.kt | 2 +- .../robolectric/internal/SandboxExtensions.kt | 31 +++++++ .../internal/validation/TestClassValidator.kt | 2 - .../RobolectricExtensionLooperModeSelfTest.kt | 84 +++++++++++++++++++ .../RobolectricExtensionSelfTest.kt | 5 -- 8 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionLooperModeSelfTest.kt diff --git a/README.md b/README.md index 335469c..843e6b0 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,10 @@ Robolectric's in-memory environment. ## Current Limitations -* **Parallel Execution:** Parallel test execution only supported with classes. +* **Parallel Execution:** Parallel test execution only experimentally supported with classes. * **Configuration:** * Robolectric `@Config`'s sdk parameter annotation can only be set on outermost test class. - * `@ResourcesMode`, `@LooperMode`, `GraphicsMode` annotations can only be set on outermost test - class. + * `@GraphicsMode` annotations can only be set on outermost test class. * **Experimental Status:** This extension is still under development, and its API might change in future versions. diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricSandboxManager.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricSandboxManager.kt index 448ea7a..1dafaaf 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricSandboxManager.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricSandboxManager.kt @@ -36,7 +36,6 @@ internal class JUnit5RobolectricSandboxManager @Inject constructor( instrumentationConfiguration = instrumentationConfig, sdk = sdk, resourcesMode = resourcesMode, - looperMode = looperMode, graphicsMode = graphicsMode, ) // Return the same sandbox for nested tests @@ -52,7 +51,6 @@ internal class JUnit5RobolectricSandboxManager @Inject constructor( private val instrumentationConfiguration: InstrumentationConfiguration, private val sdk: Sdk, private val resourcesMode: ResourcesMode, - private val looperMode: LooperMode.Mode, private val graphicsMode: GraphicsMode.Mode, ) diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt index 129c1eb..684ff45 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt @@ -58,10 +58,8 @@ internal class JUnit5RobolectricTestRunner( frameworkMethod: FrameworkMethod, bootstrappedMethod: Method, ) { - synchronized(beforeTestLock) { - logger.trace { "runBeforeTest ${bootstrappedMethod.declaringClass.simpleName}::${bootstrappedMethod.name}" } - super.beforeTest(sdkEnvironment, frameworkMethod, bootstrappedMethod) - } + logger.trace { "runBeforeTest ${bootstrappedMethod.declaringClass.simpleName}::${bootstrappedMethod.name}" } + super.beforeTest(sdkEnvironment, frameworkMethod, bootstrappedMethod) } fun runAfterTest(frameworkMethod: FrameworkMethod, bootstrappedMethod: Method) { @@ -69,9 +67,11 @@ internal class JUnit5RobolectricTestRunner( super.afterTest(frameworkMethod, bootstrappedMethod) } - fun runFinallyAfterTest(frameworkMethod: FrameworkMethod) { + fun runFinallyAfterTest(sdkEnvironment: Sandbox, frameworkMethod: FrameworkMethod) { logger.trace { "runFinallyAfterTest ${frameworkMethod.declaringClass.simpleName}::${frameworkMethod.name}" } super.finallyAfterTest(frameworkMethod) + sdkEnvironment.clearShadowLooperCache() + sdkEnvironment.resetLooper() } override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration { diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelper.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelper.kt index 997be3e..aa76356 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelper.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunnerHelper.kt @@ -56,7 +56,7 @@ internal class JUnit5RobolectricTestRunnerHelper private constructor(testClass: runWithRobolectric { robolectricTestRunner.runAfterTest(frameworkMethod, testMethod) } - robolectricTestRunner.runFinallyAfterTest(frameworkMethod) + robolectricTestRunner.runFinallyAfterTest(this, frameworkMethod) } ) } diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/SandboxExtensions.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/SandboxExtensions.kt index 60e74a8..bb2e906 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/SandboxExtensions.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/SandboxExtensions.kt @@ -40,3 +40,34 @@ internal fun Sandbox.resetClassLoaderToOriginal() { } Thread.currentThread().contextClassLoader = robolectricClassLoader.parent } + +internal fun Sandbox.clearShadowLooperCache() { + val shadowLooperClass = robolectricClassLoader.loadClass("org.robolectric.shadows.ShadowLooper") + shadowLooperClass.getDeclaredMethod("clearLooperMode").invoke(null) +} + +internal fun Sandbox.resetLooper() { + resetMainLooper() + resetMyLooper() +} + +private fun Sandbox.resetMainLooper() { + val looperClass = robolectricClassLoader.loadClass("android.os.Looper") + + @Suppress("DiscouragedPrivateApi") + val sMainLooperField = looperClass.getDeclaredField("sMainLooper") + sMainLooperField.isAccessible = true + sMainLooperField.set(null, null) + sMainLooperField.isAccessible = false +} + +private fun Sandbox.resetMyLooper() { + val looperClass = robolectricClassLoader.loadClass("android.os.Looper") + + @Suppress("DiscouragedPrivateApi") + val sThreadLocalField = looperClass.getDeclaredField("sThreadLocal") + sThreadLocalField.isAccessible = true + val threadLocal = (sThreadLocalField.get(null) as ThreadLocal<*>) + threadLocal.remove() + sThreadLocalField.isAccessible = false +} diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/validation/TestClassValidator.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/validation/TestClassValidator.kt index 1ea3da1..a03095e 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/validation/TestClassValidator.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/validation/TestClassValidator.kt @@ -4,7 +4,6 @@ import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -import org.robolectric.annotation.LooperMode import tech.apter.junit.jupiter.robolectric.internal.extensions.isJUnit5NestedTest internal object TestClassValidator { @@ -16,7 +15,6 @@ internal object TestClassValidator { validateNestedTestClassCanNotOverrideRuntimeSdk(testClass) validateNestedTestClassCanNotApplyAnnotations( testClass, - LooperMode::class.java, GraphicsMode::class.java, ) } diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionLooperModeSelfTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionLooperModeSelfTest.kt new file mode 100644 index 0000000..0b8106d --- /dev/null +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionLooperModeSelfTest.kt @@ -0,0 +1,84 @@ +package tech.apter.junit.jupiter.robolectric + +import android.os.Looper +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import org.robolectric.annotation.LooperMode +import kotlin.test.Test +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +@ExtendWith(RobolectricExtension::class) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +@Execution(ExecutionMode.SAME_THREAD) +class RobolectricExtensionLooperModeSelfTest { + @Test + @Order(1) + fun `Given a test method with default looper mode then testMethod should be invoked on the main thread`() { + assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Test + @Order(2) + @LooperMode(LooperMode.Mode.PAUSED) + fun `Given a test method with looper mode paused then testMethod should be invoked on the main thread`() { + assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Test + @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) + @Order(3) + fun `Given a test method with looper mode instrumentation test when called after a paused test method then testMethod should be not invoked on the main thread`() { + assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Test + @Order(4) + @LooperMode(LooperMode.Mode.PAUSED) + fun `Given a test method with looper mode paused when called after an instrumentation test method then testMethod should be invoked on the main thread`() { + assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Nested + @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) + @TestMethodOrder(MethodOrderer.OrderAnnotation::class) + @DisplayName("Given a nested test class with looper mode instrumentation test") + inner class NestedInstrumentationTest { + + @Test + @LooperMode(LooperMode.Mode.PAUSED) + @Order(1) + fun `and a test method with looper mode paused then testMethod should be invoked on the main thread`() { + assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Test + @Order(2) + fun `and a test method with looper with default looper mode set after paused test then testMethod should be not invoked on the main thread`() { + assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + } + + @Nested + @LooperMode(LooperMode.Mode.PAUSED) + @TestMethodOrder(MethodOrderer.OrderAnnotation::class) + @DisplayName("Given a nested test class with looper mode paused") + inner class NestedPausedTest { + @Test + fun `and a test method with looper with default looper mode set then testMethod should be not invoked on the main thread`() { + assertSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + + @Test + @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) + fun `and a test method with looper mode instrumentation test when call after a paused test then testMethod should be not invoked on the main thread`() { + assertNotSame(Thread.currentThread(), Looper.getMainLooper()?.thread) + } + } +} diff --git a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt index 3ce5f4d..9809d7c 100644 --- a/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt +++ b/robolectric-extension/src/test/kotlin/tech/apter/junit/jupiter/robolectric/RobolectricExtensionSelfTest.kt @@ -59,11 +59,6 @@ class RobolectricExtensionSelfTest { assertContains(RuntimeEnvironment.getQualifiers(), "en") } - @Test - fun testMethod_shouldBeInvoked_onMainThread() { - assertSame(Thread.currentThread(), Looper.getMainLooper().thread) - } - @Test @Timeout(1000) fun whenTestHarnessUsesDifferentThread_shouldStillReportAsMainThread() { From f5ae744f647dae1874971e83926ff272929e85d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Varga?= Date: Thu, 9 May 2024 15:54:19 +0200 Subject: [PATCH 2/2] chore: Fix detekt findings --- .../jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt index 684ff45..f4a5e34 100644 --- a/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt +++ b/robolectric-extension/src/main/kotlin/tech/apter/junit/jupiter/robolectric/internal/JUnit5RobolectricTestRunner.kt @@ -115,8 +115,6 @@ internal class JUnit5RobolectricTestRunner( } internal companion object { - private val beforeTestLock = Any() - private fun defaultInjectorBuilder() = defaultInjector().bind(SandboxBuilder::class.java, JUnit5RobolectricSandboxBuilder::class.java) .bind(MavenDependencyResolver::class.java, JUnit5MavenDependencyResolver::class.java)