diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c4e7b..6feef1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f089965..28c3c3e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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: @@ -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 diff --git a/gradle.properties b/gradle.properties index 7b8fe24..2945ea5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 @@ -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 diff --git a/mockingbird-compiler/build.gradle.kts b/mockingbird-compiler/build.gradle.kts index 430e4de..fefc49f 100644 --- a/mockingbird-compiler/build.gradle.kts +++ b/mockingbird-compiler/build.gradle.kts @@ -18,6 +18,7 @@ plugins { `kotlin-dsl` + id("com.github.gmazzo.buildconfig") version libs.versions.buildconfig.get() } apply(from = "../publishing.gradle") @@ -37,6 +38,11 @@ repositories { gradlePluginPortal() } +buildConfig{ + buildConfigField("String", "VERSION", "\"${project.property("VERSION") as String}\"") +} + + tasks.withType().configureEach { kotlinOptions { freeCompilerArgs = freeCompilerArgs + listOf( @@ -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) diff --git a/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPlugin.kt b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPlugin.kt index 6f7496e..63115c1 100644 --- a/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPlugin.kt +++ b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPlugin.kt @@ -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 { - 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( - 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, outputDir: File) { - for (kmClass in classNames) { - mockGenerator.createClass(kmClass).writeTo(outputDir) - } + mockingbirdPluginLegacyCodeGenDelegate.apply(target) + mockingbirdPluginKspDelegate.apply(target) } } diff --git a/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginKspDelegate.kt b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginKspDelegate.kt new file mode 100644 index 0000000..9fdf43d --- /dev/null +++ b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginKspDelegate.kt @@ -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 + } +} \ No newline at end of file diff --git a/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginLegacyCodeGenDelegate.kt b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginLegacyCodeGenDelegate.kt new file mode 100644 index 0000000..2021afd --- /dev/null +++ b/mockingbird-compiler/src/main/kotlin/com/careem/mockingbird/MockingbirdPluginLegacyCodeGenDelegate.kt @@ -0,0 +1,121 @@ +package com.careem.mockingbird + +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.toKmClass +import kotlinx.metadata.KmClass +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 + +@OptIn(KotlinPoetMetadataPreview::class) +class MockingbirdPluginLegacyCodeGenDelegate { + 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) + + fun apply(target: Project) { + target.extensions.add( + EXTENSION_NAME, MockingbirdPluginExtensionImpl(target.objects) + ) + val sourceSetResolver = SourceSetResolver() + projectExplorer = ProjectExplorer(sourceSetResolver) + try { + target.afterEvaluate { + if(legacyCodeGenRequired(this)){ + 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 legacyCodeGenRequired(target: Project): Boolean { + val pluginExtensions = target.extensions[EXTENSION_NAME] as MockingbirdPluginExtensionImpl + val legacyCodeGenRequested = pluginExtensions.generateMocksFor.isNotEmpty() + logger.info("LegacyCodeGen: $legacyCodeGenRequested") + return legacyCodeGenRequested + } + + 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, outputDir: File) { + for (kmClass in classNames) { + mockGenerator.createClass(kmClass).writeTo(outputDir) + } + } + + private fun setupDependencies(target: Project) { + classLoader = ClassLoaderWrapper(projectExplorer, target) + functionsMiner = FunctionsMiner(classLoader) + mockGenerator = MockGenerator(classLoader, functionsMiner) + } + + companion object{ + private const val EXTENSION_NAME = "mockingBird" + } +} \ No newline at end of file diff --git a/samples/kspsample/build.gradle.kts b/samples/kspsample/build.gradle.kts index 64ef089..11f4197 100644 --- a/samples/kspsample/build.gradle.kts +++ b/samples/kspsample/build.gradle.kts @@ -16,9 +16,10 @@ */ import groovy.lang.Closure -plugins{ +plugins { id("org.jetbrains.kotlin.multiplatform") id("com.google.devtools.ksp") version libs.versions.kspVersion.get() + id("com.careem.mockingbird") } apply(from = "../../utils.gradle") @@ -42,10 +43,3 @@ kotlin { } } -dependencies { - "kspJvmTest"("com.careem.mockingbird:mockingbird-processor") - "kspIosSimulatorArm64Test"("com.careem.mockingbird:mockingbird-processor") - "kspIosX64Test"("com.careem.mockingbird:mockingbird-processor") - "kspIosArm64Test"("com.careem.mockingbird:mockingbird-processor") -} - diff --git a/versions.toml b/versions.toml index 1dadfba..aa03c43 100644 --- a/versions.toml +++ b/versions.toml @@ -10,6 +10,7 @@ kotlinPoet = "1.11.0" kotlinPoetKsp = "1.10.2" kotlinxMetadata = "0.4.2" mockk = "1.12.3" +buildconfig = "3.1.0" [libraries] jacoco-jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" } @@ -26,8 +27,12 @@ square-kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = square-kotlinpoet-metadata = { module = "com.squareup:kotlinpoet-metadata", version.ref = "kotlinPoet" } square-kotlinpoet-metadata-specs = { module = "com.squareup:kotlinpoet-metadata-specs", version.ref = "kotlinPoet" } kotlinx-metadata-jvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version.ref = "kotlinxMetadata" } + google-ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "kspVersion" } kotlinx-atomicfu-gradle = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicFu" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -mockk-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } \ No newline at end of file +mockk-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } + +[plugins] +gmazzo-buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } \ No newline at end of file