Skip to content

Commit

Permalink
Kotlin 2.0.20 support (#27) (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeppeman authored Aug 30, 2024
1 parent 7674afd commit b925aff
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 53 deletions.
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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' }
Expand All @@ -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' }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestEvent>(replay = 1)
lateinit var present: @Composable (MutableSharedFlow<TestEvent>) -> TestModel
lateinit var events: MutableSharedFlow<TestEvent>
lateinit var composableLauncher: CoroutineScope.(@Composable () -> TestModel) -> Flow<TestModel>
lateinit var presenter: TestPresenter

private val content: LinearLayout by lazy {
LinearLayout(requireContext()).apply {
Expand All @@ -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) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,16 +15,20 @@ import kotlinx.coroutines.flow.MutableSharedFlow
@Composable
fun ComposeTestView(
presenter: TestPresenter = viewModel(),
events: MutableSharedFlow<TestEvent> = 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<TestEvent> = 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -49,16 +55,12 @@ class ComposableMoleculePresenterTest {
val fakeErrorModel = TestModel.Error("Failed")
val fakeDataModel = TestModel.Data(2)
val mockPresenter = mockk<TestPresenter> {
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
Expand All @@ -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
Expand All @@ -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<TestEvent>(events) {
override fun tryEmit(
value: TestEvent
): Boolean = super.tryEmit(value).apply {
recomposeScope.invalidate()
}
events = object : ProxyMutableSharedFlow<TestEvent>() {
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
}
}
}
Expand All @@ -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()))
}
}
}
2 changes: 1 addition & 1 deletion mockposable/gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Composer?>(), any<Int>() }
* 2. verifyComposable { f(args, $composer, $changed) } -> verifyComposable { f(args, any<Composer?>(), any<Int>() }
Expand All @@ -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")
Expand All @@ -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"
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -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"
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
)
Expand Down

0 comments on commit b925aff

Please sign in to comment.