Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions tests/helpers/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ def generate_synthetic_image(
"""
Random colored squares on white background. Caches JPEG by ``width`` /
``height`` unless ``force_regenerate`` is true. Cache root: ``cache_dir``
if given, else the default temp subdirectory.
if given, else the default temp subdirectory. Cache files are published atomically.
"""
if seed is not None:
random.seed(seed)
Expand Down Expand Up @@ -556,8 +556,19 @@ def generate_synthetic_image(
border_width = random.randint(1, 5)
draw.rectangle([x, y, x + square_size, y + square_size], fill=color, outline=(0, 0, 0), width=border_width)

image.save(str(cache_path), format="JPEG", quality=85, optimize=True)
image_bytes = cache_path.read_bytes()
buffer = io.BytesIO()
image.save(buffer, format="JPEG", quality=85, optimize=True)
image_bytes = buffer.getvalue()

temp_cache_path = root / f".{cache_path.name}.{uuid.uuid4().hex}.tmp"
# Match video cache publication so concurrent readers only see complete files.
temp_cache = temp_cache_path.open("xb")
try:
with temp_cache:
temp_cache.write(image_bytes)
os.replace(temp_cache_path, cache_path)
finally:
temp_cache_path.unlink(missing_ok=True)

return {
"np_array": np.array(image).copy(),
Expand Down
49 changes: 49 additions & 0 deletions tests/helpers/tests/test_synthetic_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project

import base64
from pathlib import Path

import pytest
from PIL import Image

from tests.helpers import media

pytestmark = [pytest.mark.core_model, pytest.mark.cpu]


def test_image_cache_is_published_atomically(mocker, tmp_path):
original = media.generate_synthetic_image(64, 32, cache_dir=tmp_path, seed=42)
cache_path = Path(original["file_path"])
old_bytes = cache_path.read_bytes()
replace = media.os.replace

def observe_replace(source, destination):
assert Path(destination) == cache_path
assert Path(source).parent == cache_path.parent
# Readers keep the old complete JPEG until the replacement is ready.
assert cache_path.read_bytes() == old_bytes
with Image.open(source) as image:
image.load()
assert image.size == (64, 32)
replace(source, destination)

publish = mocker.patch.object(media.os, "replace", side_effect=observe_replace)
result = media.generate_synthetic_image(64, 32, force_regenerate=True, cache_dir=tmp_path, seed=43)

publish.assert_called_once()
assert base64.b64decode(result["base64"]) == cache_path.read_bytes()
assert list(tmp_path.iterdir()) == [cache_path]


def test_failed_image_cache_publication_preserves_old_file(mocker, tmp_path):
original = media.generate_synthetic_image(64, 32, cache_dir=tmp_path, seed=42)
cache_path = Path(original["file_path"])
old_bytes = cache_path.read_bytes()
mocker.patch.object(media.os, "replace", side_effect=OSError("publication failed"))

with pytest.raises(OSError, match="publication failed"):
media.generate_synthetic_image(64, 32, force_regenerate=True, cache_dir=tmp_path, seed=43)

assert cache_path.read_bytes() == old_bytes
assert list(tmp_path.iterdir()) == [cache_path]
Loading