Reproduce premature video shutdown acknowledgments and lost camera cleanup owners
This commit is contained in:
1 parent
43f61f6f0b
commit
53383ec754
2 files changed
+120
-3
No files matched your search
+3
-3
@@ -40,7 +40,7 @@ class CaptureRequest {
|
||||
/** Реальные callback-поколения SDK воспроизводит тест; камера/сессия закрываются независимо. */
|
||||
class CameraManager {
|
||||
companion object { var shared = CameraManager() }
|
||||
data class Open(val device: CameraDevice, val callback: CameraDevice.StateCallback, val handler: Handler)
|
||||
data class Open(val device: CameraDevice, val callback: CameraDevice.StateCallback, val handler: Handler, @Volatile var delivered: Boolean = false)
|
||||
val opened = CopyOnWriteArrayList<Open>()
|
||||
val sessions = CopyOnWriteArrayList<CameraCaptureSession>()
|
||||
val characteristics = CameraCharacteristics()
|
||||
@@ -57,10 +57,10 @@ class CameraManager {
|
||||
check("open" !in failures) { "camera open failed" }
|
||||
val device = CameraDevice(id, this, callback, handler).also { it.failures = nextDeviceFailures }
|
||||
opened.add(Open(device, callback, handler))
|
||||
if (autoOpened) handler.post { callback.onOpened(device) }
|
||||
if (autoOpened) deliverOpened(opened.lastIndex)
|
||||
check("open after" !in failures) { "camera open failed after effect" }
|
||||
}
|
||||
fun deliverOpened(index: Int) { val r = opened[index]; r.handler.post { r.callback.onOpened(r.device) } }
|
||||
fun deliverOpened(index: Int) { val r = opened[index]; r.handler.post { r.delivered = true; r.callback.onOpened(r.device) } }
|
||||
fun deliverClosed(index: Int) { val r = opened[index]; r.handler.post { r.callback.onClosed(r.device) } }
|
||||
}
|
||||
class CameraDevice(val id: String, private val owner: CameraManager, private val callback: StateCallback, private val handler: Handler) {
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.media.ImageReader
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import android.view.OrientationEventListener
|
||||
import com.utun.chat.ChatApplication
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Реальные TX/RX/camera callbacks; hardware, capability probe и JNI задаются на границах. */
|
||||
class CallVideoEngineLifecycleTest {
|
||||
private val engines = mutableListOf<CallVideoEngine>()
|
||||
private val uncaught = CopyOnWriteArrayList<Throwable>()
|
||||
private val manager get() = CameraManager.shared
|
||||
private val main get() = Looper.getMainLooper()
|
||||
@Before fun prepare() {
|
||||
main.advanceBy(0); assertSame(main, Looper.myLooper())
|
||||
CameraManager.shared = CameraManager(); ImageReader.created.clear(); ImageReader.nextFailures = emptySet()
|
||||
CallVideoCodec.created.clear(); CallVideoCodec.nextFailures = emptySet(); CallVideoCodec.onProbe = {}
|
||||
OrientationEventListener.created.clear(); OrientationEventListener.failures = emptySet()
|
||||
ChatApplication.instance.configProvider.failures = emptySet()
|
||||
CallVideoNative.reset(); Handler.rejectDelayed = false; Handler.rejectImmediate = false; LogManager.entries.clear()
|
||||
}
|
||||
private fun thread(engine: CallVideoEngine) = CallVideoEngine::class.java.getDeclaredField("thread")
|
||||
.apply { isAccessible = true }.get(engine) as HandlerThread
|
||||
private fun await(message: String, condition: () -> Boolean) {
|
||||
val end = System.nanoTime() + TimeUnit.SECONDS.toNanos(3)
|
||||
while (!condition() && System.nanoTime() < end) { main.advanceBy(10); Thread.sleep(2) }
|
||||
assertTrue(message, condition())
|
||||
}
|
||||
private fun engine(): CallVideoEngine = CallVideoEngine(Context(), 7).also { engine ->
|
||||
engines.add(engine)
|
||||
thread(engine).uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, error ->
|
||||
uncaught.add(error); LogManager.addLog("ERROR", "VideoEngineTest", "uncaught worker error: $error")
|
||||
}
|
||||
await("video capabilities ready") { engine.state.value.ready }
|
||||
}
|
||||
private fun capture(): CallVideoEngine {
|
||||
val engine = engine(); engine.select(engine.state.value.settings.copy(enabled = true))
|
||||
await("camera is repeating") { manager.sessions.any { it.repeating } }
|
||||
return engine
|
||||
}
|
||||
@After fun release() {
|
||||
Handler.rejectDelayed = false; Handler.rejectImmediate = false
|
||||
OrientationEventListener.failures = emptySet(); ChatApplication.instance.configProvider.failures = emptySet()
|
||||
CallVideoNative.failures = emptySet(); CallVideoNative.onOpen = {}; CallVideoNative.onClose = {}
|
||||
CallVideoCodec.onProbe = {}; CallVideoCodec.created.forEach { it.failures = emptySet() }
|
||||
manager.failures = emptySet(); manager.autoClosed = true
|
||||
manager.opened.forEachIndexed { index, open ->
|
||||
open.device.failures = emptySet()
|
||||
if (!open.delivered && !open.device.closed) manager.deliverOpened(index)
|
||||
if (open.device.closed) manager.deliverClosed(index)
|
||||
}
|
||||
manager.sessions.forEach { it.failures = emptySet() }; ImageReader.created.forEach { it.failures = emptySet() }
|
||||
for (engine in engines) {
|
||||
await("confirmed video stop") { engine.stop() && !thread(engine).isAlive }
|
||||
}
|
||||
main.advanceBy(30_000)
|
||||
assertTrue("uncaught worker exceptions: $uncaught", uncaught.isEmpty())
|
||||
assertTrue(manager.opened.all { it.device.closed }); assertTrue(ImageReader.created.all { it.closed })
|
||||
assertTrue(CallVideoCodec.created.all { it.closed }); assertTrue(CallVideoNative.ports.isEmpty())
|
||||
assertTrue(OrientationEventListener.created.none { it.enabled }); assertTrue(CallVideoNative.live.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun stopWaitsForCameraClosedBeforeAcknowledgingAndClosingTheNativePort() {
|
||||
val engine = capture(); manager.autoClosed = false
|
||||
assertFalse(engine.stop())
|
||||
await("close requested") { manager.opened.single().device.closed }
|
||||
assertFalse(engine.stop()); assertTrue(7L in CallVideoNative.ports)
|
||||
manager.deliverClosed(0)
|
||||
await("camera closure acknowledged") { engine.stop() }
|
||||
assertFalse(7L in CallVideoNative.ports)
|
||||
}
|
||||
|
||||
@Test fun failedReaderReleaseDoesNotSkipCameraCloseOrLoseTheReader() {
|
||||
val engine = capture(); val reader = ImageReader.created.single().also { it.failures = setOf("close") }
|
||||
engine.stop()
|
||||
await("camera close despite reader failure") { manager.opened.single().device.closed }
|
||||
assertFalse(engine.stop()); assertFalse(reader.closed); assertTrue(7L in CallVideoNative.ports)
|
||||
reader.failures = emptySet(); await("reader retry completes stop") { engine.stop() }
|
||||
assertTrue(reader.closed)
|
||||
}
|
||||
|
||||
@Test fun failedDeviceCloseKeepsOwnershipAndTheNativePortUntilRetry() {
|
||||
val engine = capture(); val device = manager.opened.single().device.also { it.failures = setOf("close") }
|
||||
engine.stop(); await("device close attempted") { device.closeCalls > 0 }
|
||||
assertFalse(device.closed); assertFalse(engine.stop()); assertTrue(7L in CallVideoNative.ports)
|
||||
device.failures = emptySet(); await("device close retries") { engine.stop() }
|
||||
assertTrue(device.closed)
|
||||
}
|
||||
|
||||
@Test fun encoderReleaseErrorCannotAbortIndependentCameraAndSensorCleanup() {
|
||||
val engine = capture(); val encoder = CallVideoCodec.created.single().also { it.failures = setOf("close") }
|
||||
engine.stop()
|
||||
await("camera closes even when encoder release fails") { manager.opened.single().device.closed }
|
||||
assertTrue(OrientationEventListener.created.none { it.enabled })
|
||||
assertFalse(encoder.closed); assertFalse(engine.stop()); assertTrue(thread(engine).isAlive)
|
||||
encoder.failures = emptySet(); await("encoder release retries") { engine.stop() }
|
||||
}
|
||||
|
||||
@Test fun settingsSaveFailureIsContainedAndDoesNotApplyAnUncommittedSelection() {
|
||||
val engine = engine(); val previous = engine.state.value.settings
|
||||
ChatApplication.instance.configProvider.failures = setOf("settings save")
|
||||
engine.select(previous.copy(enabled = true))
|
||||
await("selection rejection reported") { engine.state.value.notice != null }
|
||||
assertTrue(thread(engine).isAlive); assertEquals(previous, engine.state.value.settings)
|
||||
assertTrue(manager.opened.isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user