|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import math |
4 | 5 | import queue |
5 | 6 | import shutil |
6 | 7 | import struct |
7 | 8 | import subprocess |
| 9 | +import sys |
| 10 | +import textwrap |
8 | 11 | import threading |
9 | 12 | import time |
10 | 13 | import wave |
|
30 | 33 | ] |
31 | 34 |
|
32 | 35 | _CLOSE_TIMEOUT_S = 5.0 |
| 36 | +_REAL_SOURCE_RERUN_TIMEOUT_S = 12.0 |
33 | 37 | _PLAYBACK_DURATION_S = 3.0 |
34 | 38 | _EXPECTED_TONE_HZ = 440.0 |
35 | 39 |
|
@@ -312,3 +316,102 @@ def test_medium_system_audio_engine_close_remains_stable_across_repeated_runs( |
312 | 316 |
|
313 | 317 | assert len(elapsed_values) == 3 |
314 | 318 | assert max(elapsed_values) < _CLOSE_TIMEOUT_S |
| 319 | + |
| 320 | + |
| 321 | +def test_medium_real_source_asr_rerun_engine_close_remains_stable(tmp_path: Path) -> None: |
| 322 | + probe = textwrap.dedent( |
| 323 | + """ |
| 324 | + import asyncio |
| 325 | + import json |
| 326 | + import time |
| 327 | +
|
| 328 | + import macloop |
| 329 | +
|
| 330 | + async def run() -> None: |
| 331 | + stats = [] |
| 332 | + for cycle in range(2): |
| 333 | + engine = macloop.AudioEngine() |
| 334 | + try: |
| 335 | + remote = engine.create_stream(macloop.SystemAudioSource) |
| 336 | + mic = engine.create_stream(macloop.MicrophoneSource, vpio_enabled=True) |
| 337 | + routes = [ |
| 338 | + engine.route(id=f"remote_{cycle}", stream=remote), |
| 339 | + engine.route(id=f"mic_{cycle}", stream=mic), |
| 340 | + ] |
| 341 | +
|
| 342 | + for sink_cycle in range(2): |
| 343 | + sink = macloop.AsrSink( |
| 344 | + routes=routes, |
| 345 | + chunk_frames=1280, |
| 346 | + sample_rate=16000, |
| 347 | + channels=1, |
| 348 | + sample_format="i16", |
| 349 | + ) |
| 350 | +
|
| 351 | + async def reader() -> None: |
| 352 | + async for _ in sink.chunks_async(): |
| 353 | + pass |
| 354 | +
|
| 355 | + task = asyncio.create_task(reader()) |
| 356 | + await asyncio.sleep(1.5) |
| 357 | + sink.close() |
| 358 | + await task |
| 359 | + await asyncio.sleep(0.25) |
| 360 | +
|
| 361 | + started = time.monotonic() |
| 362 | + engine.close() |
| 363 | + stats.append( |
| 364 | + { |
| 365 | + "cycle": cycle, |
| 366 | + "engine_close_elapsed_s": round(time.monotonic() - started, 6), |
| 367 | + } |
| 368 | + ) |
| 369 | + finally: |
| 370 | + try: |
| 371 | + engine.close() |
| 372 | + except Exception: |
| 373 | + pass |
| 374 | +
|
| 375 | + print(json.dumps(stats), flush=True) |
| 376 | +
|
| 377 | + asyncio.run(run()) |
| 378 | + """ |
| 379 | + ) |
| 380 | + |
| 381 | + elapsed_values: list[float] = [] |
| 382 | + for run_index in range(2): |
| 383 | + try: |
| 384 | + completed = subprocess.run( |
| 385 | + [sys.executable, "-c", probe], |
| 386 | + capture_output=True, |
| 387 | + text=True, |
| 388 | + timeout=_REAL_SOURCE_RERUN_TIMEOUT_S, |
| 389 | + cwd=tmp_path, |
| 390 | + ) |
| 391 | + except subprocess.TimeoutExpired as exc: |
| 392 | + pytest.fail( |
| 393 | + f"real-source rerun probe timed out on run {run_index + 1} after " |
| 394 | + f"{_REAL_SOURCE_RERUN_TIMEOUT_S:.1f}s\nSTDOUT:\n{exc.stdout or ''}\nSTDERR:\n{exc.stderr or ''}" |
| 395 | + ) |
| 396 | + |
| 397 | + assert completed.returncode == 0, ( |
| 398 | + f"real-source rerun probe failed on run {run_index + 1} with return code " |
| 399 | + f"{completed.returncode}\nSTDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}" |
| 400 | + ) |
| 401 | + |
| 402 | + payload = completed.stdout.strip().splitlines() |
| 403 | + assert payload, ( |
| 404 | + f"real-source rerun probe produced no JSON payload on run {run_index + 1}\n" |
| 405 | + f"STDERR:\n{completed.stderr}" |
| 406 | + ) |
| 407 | + stats = json.loads(payload[-1]) |
| 408 | + assert len(stats) == 2 |
| 409 | + close_times = [float(item["engine_close_elapsed_s"]) for item in stats] |
| 410 | + elapsed_values.extend(close_times) |
| 411 | + assert max(close_times) < _CLOSE_TIMEOUT_S, ( |
| 412 | + f"real-source rerun probe observed slow engine.close() on run {run_index + 1}: " |
| 413 | + f"{close_times}\nSTDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}" |
| 414 | + ) |
| 415 | + |
| 416 | + assert len(elapsed_values) == 4 |
| 417 | + assert max(elapsed_values) < _CLOSE_TIMEOUT_S |
0 commit comments