Skip to content

Commit

Permalink
🧪 Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
heytherewill committed Oct 30, 2023
1 parent fa8f3e3 commit c6e8bc1
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.toggl.komposable.processors

import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.toggl.komposable.FileGenerationResult
import com.toggl.komposable.architecture.ChildStates
import com.toggl.komposable.getSymbolsAnnotatedWith
import com.toggl.komposable.toCamelCase

@OptIn(KspExperimental::class)
class StateMappingSymbolProcessor(
codeGenerator: CodeGenerator,
logger: KSPLogger,
) : KotlinPoetSymbolProcessor(codeGenerator, logger) {
override fun generateFiles(resolver: Resolver): Sequence<FileGenerationResult> =
resolver
.getSymbolsAnnotatedWith(ChildStates::class)
.map { parentState ->
val parentStateClassName = parentState.toClassName()
val parentStateTypeName = parentStateClassName.simpleName

val parentStateArgumentName = parentStateTypeName.toCamelCase()

(
(
parentState
.annotations
.toList()
.single()
.arguments
.single()
.value as ArrayList<*>
)
.single() as KSType
)
.declaration
.getAnnotationsByType(ChildStates::class)
.single()
.childStates
.fold(parentState.stateMappingFileBuilder()) { fileBuilder, childState ->
val childStateTypeName = childState.simpleName.orEmpty()
val childStateArgumentName = childStateTypeName.toCamelCase()

fileBuilder
.addFunction(
FunSpec
.builder("map${parentStateTypeName}To$childStateTypeName")
.addParameter(parentStateArgumentName, parentState.toClassName())
.returns(childState)
.addCode("return $childStateTypeName(${buildChildStateConstructorParameterList()})")
.build(),
)
.addFunction(
FunSpec
.builder("map${childStateTypeName}To$parentStateTypeName")
.addParameter(parentStateArgumentName, parentStateClassName)
.addParameter(childStateArgumentName, childState)
.returns(parentStateClassName)
.addCode("return $parentStateArgumentName.copy(${buildParentStateConstructorParameterList()})")
.build(),
)

fileBuilder
}.build().run(FileGenerationResult::Success)
}

@Suppress("Unused")
private fun buildChildStateConstructorParameterList(
// childState: KClass<*>,
// parentState: KSClassDeclaration
): String = ""

@Suppress("Unused")
private fun buildParentStateConstructorParameterList(
// childState: KClass<*>,
// parentState: KSClassDeclaration
): String = ""

private fun KSClassDeclaration.stateMappingFileBuilder() =
FileSpec.builder(
packageName = packageName.getQualifier(),
fileName = "${simpleName}StateMappings",
).addImport(toClassName())

// mapToLocalState = { appState -> ListState(appState.todoList, appState.backStack) },
// mapToGlobalState = { appState, listState -> appState.copy(todoList = listState.todoList, backStack = listState.backStack) },
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import com.toggl.komposable.processors.ActionMappingSymbolProcessorProvider
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.symbolProcessorProviders
import io.kotest.matchers.shouldBe
import kotlin.test.Test

class ActionMappingSymbolProcessorTests {
@Test
fun `Action mapping methods are generated`() {
// Arrange
val compilation = KotlinCompilation().apply {
sources = listOf(SourceFiles.appAction, SourceFiles.settingsAction)
symbolProcessorProviders = listOf(ActionMappingSymbolProcessorProvider())
inheritClassPath = true
messageOutputStream = System.out
}

// Act
val result = compilation.compile()

// Assert
result.exitCode shouldBe KotlinCompilation.ExitCode.OK

val sources = result.kspGeneratedSources()
sources.size.shouldBe(1)
sources.single().readText().contentEquals(SourceFiles.generatedSettingsFile)
}

@Test
fun `If a class annotated with @WrapperAction has multiple properties then the compilation fails`() {
// Arrange
val compilation = KotlinCompilation().apply {
sources = listOf(SourceFiles.appActionWithMultipleProperties, SourceFiles.settingsAction)
symbolProcessorProviders = listOf(ActionMappingSymbolProcessorProvider())
inheritClassPath = true
messageOutputStream = System.out
}

// Act
val result = compilation.compile()

// Assert
result.exitCode shouldBe KotlinCompilation.ExitCode.COMPILATION_ERROR

result.messages.contains("AppAction.Settings needs to have exactly one property to be annotated with @WrapperAction")

val sources = result.kspGeneratedSources()
sources.size.shouldBe(0)
}
}
13 changes: 13 additions & 0 deletions komposable-architecture-compiler/src/test/kotlin/Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import com.tschuchort.compiletesting.KotlinCompilation
import java.io.File

private val KotlinCompilation.Result.workingDir: File get() = outputDirectory.parentFile!!

// HACK: Workaround for finding KSP files. See:
// https://github.com/tschuchortdev/kotlin-compile-testing/issues/129
fun KotlinCompilation.Result.kspGeneratedSources(): List<File> {
val kspWorkingDir = workingDir.resolve("ksp")
val kspGeneratedDir = kspWorkingDir.resolve("sources")
val kotlinGeneratedDir = kspGeneratedDir.resolve("kotlin")
return kotlinGeneratedDir.walkTopDown().filter { it.isFile }.toList()
}
52 changes: 52 additions & 0 deletions komposable-architecture-compiler/src/test/kotlin/SourceFiles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import com.tschuchort.compiletesting.SourceFile
import org.intellij.lang.annotations.Language

object SourceFiles {

val settingsAction = SourceFile.kotlin(
"SettingsAction.kt",
"""
sealed interface SettingsAction {
data class ChangeSomeSetting(val newValue: Boolean) : SettingsAction
}
""".trimIndent(),
)

val appAction = SourceFile.kotlin(
"AppAction.kt",
"""
import com.toggl.komposable.architecture.WrapperAction
sealed interface AppAction {
data object ClearList : AppAction
@WrapperAction
data class Settings(val settingsAction: SettingsAction) : AppAction
}
""".trimIndent(),
)

val appActionWithMultipleProperties = SourceFile.kotlin(
"AppAction.kt",
"""
import com.toggl.komposable.architecture.WrapperAction
sealed interface AppAction {
data object ClearList : AppAction
@WrapperAction
data class Settings(val settingsAction: SettingsAction, val foo: String) : AppAction
}
""".trimIndent(),
)

@Language("kotlin")
val generatedSettingsFile = """import AppAction.Settings
public fun mapAppActionToSettingsAction(appAction: AppAction): SettingsAction? = if(appAction is
AppAction.Settings) appAction.settingsAction else null
public fun mapSettingsActionToAppAction(settingsAction: SettingsAction): AppAction.Settings =
AppAction.Settings(settingsAction)
""".trimIndent()
}

0 comments on commit c6e8bc1

Please sign in to comment.