Skip to content

Commit

Permalink
Add Ksp mocking to MockingBird plugin (#125)
Browse files Browse the repository at this point in the history
Add Ksp mocking to MockingBird plugin

This patch is to let the mockingbird plugin take care of the configurations and workaround the current ksp limitations that are present for commonTest code generation.
  • Loading branch information
MarcoSignoretto authored Sep 27, 2022
1 parent b951415 commit 50dbe57
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 124 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
---

## master
* MockingBird plugin support ksp codegen out of the box of google ksp plugin is applied
* Introduced Ksp code generation using the @Mock annotation
* Fix for mock generation to set the right visibility

Expand Down
44 changes: 35 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ libraries like `Mockito` or `Mockk`
The mock generation plugin generates mock boilerplate code for you, the plugin can be used along with manual mocks, it
is currently experimental and has several limitations.

To use this plugin you have to use mockingbird version `2.0.0-beta06` or above, to see examples you can explore
To use this plugin you have to use mockingbird version `2.7.0` or above, to see examples you can explore
the `samples` project, You can open `samples` is a standalone project.

NOTE: the plugin doesn't discover which interfaces to mock, it's up to you to configure those.

WARNING: If you do not what to use the plugin you can use the old way of manual mock generation,
check [Mocks](https://github.com/careem/mockingbird#mocks) or [Spies](https://github.com/careem/mockingbird#spies)

Expand Down Expand Up @@ -97,6 +95,39 @@ Groovy DSL:
apply plugin: "com.careem.mockingbird"
```

##### Ksp code generator
Examples are under `:samples:kspsample`

The plugin has a new version that allow you to use google ksp to generate the mocks annotating the filed in your test classes.

To enable the ksp based code gen it is enough for you to add the google ksp plugin to your module where the plugin is applied for example
your plugin block could look similar to

```kotlin
plugins {
id("com.google.devtools.ksp") version "1.6.21-1.0.6"
id("com.careem.mockingbird")
}
```

To create a mock you just need to annotate the field in your test class example:

```kotlin
class KspSampleTest {
@Mock
val pippoMock: PippoSample = PippoSampleMock()

...
}
```

Where `PippoSample` is the interface you want to mock and `PippoSampleMock` is the mock that will be generated by ksp.

##### Legacy code generator ( deprecated )
Examples are under `:samples:sample`

NOTE: the plugin doesn't discover which interfaces to mock, it's up to you to configure those.

And then specify for what interfaces you what to generate mocks for, see the example below

Kotlin DSL:
Expand All @@ -123,19 +154,14 @@ mockingBird {
}
```

#### Plugin Usage

To generate mocks you can simply run `./gradlew generateMocks` or simply run `./gradlew build`, for faster development
loop using `generatedMocks` is recommended

#### Plugin Limitations

* The plugin can only be used in modules containing a `jvm` target
* The plugin can only be used in modules containing a `jvm` target ( this limitation applies only to the legacy code generator )
* The plugin can generate mocks only, no support for spies yet
* Only interfaces can be mocked
* Only interfaces that have generic types in their definitions can be mocked
* Only interfaces without `lambdas` can be mocked
* Only interfaces without `suspend` functions can be mocked
* Only interfaces without `inline` functions can be mocked
* Only interfaces without `reified` functions can be mocked

Expand Down
4 changes: 1 addition & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

GROUP=com.careem.mockingbird
VERSION=2.7.0-SNAPSHOT
POM_NAME=mockingbird
Expand All @@ -25,7 +24,6 @@ POM_SCM_URL=https://github.com/careem/mockingbird
POM_DEVELOPER_ORG=Careem Inc
POM_DEVELOPER_URL=https://careem.com
# Kotlin
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
kotlin.mpp.stability.nowarn=true
kotlin.js.compiler=both
Expand Down
7 changes: 7 additions & 0 deletions mockingbird-compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

plugins {
`kotlin-dsl`
id("com.github.gmazzo.buildconfig") version libs.versions.buildconfig.get()
}

apply(from = "../publishing.gradle")
Expand All @@ -37,6 +38,11 @@ repositories {
gradlePluginPortal()
}

buildConfig{
buildConfigField("String", "VERSION", "\"${project.property("VERSION") as String}\"")
}


tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf(
Expand All @@ -59,6 +65,7 @@ dependencies {
implementation(libs.square.kotlinpoet)
implementation(libs.square.kotlinpoet.metadata)
implementation(libs.kotlinx.metadata.jvm)
implementation(project(":mockingbird-processor"))
implementation(project(":mockingbird"))

testImplementation(libs.kotlin.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,116 +15,17 @@
*/
package com.careem.mockingbird

import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
import com.squareup.kotlinpoet.metadata.toKmClass
import kotlinx.metadata.KmClass
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.kotlin.dsl.add
import org.gradle.kotlin.dsl.get
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import java.io.File

private const val EXTENSION_NAME = "mockingBird"


@Suppress("UnstableApiUsage")
@KotlinPoetMetadataPreview
abstract class MockingbirdPlugin : Plugin<Project> {

private lateinit var classLoader: ClassLoaderWrapper
private lateinit var functionsMiner: FunctionsMiner
private lateinit var projectExplorer: ProjectExplorer
private lateinit var mockGenerator: MockGenerator
private val logger: Logger = Logging.getLogger(this::class.java)

private fun setupDependencies(target: Project) {
classLoader = ClassLoaderWrapper(projectExplorer, target)
functionsMiner = FunctionsMiner(classLoader)
mockGenerator = MockGenerator(classLoader, functionsMiner)
}
private val mockingbirdPluginLegacyCodeGenDelegate = MockingbirdPluginLegacyCodeGenDelegate()
private val mockingbirdPluginKspDelegate = MockingbirdPluginKspDelegate()

override fun apply(target: Project) {
val sourceSetResolver = SourceSetResolver()
projectExplorer = ProjectExplorer(sourceSetResolver)
try {
target.extensions.add<MockingbirdPluginExtension>(
EXTENSION_NAME, MockingbirdPluginExtensionImpl(target.objects)
)

target.gradle.projectsEvaluated {
val generateMocksTask = target.task(GradleTasks.GENERATE_MOCKS) {
dependsOn(target.tasks.getByName(GradleTasks.JVM_JAR))
doFirst {
val outputDir = targetOutputDir(target)
outputDir.deleteRecursively()
}
doLast {
generateMocks(target)
}
}

target.tasks.forEach { task ->
if (task.name.contains("Test") && (task is KotlinCompile<*>)) {
task.dependsOn(generateMocksTask)
}
}

configureSourceSets(target)

projectExplorer.visitRootProject(target.rootProject)
// Add test dependencies for classes that need to be mocked
val dependencySet = projectExplorer.explore(target)
target.extensions.getByType(KotlinMultiplatformExtension::class.java).run {
sourceSets.getByName("commonTest") {
dependencies {
dependencySet.forEach { implementation(it) }
}
}
}
}

} catch (e: Exception) {
// Useful to debug
e.printStackTrace()
throw e
}
}

private fun generateMocks(target: Project) {
setupDependencies(target)

val pluginExtensions = target.extensions[EXTENSION_NAME] as MockingbirdPluginExtensionImpl
logger.info("Mocking: ${pluginExtensions.generateMocksFor}")
val outputDir = targetOutputDir(target)
outputDir.mkdirs()

pluginExtensions.generateMocksFor
.map { classLoader.loadClass(it).toKmClass() }
.let { generateClasses(it, outputDir) }
}

private fun configureSourceSets(target: Project) {
// TODO check if kmpProject before this
target.extensions.configure(KotlinMultiplatformExtension::class.java) {
sourceSets.getByName("commonTest") {
kotlin.srcDir("build/generated/mockingbird")
}
}
}

private fun targetOutputDir(target: Project): File {
return File(target.buildDir.absolutePath + File.separator + "generated" + File.separator + "mockingbird")
}


private fun generateClasses(classNames: List<KmClass>, outputDir: File) {
for (kmClass in classNames) {
mockGenerator.createClass(kmClass).writeTo(outputDir)
}
mockingbirdPluginLegacyCodeGenDelegate.apply(target)
mockingbirdPluginKspDelegate.apply(target)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.careem.mockingbird

import com.careem.mockingbird.mockingbird_compiler.BuildConfig
import org.gradle.api.Project
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.get
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MockingbirdPluginKspDelegate {
fun apply(target: Project) {
target.afterEvaluate {
if (hasKspPlugin(this)) {
// TODO revisit this whole logic once Ksp will be able to generate code for commonTest ( not the case today see: https://github.com/google/ksp/issues/567 )
// The following code will workaround the current ksp limitations doing the follow:
// 1. To avoid running ksp for each target the plugin will run ksp for a single target jvm will be prefered if the target is available otherwise it will pick the first target
// 2. Since current multiplatform ksp implementation can target specific targets, mocks generated will not be resolved
// in commonTest. The plugin will add this the code generated at point 1 as source set for common test so that
// this code will be available for each platform and resolvable by the IDE
target.extensions.configure(KotlinMultiplatformExtension::class.java) {
val firstTargetName = targets.filter { it.targetName != "metadata" }.first().targetName
val selectedTargetName =
targets.filter { it.targetName == "jvm" }.firstOrNull()?.targetName ?: firstTargetName
sourceSets.getByName("commonTest") {
kotlin.srcDir("build/generated/ksp/$selectedTargetName/${selectedTargetName}Test/kotlin")
}
target.dependencies {
"ksp${selectedTargetName.capitalized()}Test"("com.careem.mockingbird:mockingbird-processor:${BuildConfig.VERSION}")
}
tasks.forEach { task ->
if (task.name.contains("Test") && (task is KotlinCompile<*>)) {
task.dependsOn("kspTestKotlin${selectedTargetName.capitalized()}")
}
}
}
}
}
}

private fun hasKspPlugin(target: Project): Boolean {
return target.plugins.findPlugin("com.google.devtools.ksp") != null
}
}
Loading

0 comments on commit 50dbe57

Please sign in to comment.