Files
2026-10-15 08:26:22 +00:00

97 lines
3.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import asyncio
from unittest.mock import MagicMock
import pytest
from vllm.renderers.chat_utils import AsyncMultiModalItemTracker
@pytest.mark.asyncio
async def test_resolve_items_runs_modalities_concurrently_and_preserves_order():
"""Media fetches overlap while modality and item order are preserved."""
active_fetches = 0
max_active_fetches = 0
async def _fetch(name: str, delay: float):
nonlocal active_fetches, max_active_fetches
active_fetches += 1
max_active_fetches = max(max_active_fetches, active_fetches)
try:
await asyncio.sleep(delay)
return name, f"{name}-uuid"
finally:
active_fetches -= 1
tracker = AsyncMultiModalItemTracker(MagicMock(is_multimodal_model=True))
tracker.__dict__["mm_processor"] = MagicMock()
tracker._items_by_modality["video"] = [
lambda: _fetch("video-0", 0.02),
lambda: _fetch("video-1", 0.01),
]
tracker._items_by_modality["image"] = [
lambda: _fetch("image-0", 0.02),
lambda: _fetch("image-1", 0.01),
]
tracker._items_by_modality["audio"] = [
lambda: _fetch("audio-0", 0.02),
lambda: _fetch("audio-1", 0.01),
]
mm_data, mm_uuids = await tracker.resolve_items()
assert mm_data is not None
assert mm_uuids is not None
assert max_active_fetches == 6
assert list(mm_data) == ["image", "audio", "video"]
assert list(mm_uuids) == ["image", "audio", "video"]
assert mm_data == {
"image": ["image-0", "image-1"],
"audio": ["audio-0", "audio-1"],
"video": ["video-0", "video-1"],
}
assert mm_uuids == {
"image": ["image-0-uuid", "image-1-uuid"],
"audio": ["audio-0-uuid", "audio-1-uuid"],
"video": ["video-0-uuid", "video-1-uuid"],
}
@pytest.mark.asyncio
async def test_resolve_items_does_not_leak_tasks_on_partial_failure():
"""Regression test: one failing media fetch must not abandon the other
still-in-flight fetches across modality batches.
Before the fix, `resolve_items` gathered per-modality fetches with plain
`asyncio.gather`, so the first exception propagated immediately while
sibling fetches (real network/thread-pool work in production) kept
running detached, with nothing left to await or cancel them.
"""
async def _fetch(should_fail: bool, delay: float):
if should_fail:
await asyncio.sleep(0.01)
raise ValueError("simulated fetch failure")
await asyncio.sleep(delay)
return ("decoded", None)
tracker = AsyncMultiModalItemTracker(MagicMock())
tracker._items_by_modality["image"] = [
lambda: _fetch(True, 0),
lambda: _fetch(False, 0.2),
lambda: _fetch(False, 0.2),
]
tracker._items_by_modality["audio"] = [lambda: _fetch(False, 0.2)]
tracker._items_by_modality["video"] = [lambda: _fetch(False, 0.2)]
tasks_before = asyncio.all_tasks()
with pytest.raises(ValueError, match="simulated fetch failure"):
await tracker.resolve_items()
leaked_tasks = asyncio.all_tasks() - tasks_before
assert not leaked_tasks, (
f"resolve_items left {len(leaked_tasks)} task(s) running after "
f"raising: {leaked_tasks}"
)