mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
test(android): drain runtime fixtures before sandbox teardown (#130861)
This commit is contained in:
committed by
GitHub
parent
46ca7d1f27
commit
7dbd87d160
@@ -175,7 +175,7 @@ class NodeForegroundServiceTest {
|
||||
assertEquals(2, Shadows.shadowOf(controller.get()).stopSelfResultId)
|
||||
assertNull(app.peekRuntime())
|
||||
} finally {
|
||||
controller.destroy()
|
||||
closeNodeServiceTestFixture(controller, app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +207,7 @@ class NodeForegroundServiceTest {
|
||||
assertEquals(Service.START_NOT_STICKY, stopped)
|
||||
assertEquals(Service.START_STICKY, resumed)
|
||||
} finally {
|
||||
controller.destroy()
|
||||
app.peekRuntime()?.disconnect()
|
||||
closeNodeServiceTestFixture(controller, app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +224,7 @@ class NodeForegroundServiceTest {
|
||||
assertFalse(runtime.isForeground.value)
|
||||
assertFalse(prefs.voiceMicEnabled.value)
|
||||
} finally {
|
||||
runtime.disconnect()
|
||||
closeNodeRuntimeTestFixture(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.chat.AndroidClientDatabases
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.android.controller.ServiceController
|
||||
import org.robolectric.shadows.ShadowPausedLooper
|
||||
import org.robolectric.util.ReflectionHelpers
|
||||
|
||||
internal fun closeNodeRuntimeTestFixture(runtime: NodeRuntime) = drainWithMainLooper { closeRuntime(runtime) }
|
||||
|
||||
internal fun closeNodeServiceTestFixture(
|
||||
controller: ServiceController<NodeForegroundService>,
|
||||
app: NodeApp,
|
||||
) {
|
||||
try {
|
||||
controller.destroy()
|
||||
} finally {
|
||||
drainWithMainLooper {
|
||||
// A canceled service can still be constructing the process runtime. Join its producer
|
||||
// before taking the app's final runtime snapshot, including the asynchronous STOP task.
|
||||
ReflectionHelpers
|
||||
.getField<CoroutineScope>(controller.get(), "scope")
|
||||
.coroutineContext.job
|
||||
.join()
|
||||
ReflectionHelpers
|
||||
.getField<CoroutineScope>(app, "runtimeScope")
|
||||
.coroutineContext.job
|
||||
.cancelAndJoin()
|
||||
app.peekRuntime()?.let { closeRuntime(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun closeRuntime(runtime: NodeRuntime) {
|
||||
val databases = ReflectionHelpers.getField<AndroidClientDatabases>(runtime, "clientDatabases")
|
||||
try {
|
||||
try {
|
||||
runtime.disconnect()
|
||||
} finally {
|
||||
ReflectionHelpers
|
||||
.getField<CoroutineScope>(runtime, "scope")
|
||||
.coroutineContext.job
|
||||
.cancelAndJoin()
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
// Database initialization has its own IO scope. Await its real result before closing so
|
||||
// native loading cannot escape this sandbox and initialization failures remain test failures.
|
||||
databases.clientStateDatabase()
|
||||
} finally {
|
||||
databases.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drainWithMainLooper(block: suspend () -> Unit) {
|
||||
val mainLooper = Looper.getMainLooper()
|
||||
val shadowLooper = shadowOf(mainLooper) as ShadowPausedLooper
|
||||
val handler = Handler(mainLooper)
|
||||
var completed = false
|
||||
val wakeMain = Runnable { completed = true }
|
||||
try {
|
||||
runBlocking {
|
||||
val cleanup = async(Dispatchers.Default) { block() }
|
||||
cleanup.invokeOnCompletion { handler.post(wakeMain) }
|
||||
// Robolectric's paused Main looper needs pumping while worker finalizers dispatch to it.
|
||||
// Completion posts a wakeup so poll cannot strand Main after the last worker finishes.
|
||||
while (!completed) {
|
||||
shadowLooper.poll(0)
|
||||
shadowLooper.idle()
|
||||
}
|
||||
cleanup.await()
|
||||
}
|
||||
} finally {
|
||||
handler.removeCallbacks(wakeMain)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -21,6 +22,13 @@ import java.util.UUID
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VoiceWakeRuntimeTest {
|
||||
private var runtimeUnderTest: NodeRuntime? = null
|
||||
|
||||
@After
|
||||
fun closeRuntime() {
|
||||
runtimeUnderTest?.let(::closeNodeRuntimeTestFixture)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disconnectedSaveDoesNotCreateLocalOverride() {
|
||||
val runtime = createTestRuntime()
|
||||
@@ -173,7 +181,7 @@ class VoiceWakeRuntimeTest {
|
||||
)
|
||||
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
|
||||
prefs.setVoiceWakeEnabled(true)
|
||||
val runtime = NodeRuntime(app, prefs, mode = NodeRuntimeMode.ScreenshotFixture)
|
||||
val runtime = NodeRuntime(app, prefs, mode = NodeRuntimeMode.ScreenshotFixture).also { runtimeUnderTest = it }
|
||||
val endpoint = GatewayEndpoint.manual("127.0.0.1", 18789)
|
||||
writeField(runtime, "connectedEndpoint", endpoint)
|
||||
|
||||
@@ -225,7 +233,7 @@ class VoiceWakeRuntimeTest {
|
||||
"openclaw.node.voicewake.runtime.test.${UUID.randomUUID()}",
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
return NodeRuntime(app, SecurePrefs(app, securePrefsOverride = securePrefs))
|
||||
return NodeRuntime(app, SecurePrefs(app, securePrefsOverride = securePrefs)).also { runtimeUnderTest = it }
|
||||
}
|
||||
|
||||
private fun seedConnectedRuntime(runtime: NodeRuntime): GatewayEndpoint {
|
||||
|
||||
Reference in New Issue
Block a user