Skip to content

Commit

Permalink
Generate testing APIs (cashapp#787)
Browse files Browse the repository at this point in the history
* Generate testing APIs

* Code review feedback

* Generate testing facets for the test-schema
  • Loading branch information
squarejesse authored Feb 3, 2023
1 parent d08de06 commit b29b64c
Show file tree
Hide file tree
Showing 22 changed files with 885 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
"--compose" to CodegenType.Compose,
"--compose-protocol" to ProtocolCodegenType.Compose,
"--layout-modifiers" to CodegenType.LayoutModifiers,
"--testing" to CodegenType.Testing,
"--widget" to CodegenType.Widget,
"--widget-protocol" to ProtocolCodegenType.Widget,
)
Expand Down
6 changes: 5 additions & 1 deletion redwood-compose-testing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ kotlin {
commonMain {
dependencies {
api libs.jetbrains.compose.runtime
api libs.kotlin.test
api libs.kotlinx.coroutines.core
api projects.redwoodCompose
api projects.redwoodRuntime
}
}
commonTest {
dependencies {
implementation libs.kotlin.test
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,66 @@
*/
package app.cash.redwood.compose.testing

import app.cash.redwood.LayoutModifier

/**
* A widget that's implemented as a value class, appropriate for use in tests.
*
* Implementations of this interface may have lambda properties that trigger application behavior.
* These lambda properties are **excluded** from [Any.equals], [Any.hashCode], and [Any.toString].
*/
public interface WidgetValue
public interface WidgetValue {
public val layoutModifiers: LayoutModifier

/** Returns all of the direct children of this widget, grouped by slot. */
public val childrenLists: List<List<WidgetValue>>
get() = listOf()
}

/**
* Returns a sequence that does a depth-first preorder traversal of the entire widget tree whose
* roots are this list. This is the same order elements occur in code.
*
* For example, given the following structure:
*
* ```
* Column {
* Toolbar {
* Icon(...)
* Title(...)
* }
* Row {
* Text(...)
* Button(...)
* }
* }
* ```
*
* The flattened elements are returned in this order:
*
* ```
* Column,
* Toolbar,
* Icon,
* Title,
* Row,
* Text,
* Button
* ```
*/
public fun List<WidgetValue>.flatten(): Sequence<WidgetValue> {
return sequence {
for (widget in this@flatten) {
flattenRecursive(widget)
}
}
}

private suspend fun SequenceScope<WidgetValue>.flattenRecursive(widgetValue: WidgetValue) {
yield(widgetValue)
for (children in widgetValue.childrenLists) {
for (child in children) {
flattenRecursive(child)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.compose.testing

import app.cash.redwood.LayoutModifier
import kotlin.test.Test
import kotlin.test.assertEquals

class WidgetValueTest {
@Test
fun flattenNoHierarchy() {
val a = SimpleWidgetValue()
val b = SimpleWidgetValue()
val c = SimpleWidgetValue()

assertEquals(
listOf(),
listOf<WidgetValue>().flatten().toList(),
)
assertEquals(
listOf(a),
listOf<WidgetValue>(a).flatten().toList(),
)
assertEquals(
listOf(a, b, c),
listOf<WidgetValue>(a, b, c).flatten().toList(),
)
}

@Test
fun flattenParentsFirst() {
val a = SimpleWidgetValue()
val aa = SimpleWidgetValue(childrenLists = listOf(listOf(a)))
val aaa = SimpleWidgetValue(childrenLists = listOf(listOf(aa)))

assertEquals(
listOf(aa, a),
listOf<WidgetValue>(aa).flatten().toList(),
)
assertEquals(
listOf(aaa, aa, a),
listOf<WidgetValue>(aaa).flatten().toList(),
)
}

@Test
fun flattenSiblingSubtreeFirst() {
val a = SimpleWidgetValue()
val aa = SimpleWidgetValue(childrenLists = listOf(listOf(a)))
val aaa = SimpleWidgetValue(childrenLists = listOf(listOf(aa)))
val b = SimpleWidgetValue()

assertEquals(
listOf(aa, a, b),
listOf<WidgetValue>(aa, b).flatten().toList(),
)
assertEquals(
listOf(aaa, aa, a, b),
listOf<WidgetValue>(aaa, b).flatten().toList(),
)
}

class SimpleWidgetValue(
override val layoutModifiers: LayoutModifier = LayoutModifier,
override val childrenLists: List<List<WidgetValue>> = listOf(),
) : WidgetValue
}
6 changes: 6 additions & 0 deletions redwood-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ gradlePlugin {
description = "Redwood schema layout modifier code generation Gradle plugin"
implementationClass = "app.cash.redwood.gradle.RedwoodLayoutModifiersGeneratorPlugin"
}
redwoodTestingGenerator {
id = "app.cash.redwood.generator.testing"
displayName = "Redwood generator (testing)"
description = "Redwood schema testing code generation Gradle plugin"
implementationClass = "app.cash.redwood.gradle.RedwoodTestingGeneratorPlugin"
}
redwoodWidgetGenerator {
id = "app.cash.redwood.generator.widget"
displayName = "Redwood generator (widget)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package app.cash.redwood.gradle
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.Compose
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.ComposeProtocol
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.LayoutModifiers
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.Testing
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.Widget
import app.cash.redwood.gradle.RedwoodGeneratorPlugin.Strategy.WidgetProtocol
import org.gradle.api.Plugin
Expand All @@ -35,6 +36,9 @@ public class RedwoodComposeProtocolGeneratorPlugin : RedwoodGeneratorPlugin(Comp
@Suppress("unused") // Invoked reflectively by Gradle.
public class RedwoodLayoutModifiersGeneratorPlugin : RedwoodGeneratorPlugin(LayoutModifiers)

@Suppress("unused") // Invoked reflectively by Gradle.
public class RedwoodTestingGeneratorPlugin : RedwoodGeneratorPlugin(Testing)

@Suppress("unused") // Invoked reflectively by Gradle.
public class RedwoodWidgetGeneratorPlugin : RedwoodGeneratorPlugin(Widget)

Expand All @@ -51,6 +55,7 @@ public abstract class RedwoodGeneratorPlugin(
Compose("--compose", "redwood-compose"),
ComposeProtocol("--compose-protocol", "redwood-protocol-compose"),
LayoutModifiers("--layout-modifiers", "redwood-runtime"),
Testing("--testing", "redwood-compose-testing"),
Widget("--widget", "redwood-widget"),
WidgetProtocol("--widget-protocol", "redwood-protocol-widget"),
}
Expand Down
22 changes: 22 additions & 0 deletions redwood-layout-testing/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'app.cash.redwood.generator.testing'
apply plugin: 'com.vanniktech.maven.publish'
apply plugin: 'org.jetbrains.dokka' // Must be applied here for publish plugin.

kotlin {
apply from: "${rootDir}/addAllTargets.gradle"

sourceSets {
commonMain {
dependencies {
api projects.redwoodLayoutLayoutmodifiers
api projects.redwoodLayoutWidget
}
}
}
}

redwoodSchema {
source = projects.redwoodLayoutSchema
type = 'app.cash.redwood.layout.RedwoodLayout'
}
2 changes: 2 additions & 0 deletions redwood-layout-testing/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POM_ARTIFACT_ID=redwood-layout-testing
POM_NAME=Redwood Layout Testing
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ package app.cash.redwood.tooling.codegen

import app.cash.redwood.tooling.codegen.CodegenType.Compose
import app.cash.redwood.tooling.codegen.CodegenType.LayoutModifiers
import app.cash.redwood.tooling.codegen.CodegenType.Testing
import app.cash.redwood.tooling.codegen.CodegenType.Widget
import app.cash.redwood.tooling.schema.Schema
import java.nio.file.Path

public enum class CodegenType {
Compose,
LayoutModifiers,
Testing,
Widget,
}

Expand All @@ -43,6 +45,14 @@ public fun Schema.generate(type: CodegenType, destination: Path) {
generateLayoutModifierInterface(this, layoutModifier).writeTo(destination)
}
}
Testing -> {
generateTester(this).writeTo(destination)
generateMutableWidgetFactory(this).writeTo(destination)
for (widget in widgets) {
generateMutableWidget(this, widget).writeTo(destination)
generateWidgetValue(this, widget).writeTo(destination)
}
}
Widget -> {
generateWidgetFactories(this).writeTo(destination)
generateWidgetFactory(this).writeTo(destination)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import app.cash.redwood.tooling.schema.Widget
import app.cash.redwood.tooling.schema.Widget.Children
import app.cash.redwood.tooling.schema.Widget.Event
import app.cash.redwood.tooling.schema.Widget.Property
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
Expand Down Expand Up @@ -75,11 +74,7 @@ internal fun generateComposable(
FunSpec.builder(flatName)
.addModifiers(PUBLIC)
.addAnnotation(ComposeRuntime.Composable)
.addAnnotation(
AnnotationSpec.builder(Stdlib.OptIn)
.addMember("%T::class", Redwood.RedwoodCodegenApi)
.build(),
)
.addAnnotation(Redwood.OptInToRedwoodCodegenApi)
.apply {
// Set the layout modifier as the last non-child lambda in the function signature.
// This ensures you can still use trailing lambda syntax.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ internal fun Schema.getWidgetFactoryType(): ClassName {
return ClassName(widgetPackage(this), "${name}WidgetFactory")
}

internal fun Schema.getMutableWidgetFactoryType(): ClassName {
return ClassName(widgetPackage(this), "Mutable${name}WidgetFactory")
}

internal fun Schema.mutableWidgetType(widget: Widget): ClassName {
return ClassName(widgetPackage(this), "Mutable${widget.type.flatName}")
}

internal fun Schema.widgetValueType(widget: Widget): ClassName {
return ClassName(widgetPackage(this), "${widget.type.flatName}Value")
}

internal fun Schema.getWidgetFactoryProviderType(): ClassName {
return ClassName(widgetPackage(this), "${name}WidgetFactoryProvider")
}
Expand Down Expand Up @@ -120,6 +132,10 @@ internal fun Schema.layoutModifierImpl(layoutModifier: LayoutModifier): ClassNam
return ClassName(composePackage(), layoutModifier.type.simpleName!! + "Impl")
}

internal fun Schema.getTesterFunction(): MemberName {
return MemberName(widgetPackage(this), "${name}Tester")
}

internal val Schema.toLayoutModifier: MemberName get() =
MemberName(widgetPackage(this), "toLayoutModifier")

Expand Down
Loading

0 comments on commit b29b64c

Please sign in to comment.