@@ -5,6 +5,7 @@ import assertk.assertions.contains
55import assertk.assertions.isFalse
66import assertk.assertions.isNotNull
77import assertk.assertions.isNotSameInstanceAs
8+ import assertk.assertions.isSameInstanceAs
89import assertk.assertions.isTrue
910import kotlin.reflect.KClass
1011import kotlin.test.Test
@@ -173,6 +174,202 @@ class RobotTest {
173174 assertFailsWith<IllegalStateException > { robot<ChildTestRobot >(rootScope) {} }
174175 }
175176
177+ @Test
178+ fun `scopes destroyed before their services are read are skipped` () {
179+ val expectedRobot = MetroTestRobot ()
180+ val rootScope = rootScope(kiRobot = null , metroRobot = expectedRobot)
181+ val childScope = rootScope.buildChild(" child" )
182+ val destroyingChild =
183+ object : Scope by childScope {
184+ override fun isDestroyed (): Boolean {
185+ val destroyed = childScope.isDestroyed()
186+ childScope.destroy()
187+ return destroyed
188+ }
189+ }
190+ val scope =
191+ object : Scope by rootScope {
192+ override fun children (): Set <Scope > = setOf (destroyingChild)
193+ }
194+
195+ robot<MetroTestRobot >(scope) { assertThat(this ).isSameInstanceAs(expectedRobot) }
196+ assertThat(childScope.isDestroyed()).isTrue()
197+ }
198+
199+ @Test
200+ fun `scopes destroyed before their children are read are skipped` () {
201+ val expectedRobot = MetroTestRobot ()
202+ val rootScope = rootScope(kiRobot = null , metroRobot = expectedRobot)
203+ val childScope = rootScope.buildChild(" child" )
204+ val destroyingChild =
205+ object : Scope by childScope {
206+ override fun children (): Set <Scope > {
207+ childScope.destroy()
208+ return childScope.children()
209+ }
210+ }
211+ val scope =
212+ object : Scope by rootScope {
213+ override fun children (): Set <Scope > = setOf (destroyingChild)
214+ }
215+
216+ robot<MetroTestRobot >(scope) { assertThat(this ).isSameInstanceAs(expectedRobot) }
217+ assertThat(childScope.isDestroyed()).isTrue()
218+ }
219+
220+ @Test
221+ fun `service failures from a live scope propagate unchanged` () {
222+ val failure = IllegalStateException (" Service lookup failed" )
223+ val scope =
224+ object : Scope by rootScope() {
225+ override fun <T : Any > getService (key : String ): T ? {
226+ throw failure
227+ }
228+ }
229+
230+ val actual = assertFailsWith<IllegalStateException > { robot<MetroTestRobot >(scope) {} }
231+
232+ assertThat(actual).isSameInstanceAs(failure)
233+ assertThat(scope.isDestroyed()).isFalse()
234+ }
235+
236+ @Test
237+ fun `children failures from a live scope propagate unchanged` () {
238+ val failure = IllegalStateException (" Children lookup failed" )
239+ val scope =
240+ object : Scope by rootScope() {
241+ override fun children (): Set <Scope > {
242+ throw failure
243+ }
244+ }
245+
246+ val actual = assertFailsWith<IllegalStateException > { robot<MetroTestRobot >(scope) {} }
247+
248+ assertThat(actual).isSameInstanceAs(failure)
249+ assertThat(scope.isDestroyed()).isFalse()
250+ }
251+
252+ @Test
253+ fun `scopes destroyed while reading robots do not hide a sibling robot` () {
254+ val rootScope = rootScope(kiRobot = null , metroRobot = null )
255+ lateinit var destroyingChild: Scope
256+ destroyingChild =
257+ rootScope.buildChild(" destroying-child" ) {
258+ addMetroDependencyGraph(
259+ object : RobotGraph {
260+ override val robots: Map <KClass <* >, () -> Robot >
261+ get() {
262+ destroyingChild.destroy()
263+ return mapOf (ChildTestRobot ::class to { ChildTestRobot () })
264+ }
265+ },
266+ )
267+ }
268+ val expectedRobot = ChildTestRobot ()
269+ rootScope.buildChild(" live-child" ) { addMetroDependencyGraph(Graph (expectedRobot)) }
270+
271+ robot<ChildTestRobot >(rootScope) { assertThat(this ).isSameInstanceAs(expectedRobot) }
272+ assertThat(destroyingChild.isDestroyed()).isTrue()
273+ }
274+
275+ @Test
276+ fun `a matching scope destroyed later during discovery is ignored` () {
277+ val rootScope = rootScope(kiRobot = null , metroRobot = null )
278+ val firstChild =
279+ rootScope.buildChild(" first-child" ) { addMetroDependencyGraph(Graph (ChildTestRobot ())) }
280+ val expectedRobot = ChildTestRobot ()
281+ rootScope.buildChild(" second-child" ) {
282+ addMetroDependencyGraph(
283+ object : RobotGraph {
284+ override val robots: Map <KClass <* >, () -> Robot >
285+ get() {
286+ firstChild.destroy()
287+ return mapOf (ChildTestRobot ::class to { expectedRobot })
288+ }
289+ },
290+ )
291+ }
292+
293+ robot<ChildTestRobot >(rootScope) { assertThat(this ).isSameInstanceAs(expectedRobot) }
294+ assertThat(firstChild.isDestroyed()).isTrue()
295+ }
296+
297+ @Test
298+ fun `robot getter failures propagate even if the scope was destroyed` () {
299+ val rootScope = rootScope(kiRobot = null , metroRobot = null )
300+ val failure = IllegalStateException (" Robot getter failed" )
301+ lateinit var childScope: Scope
302+ childScope =
303+ rootScope.buildChild(" child" ) {
304+ addMetroDependencyGraph(
305+ object : RobotGraph {
306+ override val robots: Map <KClass <* >, () -> Robot >
307+ get() {
308+ childScope.destroy()
309+ throw failure
310+ }
311+ },
312+ )
313+ }
314+
315+ val actual = assertFailsWith<IllegalStateException > { robot<ChildTestRobot >(rootScope) {} }
316+
317+ assertThat(actual).isSameInstanceAs(failure)
318+ }
319+
320+ @Test
321+ fun `robot getters accessing another destroyed scope propagate the failure` () {
322+ val destroyedScope = Scope .buildRootScope(" destroyed" )
323+ destroyedScope.destroy()
324+ var failure: IllegalStateException ? = null
325+ val rootScope = Scope .buildRootScope {
326+ addMetroDependencyGraph(
327+ object : RobotGraph {
328+ override val robots: Map <KClass <* >, () -> Robot >
329+ get() =
330+ try {
331+ destroyedScope.getService<Map <KClass <* >, () -> Robot >> (" robots" ).orEmpty()
332+ } catch (exception: IllegalStateException ) {
333+ failure = exception
334+ throw exception
335+ }
336+ },
337+ )
338+ }
339+
340+ val actual = assertFailsWith<IllegalStateException > { robot<ChildTestRobot >(rootScope) {} }
341+
342+ assertThat(actual).isSameInstanceAs(failure)
343+ assertThat(rootScope.isDestroyed()).isFalse()
344+ }
345+
346+ @Test
347+ fun `robot factory failures propagate unchanged` () {
348+ val failure = IllegalStateException (" Robot factory failed" )
349+ val rootScope = Scope .buildRootScope {
350+ addKotlinInjectComponent(
351+ object : RobotComponent {
352+ override val robots: Map <KClass <out Robot >, () -> Robot > =
353+ mapOf (KiTestRobot ::class to { throw failure })
354+ },
355+ )
356+ }
357+
358+ val actual = assertFailsWith<IllegalStateException > { robot<KiTestRobot >(rootScope) {} }
359+
360+ assertThat(actual).isSameInstanceAs(failure)
361+ }
362+
363+ @Test
364+ fun `discovery starting at a child finds robots from its parent` () {
365+ val kiRobot = KiTestRobot ()
366+ val metroRobot = MetroTestRobot ()
367+ val childScope = rootScope(kiRobot, metroRobot).buildChild(" child" )
368+
369+ robot<KiTestRobot >(childScope) { assertThat(this ).isSameInstanceAs(kiRobot) }
370+ robot<MetroTestRobot >(childScope) { assertThat(this ).isSameInstanceAs(metroRobot) }
371+ }
372+
176373 private fun rootScope (
177374 kiRobot : Robot ? = KiTestRobot (),
178375 metroRobot : Robot ? = MetroTestRobot (),
0 commit comments