"""streaming.py 三层活性看门狗测试(移植自 VT adapters/streaming.py,语义零改动)。""" import asyncio import pytest from polygateway.streaming import StreamLivenessTimeout, stream_with_liveness_timeouts async def _emit(items, *, delay_s=0.0, first_delay_s=None): """可控节奏的假流;first_delay_s 单独控制首项延迟。""" for i, item in enumerate(items): wait = first_delay_s if (i == 0 and first_delay_s is not None) else delay_s if wait: await asyncio.sleep(wait) yield item async def _collect(agen): return [item async for item in agen] class TestPassthrough: async def test_items_pass_through_in_order(self): wrapped = stream_with_liveness_timeouts( _emit(["a", "b", "c"]), ttft_s=1.0, inter_token_s=1.0, total_s=5.0 ) assert await _collect(wrapped) == ["a", "b", "c"] async def test_empty_stream_ends_cleanly(self): wrapped = stream_with_liveness_timeouts( _emit([]), ttft_s=1.0, inter_token_s=1.0, total_s=5.0 ) assert await _collect(wrapped) == [] class TestThreeLayers: async def test_ttft_timeout(self): wrapped = stream_with_liveness_timeouts( _emit(["a"], first_delay_s=0.2), ttft_s=0.05, inter_token_s=1.0, total_s=5.0 ) with pytest.raises(StreamLivenessTimeout) as ei: await _collect(wrapped) assert ei.value.kind == "ttft" assert ei.value.first_token_seen is False async def test_inter_token_timeout(self): wrapped = stream_with_liveness_timeouts( _emit(["a", "b"], delay_s=0.2, first_delay_s=0.0), ttft_s=1.0, inter_token_s=0.05, total_s=5.0, ) with pytest.raises(StreamLivenessTimeout) as ei: await _collect(wrapped) assert ei.value.kind == "inter_token" assert ei.value.first_token_seen is True async def test_total_timeout(self): # 每项都快,但项数多到总时长超限 → 判 total 而非 inter_token wrapped = stream_with_liveness_timeouts( _emit(list(range(100)), delay_s=0.02), ttft_s=1.0, inter_token_s=1.0, total_s=0.1 ) with pytest.raises(StreamLivenessTimeout) as ei: await _collect(wrapped) assert ei.value.kind == "total" class TestForeignTimeoutAndCleanup: async def test_upstream_timeout_error_not_reclassified(self): """上游自抛的 TimeoutError 不得被误判为本层活性超时。""" async def bad_stream(): raise TimeoutError("upstream own timeout") yield # pragma: no cover wrapped = stream_with_liveness_timeouts( bad_stream(), ttft_s=1.0, inter_token_s=1.0, total_s=5.0 ) with pytest.raises(TimeoutError) as ei: await _collect(wrapped) assert not isinstance(ei.value, StreamLivenessTimeout) async def test_underlying_iterator_closed_on_abandon(self): closed = asyncio.Event() async def tracked(): try: for i in range(100): await asyncio.sleep(0.01) yield i finally: closed.set() wrapped = stream_with_liveness_timeouts( tracked(), ttft_s=1.0, inter_token_s=1.0, total_s=5.0 ) assert (await wrapped.__anext__()) == 0 await wrapped.aclose() # 消费方提前放弃 → finally 关闭底层迭代器 assert closed.is_set() async def test_cancellation_propagates(self): wrapped = stream_with_liveness_timeouts( _emit(["a"], first_delay_s=10.0), ttft_s=30.0, inter_token_s=30.0, total_s=60.0 ) task = asyncio.ensure_future(_collect(wrapped)) await asyncio.sleep(0.05) task.cancel() with pytest.raises(asyncio.CancelledError): await task