From b925aff0d5e9557c4625e607315e13ceecc21fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20=C3=85man?= Date: Fri, 30 Aug 2024 13:52:27 +0200 Subject: [PATCH] Kotlin 2.0.20 support (#27) (#28) --- gradle/libs.versions.toml | 10 ++--- .../integrationtests/android/TestFragment.kt | 14 +++--- .../integrationtests/android/TestPresenter.kt | 4 +- .../integrationtests/android/TestView.kt | 22 ++++++---- .../android/MoleculeStylePresenterTest.kt | 43 +++++++++++-------- mockposable/gradle.properties | 2 +- .../compiler/MockKIrGenerationExtension.kt | 18 ++++++-- .../compiler/MockitoIrGenerationExtension.kt | 17 ++++++-- .../mockposable/compiler/MockposablePlugin.kt | 4 +- 9 files changed, 81 insertions(+), 53 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d10c3c..14c3fa9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -kotlin = '2.0.0' +kotlin = '2.0.20' compose-ui = '1.6.8' -google-ksp = '2.0.0-1.0.22' +google-ksp = '2.0.20-1.0.24' activity = '1.9.0' agp = '8.5.0' espresso = '3.6.1' -mockk = '1.13.2' +mockk = '1.13.12' mockito = '4.8.1' [libraries] @@ -23,7 +23,7 @@ androidx-fragment-testing = { module = 'androidx.fragment:fragment-testing', ver compose-compiler = { module = 'org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable', version.ref = 'kotlin' } compose-compiler-gradle = { module = 'org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin', version.ref = 'kotlin' } -compose-runtime = { module = 'org.jetbrains.compose.runtime:runtime', version = '1.6.10' } +compose-runtime = { module = 'org.jetbrains.compose.runtime:runtime', version = '1.6.11' } compose-ui = { module = 'androidx.compose.ui:ui', version.ref = 'compose-ui' } compose-material = { module = 'androidx.compose.material:material', version = '1.6.8' } compose-foundation = { module = 'androidx.compose.foundation:foundation', version = '1.6.8' } @@ -42,7 +42,7 @@ compile-testing = { module = 'dev.zacsweers.kctfork:core', version = '0.5.1' } junit = { module = 'androidx.test.ext:junit', version = '1.1.3' } -robolectric = { module = 'org.robolectric:robolectric', version = '4.12.2' } +robolectric = { module = 'org.robolectric:robolectric', version = '4.13' } molecule = { module = 'app.cash.molecule:molecule-runtime', version = '2.0.0' } diff --git a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestFragment.kt b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestFragment.kt index 7e80432..81f0c0b 100644 --- a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestFragment.kt +++ b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestFragment.kt @@ -11,14 +11,15 @@ import android.widget.TextView import androidx.compose.runtime.Composable import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope -import app.cash.molecule.RecompositionMode -import app.cash.molecule.launchMolecule +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch class TestFragment : Fragment() { - private val events = MutableSharedFlow(replay = 1) - lateinit var present: @Composable (MutableSharedFlow) -> TestModel + lateinit var events: MutableSharedFlow + lateinit var composableLauncher: CoroutineScope.(@Composable () -> TestModel) -> Flow + lateinit var presenter: TestPresenter private val content: LinearLayout by lazy { LinearLayout(requireContext()).apply { @@ -38,10 +39,7 @@ class TestFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val models = lifecycleScope.launchMolecule(RecompositionMode.Immediate) { - present(events) - } - + val models = lifecycleScope.composableLauncher { presenter.present(events) } lifecycleScope.launch { models.collect(::render) } } diff --git a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestPresenter.kt b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestPresenter.kt index fa97a11..2582953 100644 --- a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestPresenter.kt +++ b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestPresenter.kt @@ -5,11 +5,11 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.Flow sealed interface TestEvent { - object Reload : TestEvent + data object Reload : TestEvent } sealed interface TestModel { - object Loading : TestModel + data object Loading : TestModel class Error(val message: String) : TestModel class Data(val data: Int) : TestModel } diff --git a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestView.kt b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestView.kt index 7d2b8fb..e993a93 100644 --- a/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestView.kt +++ b/integration-tests/android/src/main/kotlin/com/jeppeman/mockposable/integrationtests/android/TestView.kt @@ -4,6 +4,8 @@ import androidx.compose.material.Button import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.RecomposeScope +import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -13,16 +15,20 @@ import kotlinx.coroutines.flow.MutableSharedFlow @Composable fun ComposeTestView( presenter: TestPresenter = viewModel(), - events: MutableSharedFlow = remember { MutableSharedFlow(replay = 1) } -) = when (val model = presenter.present(events)) { - TestModel.Loading -> CircularProgressIndicator(modifier = Modifier.testTag("progress")) - is TestModel.Error -> { - Text(text = model.message) - Button(onClick = { events.tryEmit(TestEvent.Reload) }) { - Text(text = "Try again") + events: MutableSharedFlow = remember { MutableSharedFlow(replay = 1) }, + scopeCaptor: (RecomposeScope) -> Unit, +) { + scopeCaptor(currentRecomposeScope) + when (val model = presenter.present(events)) { + TestModel.Loading -> CircularProgressIndicator(modifier = Modifier.testTag("progress")) + is TestModel.Error -> { + Text(text = model.message) + Button(onClick = { events.tryEmit(TestEvent.Reload) }) { + Text(text = "Try again") + } } + is TestModel.Data -> Text(text = model.data.toString()) } - is TestModel.Data -> Text(text = model.data.toString()) } @Composable diff --git a/integration-tests/android/src/test/kotlin/com/jeppeman/mockposable/integrationtests/android/MoleculeStylePresenterTest.kt b/integration-tests/android/src/test/kotlin/com/jeppeman/mockposable/integrationtests/android/MoleculeStylePresenterTest.kt index 1a2ede6..a3a64c7 100644 --- a/integration-tests/android/src/test/kotlin/com/jeppeman/mockposable/integrationtests/android/MoleculeStylePresenterTest.kt +++ b/integration-tests/android/src/test/kotlin/com/jeppeman/mockposable/integrationtests/android/MoleculeStylePresenterTest.kt @@ -1,6 +1,7 @@ package com.jeppeman.mockposable.integrationtests.android import android.os.Build +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.RecomposeScope import androidx.compose.runtime.currentRecomposeScope import androidx.compose.ui.test.assertIsDisplayed @@ -17,10 +18,15 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.molecule.RecompositionMode +import app.cash.molecule.launchMolecule import com.jeppeman.mockposable.mockk.everyComposable import io.mockk.MockKMatcherScope import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect import org.hamcrest.CoreMatchers.equalTo import org.junit.Rule import org.junit.Test @@ -49,16 +55,12 @@ class ComposableMoleculePresenterTest { val fakeErrorModel = TestModel.Error("Failed") val fakeDataModel = TestModel.Data(2) val mockPresenter = mockk { - everyComposable { present(matchFirst { it == null }) } returnsMany listOf( - TestModel.Loading, - fakeErrorModel - ) + everyComposable { present(matchFirst { it == null }) } returns TestModel.Loading andThen fakeErrorModel everyComposable { present(matchFirst { it == TestEvent.Reload }) } returns fakeDataModel } composeTestRule.setContent { - recomposeScope = currentRecomposeScope - ComposeTestView(mockPresenter, events) + ComposeTestView(mockPresenter, events) { recomposeScope = it } } // First progress is displayed @@ -74,7 +76,10 @@ class ComposableMoleculePresenterTest { } -@Config(sdk = [Build.VERSION_CODES.TIRAMISU], instrumentedPackages = ["androidx.loader.content"]) +@Config( + sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE], + instrumentedPackages = ["androidx.loader.content"] +) @RunWith(AndroidJUnit4::class) class FragmentMoleculePresenterTest { private lateinit var recomposeScope: RecomposeScope @@ -84,18 +89,18 @@ class FragmentMoleculePresenterTest { factory = object : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return TestFragment().apply { - present = { events -> - val proxyEvents = object : ProxyMutableSharedFlow(events) { - override fun tryEmit( - value: TestEvent - ): Boolean = super.tryEmit(value).apply { - recomposeScope.invalidate() - } + events = object : ProxyMutableSharedFlow() { + override fun tryEmit( + value: TestEvent + ): Boolean = super.tryEmit(value).apply { recomposeScope.invalidate() } + } + composableLauncher = { composable -> + launchMolecule(RecompositionMode.Immediate) { + recomposeScope = currentRecomposeScope + composable() } - - recomposeScope = currentRecomposeScope - mockPresenter.present(events = proxyEvents) } + presenter = mockPresenter } } } @@ -121,6 +126,6 @@ class FragmentMoleculePresenterTest { // Press "Try again" and reload onView(withText(equalTo("Try again"))).perform(click()) // Then the actual data is displayed - onView(withText(fakeData.data.toString())) + onView(withText(fakeData.data.toString())).check(matches(isDisplayed())) } -} \ No newline at end of file +} diff --git a/mockposable/gradle.properties b/mockposable/gradle.properties index 0ce6007..9bb65b6 100644 --- a/mockposable/gradle.properties +++ b/mockposable/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.10-SNAPSHOT +VERSION_NAME=0.11-SNAPSHOT GROUP=com.jeppeman.mockposable POM_DESCRIPTION=A tool that enables stubbing and verification of @Composable-annotated functions diff --git a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockKIrGenerationExtension.kt b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockKIrGenerationExtension.kt index d1c7952..eae5ad9 100644 --- a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockKIrGenerationExtension.kt +++ b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockKIrGenerationExtension.kt @@ -1,9 +1,12 @@ package com.jeppeman.mockposable.compiler import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext -import org.jetbrains.kotlin.backend.common.checkDeclarationParents import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.validateIr +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.toLogger +import org.jetbrains.kotlin.config.IrVerificationMode import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.declarations.IrModuleFragment @@ -21,7 +24,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.util.Logger /** - * This extension has the responsibility performing the following transformations: + * This extension has the responsibility of performing the following transformations: * * 1. everyComposable { f(args, $composer, $changed) } -> everyComposable { f(args, any(), any() } * 2. verifyComposable { f(args, $composer, $changed) } -> verifyComposable { f(args, any(), any() } @@ -31,7 +34,8 @@ import org.jetbrains.kotlin.util.Logger * verify these calls with Mockk. */ class MockKIrGenerationExtension( - private val logger: Logger + private val messageCollector: MessageCollector, + private val logger: Logger = messageCollector.toLogger(), ) : IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { logger.log("Running MockK composable transformations") @@ -40,7 +44,13 @@ class MockKIrGenerationExtension( VerifyComposableElementTransformer(logger, pluginContext) ) transformers.forEach { transformer -> moduleFragment.transform(transformer, null) } - moduleFragment.checkDeclarationParents() + validateIr(messageCollector, IrVerificationMode.ERROR) { + performBasicIrValidation( + moduleFragment, + moduleFragment.irBuiltins, + "MockK transformation" + ) + } } } diff --git a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockitoIrGenerationExtension.kt b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockitoIrGenerationExtension.kt index a34907e..f5704da 100644 --- a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockitoIrGenerationExtension.kt +++ b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockitoIrGenerationExtension.kt @@ -1,10 +1,12 @@ package com.jeppeman.mockposable.compiler import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext -import org.jetbrains.kotlin.backend.common.checkDeclarationParents -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.validateIr +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.toLogger +import org.jetbrains.kotlin.config.IrVerificationMode import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.ir.expressions.IrCall @@ -26,7 +28,8 @@ import org.jetbrains.kotlin.util.Logger * verify these calls with Mockito. */ class MockitoIrGenerationExtension( - private val logger: Logger + private val messageCollector: MessageCollector, + private val logger: Logger = messageCollector.toLogger(), ) : IrGenerationExtension { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { logger.log("Running Mockito composable transformations") @@ -35,7 +38,13 @@ class MockitoIrGenerationExtension( MockitoVerifyComposableElementTransformer(logger, pluginContext) ) transformers.forEach { transformer -> moduleFragment.transform(transformer, null) } - moduleFragment.checkDeclarationParents() + validateIr(messageCollector, IrVerificationMode.ERROR) { + performBasicIrValidation( + moduleFragment, + moduleFragment.irBuiltins, + "Mockito transformation" + ) + } } } diff --git a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockposablePlugin.kt b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockposablePlugin.kt index cf3ff5e..60fc222 100644 --- a/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockposablePlugin.kt +++ b/mockposable/mockposable-compiler/src/main/java/com/jeppeman/mockposable/compiler/MockposablePlugin.kt @@ -38,8 +38,8 @@ class MockposablePlugin : ComponentRegistrar { project.registerIrLast( when (extension) { - "mockito" -> MockitoIrGenerationExtension(messageCollector.toLogger()) - "mockk" -> MockKIrGenerationExtension(messageCollector.toLogger()) + "mockito" -> MockitoIrGenerationExtension(messageCollector) + "mockk" -> MockKIrGenerationExtension(messageCollector) else -> throw IllegalArgumentException("Unsupported plugin extension: $extension") } )