Test retained voice resources, decoder failures and nonblocking disposal

This commit is contained in:
evgeny committed 2026-10-29 13:35:19 +01:00
1 parent bb08b20e58
commit b25766ae5a
3 files changed
+78 -4

No files matched your search

@@ -9,10 +9,11 @@ object AudioManager { const val STREAM_MUSIC = 3 }
class AudioTrack(stream: Int, rate: Int, mask: Int, encoding: Int, bytes: Int, mode: Int) {
class Plan {
var initialized = true
var failures = emptySet<String>()
@Volatile var failures = emptySet<String>()
val results = LinkedBlockingQueue<Int>()
val accepted = CopyOnWriteArrayList<Short>()
var releaseCalls = 0
@Volatile var released = false
}
companion object {
const val MODE_STREAM = 1; const val STATE_INITIALIZED = 1
@@ -32,5 +33,8 @@ class AudioTrack(stream: Int, rate: Int, mask: Int, encoding: Int, bytes: Int, m
return result
}
fun stop() { check("stop" !in plan.failures) { "track stop failed" } }
fun release() { plan.releaseCalls++; check("release" !in plan.failures) { "track release failed" } }
fun release() {
plan.releaseCalls++; check("release" !in plan.failures) { "track release failed" }; plan.released = true
check("release after" !in plan.failures) { "track release failed after effect" }
}
}
@@ -17,11 +17,14 @@ object NativeLib {
val cancels = AtomicInteger()
val decoderReads = LinkedBlockingQueue<Int>()
val decoderCloses = AtomicInteger()
@Volatile var failDecoderClose = false
@Volatile var failCancel = false
var decoderInfo = intArrayOf(48000, 1, 960)
fun reset() {
active = false; acceptStart = true; acceptFeed = true; stopResult = 800; onFeed = {}
starts.clear(); feeds.clear(); stops.set(0); cancels.set(0)
decoderReads.clear(); decoderCloses.set(0); decoderInfo = intArrayOf(48000, 1, 960)
failDecoderClose = false; failCancel = false
}
@Synchronized fun voiceStart(channel: String, reply: String = "", request: Long = 0, generation: Long = 0): Boolean {
starts.add(listOf(channel, reply, request, generation))
@@ -30,14 +33,14 @@ object NativeLib {
}
fun voiceFeed(pcm: ShortArray): Boolean { feeds.add(pcm.copyOf()); onFeed(); return active && acceptFeed }
@Synchronized fun voiceStop(): Int { stops.incrementAndGet(); active = false; return stopResult }
@Synchronized fun voiceCancel() { cancels.incrementAndGet(); active = false }
@Synchronized fun voiceCancel() { check(!failCancel) { "native cancel rejected" }; cancels.incrementAndGet(); active = false }
fun voiceDecodeOpen(path: String, info: IntArray): Long { decoderInfo.copyInto(info); return 1 }
fun voiceDecodeRead(handle: Long, pcm: ShortArray): Int {
val count = decoderReads.poll() ?: 0
if (count > 0) pcm.fill(12, 0, count)
return count
}
fun voiceDecodeClose(handle: Long) { decoderCloses.incrementAndGet() }
fun voiceDecodeClose(handle: Long) { check(!failDecoderClose) { "decoder close rejected" }; decoderCloses.incrementAndGet() }
}
object StreamController { var blocked = false; fun blocksCapture() = blocked }
@@ -21,9 +21,11 @@ class AudioRecorderManagerTest {
ChatApplication.instance = RecorderHostContext(); LogManager.entries.clear()
}
@After fun release() {
NativeLib.failCancel = false; NativeLib.failDecoderClose = false
AudioRecord.created.forEach { it.plan.failures = emptySet(); it.plan.chunks.offer(AudioRecord.Chunk(count = -3)) }
AudioTrack.created.forEach { it.plan.failures = emptySet() }
manager.cancelRecording(); manager.stopMicTest(); manager.stopPlayback()
manager.requestClose().get(3, TimeUnit.SECONDS)
AudioRecord.created.forEach {
it.plan.reader?.join(2_000)
assertFalse("reader survived cleanup", it.plan.reader?.isAlive == true)
@@ -176,4 +178,69 @@ class AudioRecorderManagerTest {
manager.playVoiceFile("voice.opus", onComplete = { done.countDown() })
assertTrue(done.await(2, TimeUnit.SECONDS)); assertEquals(1, NativeLib.decoderCloses.get()); diagnostic("decode")
}
@Test fun disposedViewModelLeavesAProcessOwnerUntilCaptureReleaseIsAcknowledged() {
val device = plan(); assertTrue(manager.startRecording("7")); device.failures = setOf("release")
val closed = manager.requestClose()
waitUntil { NativeLib.cancels.get() == 1 }
assertFalse(closed.isDone); assertFalse(NativeLib.active)
assertThrows(IllegalStateException::class.java) { AudioRecorderManager.requireReleasedForCore() }
device.failures = emptySet(); closed.get(2, TimeUnit.SECONDS)
assertTrue(device.released); AudioRecorderManager.requireReleasedForCore()
}
@Test fun nativeCancelFailureRetainsTheCaptureUntilARealAcknowledgement() {
plan(); assertTrue(manager.startRecording("7")); NativeLib.failCancel = true
assertEquals(-1, manager.stopRecording()); assertTrue(NativeLib.active)
val closed = manager.requestClose(); assertFalse(closed.isDone)
assertThrows(IllegalStateException::class.java) { AudioRecorderManager.requireReleasedForCore() }
NativeLib.failCancel = false; closed.get(2, TimeUnit.SECONDS)
assertFalse(NativeLib.active); AudioRecorderManager.requireReleasedForCore()
}
@Test fun playbackReleaseFailureDoesNotLoseItsDeviceAfterCompletionCallback() {
val output = AudioTrack.Plan().also { it.failures = setOf("release"); AudioTrack.plans.add(it) }
val completed = CountDownLatch(1)
manager.playVoiceFile("voice.opus", onComplete = { completed.countDown() })
assertTrue(completed.await(2, TimeUnit.SECONDS)); assertEquals(1, NativeLib.decoderCloses.get())
val closed = manager.requestClose(); assertFalse(closed.isDone); assertFalse(output.released)
output.failures = emptySet(); closed.get(2, TimeUnit.SECONDS)
assertTrue(output.released); assertTrue(output.releaseCalls >= 2)
}
@Test fun releaseAfterEffectStillNeedsAcknowledgementAndDoesNotCloseDecoderTwice() {
val output = AudioTrack.Plan().also { it.failures = setOf("release after"); AudioTrack.plans.add(it) }
val completed = CountDownLatch(1)
manager.playVoiceFile("voice.opus", onComplete = { completed.countDown() })
assertTrue(completed.await(2, TimeUnit.SECONDS)); assertTrue(output.released)
val closed = manager.requestClose(); assertFalse(closed.isDone)
output.failures = emptySet(); closed.get(2, TimeUnit.SECONDS)
assertEquals(1, NativeLib.decoderCloses.get())
}
@Test fun decoderCloseFailureDoesNotSkipTheIndependentTrackRelease() {
NativeLib.failDecoderClose = true
val output = AudioTrack.Plan().also { AudioTrack.plans.add(it) }; val completed = CountDownLatch(1)
manager.playVoiceFile("voice.opus", onComplete = { completed.countDown() })
assertTrue(completed.await(2, TimeUnit.SECONDS)); assertTrue(output.released)
val closed = manager.requestClose(); assertFalse(closed.isDone)
NativeLib.failDecoderClose = false; closed.get(2, TimeUnit.SECONDS)
assertEquals(1, NativeLib.decoderCloses.get()); assertEquals(1, output.releaseCalls)
}
@Test fun blockedNativeFeedCannotHoldMainWhileAnAsynchronousCloseIsPending() {
val entered = CountDownLatch(1); val leave = CountDownLatch(1)
NativeLib.onFeed = {
entered.countDown()
while (leave.count != 0L) try { leave.await() } catch (_: InterruptedException) {}
}
val device = plan(); assertTrue(manager.startRecording("7")); device.chunks.add(AudioRecord.Chunk())
assertTrue(entered.await(1, TimeUnit.SECONDS))
val closed = manager.requestClose()
try {
assertFalse(closed.isDone); assertFalse(manager.startRecording("8"))
assertThrows(IllegalStateException::class.java) { AudioRecorderManager.requireReleasedForCore() }
} finally { leave.countDown() }
closed.get(2, TimeUnit.SECONDS); assertTrue(device.released); assertFalse(NativeLib.active)
}
}