Skip to content

Commit 573d770

Browse files
committed
Robot bindings for Metro
Create a `Robot` dependency graph for Metro and combine it with the `Robot` component from kotlin-inject. This allows for an easier migration from kotlin-inject to Metro and eventually mixing both dependency injection frameworks if needed. See #119
1 parent 4b04647 commit 573d770

File tree

8 files changed

+157
-24
lines changed

8 files changed

+157
-24
lines changed

robot-compose-multiplatform/public/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ plugins {
44

55
appPlatformBuildSrc {
66
enableCompose true
7-
enableKotlinInject true
87
enablePublishing true
98
}
109

@@ -15,7 +14,9 @@ dependencies {
1514

1615
commonMainImplementation project(':robot-internal:public')
1716

17+
commonTestApi project(':metro:public')
1818
commonTestApi project(':scope:testing')
19+
commonTestApi libs.metro.runtime
1920

2021
desktopTestImplementation compose.material
2122
iosTestImplementation compose.material

robot-compose-multiplatform/public/src/appleAndDesktopTest/kotlin/software/amazon/app/platform/robot/ComposeRobotTest.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import assertk.assertThat
1414
import assertk.assertions.isFalse
1515
import assertk.assertions.isTrue
1616
import assertk.assertions.messageContains
17+
import dev.zacsweers.metro.Provider
18+
import dev.zacsweers.metro.provider
1719
import kotlin.reflect.KClass
1820
import kotlin.test.Test
1921
import software.amazon.app.platform.scope.Scope
20-
import software.amazon.app.platform.scope.di.addKotlinInjectComponent
22+
import software.amazon.app.platform.scope.di.metro.addMetroDependencyGraph
2123

2224
// Note that this class has to be duplicated and cannot be moved into commonTest, because Android
2325
// unit tests don't have access to `runComposeUiTest`.
@@ -61,7 +63,7 @@ class ComposeRobotTest {
6163
}
6264

6365
private fun rootScope(vararg robots: Robot): Scope =
64-
Scope.buildRootScope { addKotlinInjectComponent(Component(*robots)) }
66+
Scope.buildRootScope { addMetroDependencyGraph(Component(*robots)) }
6567

6668
private fun ComposeUiTest.interactionProvider(): ComposeInteractionsProvider {
6769
val interactionsProvider = this
@@ -71,9 +73,9 @@ class ComposeRobotTest {
7173
}
7274
}
7375

74-
private class Component(vararg robots: Robot) : RobotComponent {
75-
override val robots: Map<KClass<out Robot>, () -> Robot> =
76-
robots.map { robot -> robot::class to { robot } }.toMap()
76+
private class Component(vararg robots: Robot) : RobotGraph {
77+
override val robots: Map<KClass<*>, Provider<Robot>> =
78+
robots.associate { robot -> robot::class to provider { robot } }
7779
}
7880

7981
private class TestRobot : ComposeRobot() {

robot/public/api/android/public.api

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ public abstract interface class software/amazon/app/platform/robot/RobotComponen
2626
public abstract fun getRobots ()Ljava/util/Map;
2727
}
2828

29+
public abstract interface class software/amazon/app/platform/robot/RobotGraph {
30+
public abstract fun getRobots ()Ljava/util/Map;
31+
}
32+
33+
public abstract interface class software/amazon/app/platform/robot/RobotGraph$$$MetroContributionToAppScope : software/amazon/app/platform/robot/RobotGraph {
34+
}
35+
36+
public final class software/amazon/app/platform/robot/RobotKt {
37+
public static final fun getAllRobots (Lsoftware/amazon/app/platform/scope/Scope;)Ljava/util/Map;
38+
}
39+
2940
public abstract interface class software/amazon/app/platform/robot/RootMatcherProvider {
3041
public abstract fun getRootMatcher ()Lorg/hamcrest/Matcher;
3142
}

robot/public/api/desktop/public.api

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ public abstract interface class software/amazon/app/platform/robot/RobotComponen
1010
public abstract fun getRobots ()Ljava/util/Map;
1111
}
1212

13+
public abstract interface class software/amazon/app/platform/robot/RobotGraph {
14+
public abstract fun getRobots ()Ljava/util/Map;
15+
}
16+
17+
public abstract interface class software/amazon/app/platform/robot/RobotGraph$$$MetroContributionToAppScope : software/amazon/app/platform/robot/RobotGraph {
18+
}
19+
20+
public final class software/amazon/app/platform/robot/RobotKt {
21+
public static final fun getAllRobots (Lsoftware/amazon/app/platform/scope/Scope;)Ljava/util/Map;
22+
}
23+
1324
public final class software/amazon/app/platform/robot/WaiterKt {
1425
public static final fun waitFor-vLdBGDU (Ljava/lang/String;JJLkotlin/jvm/functions/Function0;)Ljava/lang/Object;
1526
public static synthetic fun waitFor-vLdBGDU$default (Ljava/lang/String;JJLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object;

robot/public/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ plugins {
44

55
appPlatformBuildSrc {
66
enableKotlinInject true
7+
enableMetro true
78
enablePublishing true
89
}
910

robot/public/src/commonMain/kotlin/software/amazon/app/platform/robot/Robot.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package software.amazon.app.platform.robot
22

3+
import kotlin.reflect.KClass
34
import software.amazon.app.platform.inject.robot.ContributesRobot
45
import software.amazon.app.platform.scope.Scope
56
import software.amazon.app.platform.scope.di.kotlinInjectComponent
7+
import software.amazon.app.platform.scope.di.metro.metroDependencyGraph
68

79
/**
810
* Test robots are an abstraction between test interactions and the underlying implementation. They
@@ -84,7 +86,7 @@ public inline fun <reified T : Robot> robot(
8486
rootScope: Scope = software.amazon.app.platform.robot.internal.rootScope,
8587
noinline block: T.() -> Unit,
8688
) {
87-
val robot = rootScope.kotlinInjectComponent<RobotComponent>().robots[T::class]?.invoke() as? T
89+
val robot = rootScope.allRobots[T::class]?.invoke() as? T
8890

8991
checkNotNull(robot) {
9092
"Could not find Robot of type ${T::class}. Did you forget to add the @ContributesRobot " +
@@ -97,3 +99,26 @@ public inline fun <reified T : Robot> robot(
9799
robot.close()
98100
}
99101
}
102+
103+
@PublishedApi
104+
internal val Scope.allRobots: Map<KClass<*>, () -> Robot>
105+
get() {
106+
return kotlinInjectComponentOrNull<RobotComponent>()?.robots.orEmpty() +
107+
metroDependencyGraphOrNull<RobotGraph>()?.robots.orEmpty().mapValues { { it.value() } }
108+
}
109+
110+
private inline fun <reified T : Any> Scope.metroDependencyGraphOrNull(): T? {
111+
return try {
112+
metroDependencyGraph<T>()
113+
} catch (_: NoSuchElementException) {
114+
null
115+
}
116+
}
117+
118+
private inline fun <reified T : Any> Scope.kotlinInjectComponentOrNull(): T? {
119+
return try {
120+
kotlinInjectComponent<T>()
121+
} catch (_: NoSuchElementException) {
122+
null
123+
}
124+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package software.amazon.app.platform.robot
2+
3+
import dev.zacsweers.metro.AppScope
4+
import dev.zacsweers.metro.ContributesTo
5+
import dev.zacsweers.metro.Provider
6+
import kotlin.reflect.KClass
7+
8+
/** Graph that provides all contributed [Robot] instances from the Metro dependency graph. */
9+
@ContributesTo(AppScope::class)
10+
public interface RobotGraph {
11+
/** All [Robot]s provided in the Metro dependency graph. */
12+
public val robots: Map<KClass<*>, Provider<Robot>>
13+
}

robot/public/src/commonTest/kotlin/software/amazon/app/platform/robot/RobotTest.kt

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,54 @@ package software.amazon.app.platform.robot
33
import assertk.assertThat
44
import assertk.assertions.contains
55
import assertk.assertions.isFalse
6+
import assertk.assertions.isNotNull
67
import assertk.assertions.isNotSameInstanceAs
78
import assertk.assertions.isTrue
9+
import dev.zacsweers.metro.Provider
10+
import dev.zacsweers.metro.provider
811
import kotlin.reflect.KClass
912
import kotlin.test.Test
1013
import kotlin.test.assertFailsWith
1114
import software.amazon.app.platform.internal.Platform
1215
import software.amazon.app.platform.internal.platform
1316
import software.amazon.app.platform.scope.Scope
1417
import software.amazon.app.platform.scope.di.addKotlinInjectComponent
18+
import software.amazon.app.platform.scope.di.metro.addMetroDependencyGraph
1519

1620
class RobotTest {
1721

1822
@Test
1923
fun `if no robot can be found in the component then a proper error is thrown`() {
20-
val exception = assertFailsWith<IllegalStateException> { robot<TestRobot>(rootScope()) {} }
24+
val exception =
25+
assertFailsWith<IllegalStateException> {
26+
robot<KiTestRobot>(rootScope(kiRobot = null, metroRobot = null)) {}
27+
}
2128

2229
val message =
23-
exception.message?.replace("RobotTest\$TestRobot", "RobotTest.TestRobot").toString()
30+
exception.message?.replace("RobotTest\$KiTestRobot", "RobotTest.KiTestRobot").toString()
2431

2532
when (platform) {
2633
Platform.JVM,
2734
Platform.Native -> {
2835
assertThat(message)
2936
.contains(
3037
"Could not find Robot of type class software.amazon.app.platform." +
31-
"robot.RobotTest.TestRobot"
38+
"robot.RobotTest.KiTestRobot"
3239
)
3340
}
3441
Platform.Web -> {
35-
assertThat(message).contains("Could not find Robot of type class TestRobot")
42+
assertThat(message).contains("Could not find Robot of type class KiTestRobot")
3643
}
3744
}
3845
assertThat(message).contains("Did you forget to add the @ContributesRobot annotation?")
3946
}
4047

4148
@Test
4249
fun `the close function is called after the lambda is invoked`() {
43-
val rootScope = rootScope(TestRobot())
50+
val rootScope = rootScope(KiTestRobot())
4451

45-
lateinit var robot: TestRobot
46-
robot<TestRobot>(rootScope) {
52+
lateinit var robot: KiTestRobot
53+
robot<KiTestRobot>(rootScope) {
4754
robot = this
4855
assertThat(closeCalled).isFalse()
4956
}
@@ -58,37 +65,99 @@ class RobotTest {
5865
addKotlinInjectComponent(
5966
object : RobotComponent {
6067
override val robots: Map<KClass<out Robot>, () -> Robot> =
61-
mapOf(TestRobot::class to { TestRobot() })
68+
mapOf(KiTestRobot::class to { KiTestRobot() })
6269
}
6370
)
6471
}
6572

66-
lateinit var robot1: TestRobot
67-
lateinit var robot2: TestRobot
73+
lateinit var robot1: KiTestRobot
74+
lateinit var robot2: KiTestRobot
6875

69-
robot<TestRobot>(rootScope) { robot1 = this }
70-
robot<TestRobot>(rootScope) { robot2 = this }
76+
robot<KiTestRobot>(rootScope) { robot1 = this }
77+
robot<KiTestRobot>(rootScope) { robot2 = this }
7178

7279
assertThat(robot1).isNotSameInstanceAs(robot2)
7380

74-
robot<TestRobot>(rootScope) {
81+
robot<KiTestRobot>(rootScope) {
7582
val robot1Inner = this
76-
robot<TestRobot>(rootScope) {
83+
robot<KiTestRobot>(rootScope) {
7784
val robot2Inner = this
7885
assertThat(robot1Inner).isNotSameInstanceAs(robot2Inner)
7986
}
8087
}
8188
}
8289

83-
private fun rootScope(vararg robots: Robot): Scope =
84-
Scope.buildRootScope { addKotlinInjectComponent(Component(*robots)) }
90+
@Test
91+
fun `a robot is provided for kotlin-inject alone`() {
92+
val rootScope = rootScope(kiRobot = KiTestRobot(), metroRobot = null)
93+
94+
var kiRobot: KiTestRobot? = null
95+
robot<KiTestRobot>(rootScope) { kiRobot = this }
96+
97+
assertFailsWith<Exception> { robot<MetroTestRobot>(rootScope) {} }
98+
99+
assertThat(kiRobot).isNotNull()
100+
}
101+
102+
@Test
103+
fun `a robot is provided for metro alone`() {
104+
val rootScope = rootScope(kiRobot = null, metroRobot = MetroTestRobot())
105+
106+
assertFailsWith<Exception> { robot<KiTestRobot>(rootScope) {} }
107+
108+
var metroRobot: MetroTestRobot? = null
109+
robot<MetroTestRobot>(rootScope) { metroRobot = this }
110+
111+
assertThat(metroRobot).isNotNull()
112+
}
113+
114+
@Test
115+
fun `a robot is provided for kotlin-inject and metro simultaneously`() {
116+
val rootScope = rootScope(kiRobot = KiTestRobot(), metroRobot = MetroTestRobot())
117+
118+
var kiRobot: KiTestRobot? = null
119+
robot<KiTestRobot>(rootScope) { kiRobot = this }
120+
121+
var metroRobot: MetroTestRobot? = null
122+
robot<MetroTestRobot>(rootScope) { metroRobot = this }
123+
124+
assertThat(kiRobot).isNotNull()
125+
assertThat(metroRobot).isNotNull()
126+
}
127+
128+
private fun rootScope(
129+
kiRobot: Robot? = KiTestRobot(),
130+
metroRobot: Robot? = MetroTestRobot(),
131+
): Scope =
132+
Scope.buildRootScope {
133+
if (kiRobot != null) {
134+
addKotlinInjectComponent(Component(kiRobot))
135+
}
136+
if (metroRobot != null) {
137+
addMetroDependencyGraph(Graph(metroRobot))
138+
}
139+
}
85140

86141
private class Component(vararg robots: Robot) : RobotComponent {
87142
override val robots: Map<KClass<out Robot>, () -> Robot> =
88143
robots.associate { robot -> robot::class to { robot } }
89144
}
90145

91-
private class TestRobot : Robot {
146+
private class Graph(vararg robots: Robot) : RobotGraph {
147+
override val robots: Map<KClass<*>, Provider<Robot>> =
148+
robots.associate { robot -> robot::class to provider { robot } }
149+
}
150+
151+
private class KiTestRobot : Robot {
152+
var closeCalled = false
153+
private set
154+
155+
override fun close() {
156+
closeCalled = true
157+
}
158+
}
159+
160+
private class MetroTestRobot : Robot {
92161
var closeCalled = false
93162
private set
94163

0 commit comments

Comments
 (0)