You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

573 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# P2P Network Communication - Image Processor Tests
"""
图片处理模块测试
测试图片格式检测、缩略图生成和图片压缩功能
需求: 5.1, 5.5
"""
import io
import os
import tempfile
import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
# 尝试导入PIL如果不可用则跳过测试
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
from client.image_processor import (
ImageProcessor,
ImageFormat,
ImageInfo,
ImageProcessorError,
UnsupportedFormatError,
ImageNotFoundError,
ThumbnailGenerationError,
CompressionError,
IMAGE_SIGNATURES,
FORMAT_EXTENSIONS,
)
from config import ClientConfig
# 如果PIL不可用跳过所有测试
pytestmark = pytest.mark.skipif(not PIL_AVAILABLE, reason="Pillow not installed")
class TestImageProcessorInit:
"""图片处理器初始化测试"""
def test_init_with_default_config(self):
"""测试使用默认配置初始化"""
processor = ImageProcessor()
assert processor.config is not None
assert processor._thumbnail_size == (200, 200)
def test_init_with_custom_config(self):
"""测试使用自定义配置初始化"""
config = ClientConfig(thumbnail_size=(100, 100))
processor = ImageProcessor(config=config)
assert processor._thumbnail_size == (100, 100)
def test_supported_formats(self):
"""测试支持的格式列表"""
processor = ImageProcessor()
assert ImageFormat.JPEG in processor.SUPPORTED_FORMATS
assert ImageFormat.PNG in processor.SUPPORTED_FORMATS
assert ImageFormat.GIF in processor.SUPPORTED_FORMATS
assert ImageFormat.BMP in processor.SUPPORTED_FORMATS
class TestFormatDetection:
"""图片格式检测测试"""
def setup_method(self):
"""每个测试前创建处理器"""
self.processor = ImageProcessor()
self.temp_files = []
def teardown_method(self):
"""每个测试后清理临时文件"""
for f in self.temp_files:
if os.path.exists(f):
os.unlink(f)
def _create_test_image(self, format: str, size: tuple = (100, 100)) -> str:
"""创建测试图片"""
img = Image.new('RGB', size, color='red')
with tempfile.NamedTemporaryFile(suffix=f'.{format.lower()}', delete=False) as f:
temp_path = f.name
img.save(temp_path, format)
self.temp_files.append(temp_path)
return temp_path
def test_detect_jpeg_format(self):
"""测试检测JPEG格式"""
temp_path = self._create_test_image('JPEG')
fmt = self.processor.detect_format(temp_path)
assert fmt == ImageFormat.JPEG
def test_detect_png_format(self):
"""测试检测PNG格式"""
temp_path = self._create_test_image('PNG')
fmt = self.processor.detect_format(temp_path)
assert fmt == ImageFormat.PNG
def test_detect_gif_format(self):
"""测试检测GIF格式"""
temp_path = self._create_test_image('GIF')
fmt = self.processor.detect_format(temp_path)
assert fmt == ImageFormat.GIF
def test_detect_bmp_format(self):
"""测试检测BMP格式"""
temp_path = self._create_test_image('BMP')
fmt = self.processor.detect_format(temp_path)
assert fmt == ImageFormat.BMP
def test_detect_format_nonexistent_file(self):
"""测试检测不存在的文件"""
with pytest.raises(ImageNotFoundError):
self.processor.detect_format("/nonexistent/image.jpg")
def test_detect_format_from_extension(self):
"""测试通过扩展名检测格式"""
assert self.processor.detect_format_from_extension("test.jpg") == ImageFormat.JPEG
assert self.processor.detect_format_from_extension("test.jpeg") == ImageFormat.JPEG
assert self.processor.detect_format_from_extension("test.png") == ImageFormat.PNG
assert self.processor.detect_format_from_extension("test.gif") == ImageFormat.GIF
assert self.processor.detect_format_from_extension("test.bmp") == ImageFormat.BMP
assert self.processor.detect_format_from_extension("test.txt") == ImageFormat.UNKNOWN
def test_is_supported_format(self):
"""测试检查是否为支持的格式"""
temp_path = self._create_test_image('JPEG')
assert self.processor.is_supported_format(temp_path) is True
assert self.processor.is_supported_format("/nonexistent/file.jpg") is False
def test_get_image_info(self):
"""测试获取图片信息"""
temp_path = self._create_test_image('JPEG', size=(200, 150))
info = self.processor.get_image_info(temp_path)
assert info.format == ImageFormat.JPEG
assert info.width == 200
assert info.height == 150
assert info.mode == 'RGB'
assert info.file_size > 0
def test_get_image_info_nonexistent(self):
"""测试获取不存在文件的信息"""
with pytest.raises(ImageNotFoundError):
self.processor.get_image_info("/nonexistent/image.jpg")
class TestThumbnailGeneration:
"""缩略图生成测试"""
def setup_method(self):
"""每个测试前创建处理器"""
self.processor = ImageProcessor()
self.temp_files = []
def teardown_method(self):
"""每个测试后清理临时文件"""
for f in self.temp_files:
if os.path.exists(f):
os.unlink(f)
# 清理缓存
self.processor.clear_cache()
def _create_test_image(self, format: str, size: tuple = (800, 600)) -> str:
"""创建测试图片"""
img = Image.new('RGB', size, color='blue')
with tempfile.NamedTemporaryFile(suffix=f'.{format.lower()}', delete=False) as f:
temp_path = f.name
img.save(temp_path, format)
self.temp_files.append(temp_path)
return temp_path
def test_generate_thumbnail_default_size(self):
"""测试生成默认大小的缩略图"""
temp_path = self._create_test_image('JPEG')
thumb_path = self.processor.generate_thumbnail(temp_path)
self.temp_files.append(thumb_path)
assert os.path.exists(thumb_path)
with Image.open(thumb_path) as thumb:
assert thumb.width <= 200
assert thumb.height <= 200
def test_generate_thumbnail_custom_size(self):
"""测试生成自定义大小的缩略图"""
temp_path = self._create_test_image('JPEG')
thumb_path = self.processor.generate_thumbnail(temp_path, size=(100, 100))
self.temp_files.append(thumb_path)
with Image.open(thumb_path) as thumb:
assert thumb.width <= 100
assert thumb.height <= 100
def test_generate_thumbnail_preserve_aspect_ratio(self):
"""测试保持宽高比生成缩略图"""
temp_path = self._create_test_image('JPEG', size=(800, 400))
thumb_path = self.processor.generate_thumbnail(
temp_path,
size=(200, 200),
preserve_aspect_ratio=True
)
self.temp_files.append(thumb_path)
with Image.open(thumb_path) as thumb:
# 宽高比应该接近2:1
ratio = thumb.width / thumb.height
assert 1.9 <= ratio <= 2.1
def test_generate_thumbnail_custom_output_path(self):
"""测试指定输出路径生成缩略图"""
temp_path = self._create_test_image('JPEG')
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
output_path = f.name
self.temp_files.append(output_path)
result_path = self.processor.generate_thumbnail(temp_path, output_path=output_path)
assert result_path == output_path
assert os.path.exists(output_path)
def test_generate_thumbnail_nonexistent_file(self):
"""测试为不存在的文件生成缩略图"""
with pytest.raises(ImageNotFoundError):
self.processor.generate_thumbnail("/nonexistent/image.jpg")
def test_generate_thumbnail_bytes(self):
"""测试生成缩略图字节数据"""
temp_path = self._create_test_image('JPEG')
thumb_bytes = self.processor.generate_thumbnail_bytes(temp_path)
assert isinstance(thumb_bytes, bytes)
assert len(thumb_bytes) > 0
# 验证是有效的JPEG
with Image.open(io.BytesIO(thumb_bytes)) as img:
assert img.format == 'JPEG'
def test_generate_thumbnail_rgba_image(self):
"""测试为RGBA图片生成缩略图"""
# 创建RGBA图片
img = Image.new('RGBA', (400, 300), color=(255, 0, 0, 128))
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as f:
temp_path = f.name
img.save(temp_path, 'PNG')
self.temp_files.append(temp_path)
thumb_path = self.processor.generate_thumbnail(temp_path)
self.temp_files.append(thumb_path)
assert os.path.exists(thumb_path)
class TestImageCompression:
"""图片压缩测试"""
def setup_method(self):
"""每个测试前创建处理器"""
self.processor = ImageProcessor()
self.temp_files = []
def teardown_method(self):
"""每个测试后清理临时文件"""
for f in self.temp_files:
if os.path.exists(f):
os.unlink(f)
self.processor.clear_cache()
def _create_test_image(self, size: tuple = (1000, 800)) -> str:
"""创建测试图片"""
# 创建有内容的图片以便压缩有效果
img = Image.new('RGB', size)
pixels = img.load()
for i in range(size[0]):
for j in range(size[1]):
pixels[i, j] = ((i * j) % 256, (i + j) % 256, (i - j) % 256)
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
temp_path = f.name
img.save(temp_path, 'JPEG', quality=100)
self.temp_files.append(temp_path)
return temp_path
def test_compress_image_default_quality(self):
"""测试默认质量压缩"""
temp_path = self._create_test_image()
original_size = os.path.getsize(temp_path)
compressed_path = self.processor.compress_image(temp_path)
self.temp_files.append(compressed_path)
assert os.path.exists(compressed_path)
compressed_size = os.path.getsize(compressed_path)
# 压缩后应该更小
assert compressed_size <= original_size
def test_compress_image_low_quality(self):
"""测试低质量压缩"""
temp_path = self._create_test_image()
compressed_path = self.processor.compress_image(temp_path, quality=30)
self.temp_files.append(compressed_path)
assert os.path.exists(compressed_path)
def test_compress_image_with_max_size(self):
"""测试带最大尺寸的压缩"""
temp_path = self._create_test_image(size=(2000, 1500))
compressed_path = self.processor.compress_image(
temp_path,
max_size=(800, 600)
)
self.temp_files.append(compressed_path)
with Image.open(compressed_path) as img:
assert img.width <= 800
assert img.height <= 600
def test_compress_image_custom_output_path(self):
"""测试指定输出路径压缩"""
temp_path = self._create_test_image()
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
output_path = f.name
self.temp_files.append(output_path)
result_path = self.processor.compress_image(temp_path, output_path=output_path)
assert result_path == output_path
assert os.path.exists(output_path)
def test_compress_image_nonexistent_file(self):
"""测试压缩不存在的文件"""
with pytest.raises(ImageNotFoundError):
self.processor.compress_image("/nonexistent/image.jpg")
def test_compress_image_bytes(self):
"""测试压缩图片字节数据"""
temp_path = self._create_test_image()
with open(temp_path, 'rb') as f:
original_bytes = f.read()
compressed_bytes = self.processor.compress_image_bytes(original_bytes, quality=50)
assert isinstance(compressed_bytes, bytes)
assert len(compressed_bytes) < len(original_bytes)
def test_should_compress(self):
"""测试判断是否需要压缩"""
# 创建小文件
small_img = Image.new('RGB', (10, 10), color='red')
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
small_path = f.name
small_img.save(small_path, 'JPEG')
self.temp_files.append(small_path)
assert self.processor.should_compress(small_path) is False
assert self.processor.should_compress("/nonexistent/file.jpg") is False
class TestImageOperations:
"""图片操作测试"""
def setup_method(self):
"""每个测试前创建处理器"""
self.processor = ImageProcessor()
self.temp_files = []
def teardown_method(self):
"""每个测试后清理临时文件"""
for f in self.temp_files:
if os.path.exists(f):
os.unlink(f)
self.processor.clear_cache()
def _create_test_image(self, size: tuple = (400, 300)) -> str:
"""创建测试图片"""
img = Image.new('RGB', size, color='green')
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
temp_path = f.name
img.save(temp_path, 'JPEG')
self.temp_files.append(temp_path)
return temp_path
def test_rotate_image_90_degrees(self):
"""测试旋转90度"""
temp_path = self._create_test_image(size=(400, 200))
rotated_path = self.processor.rotate_image(temp_path, 90)
self.temp_files.append(rotated_path)
with Image.open(rotated_path) as img:
# 旋转90度后宽高交换
assert img.height >= 400 or img.width >= 200
def test_rotate_image_nonexistent_file(self):
"""测试旋转不存在的文件"""
with pytest.raises(ImageNotFoundError):
self.processor.rotate_image("/nonexistent/image.jpg", 90)
def test_resize_image(self):
"""测试调整图片大小"""
temp_path = self._create_test_image(size=(800, 600))
resized_path = self.processor.resize_image(temp_path, (400, 300))
self.temp_files.append(resized_path)
with Image.open(resized_path) as img:
assert img.width <= 400
assert img.height <= 300
def test_resize_image_nonexistent_file(self):
"""测试调整不存在文件的大小"""
with pytest.raises(ImageNotFoundError):
self.processor.resize_image("/nonexistent/image.jpg", (100, 100))
class TestImageInfo:
"""ImageInfo数据类测试"""
def test_image_info_creation(self):
"""测试创建ImageInfo"""
info = ImageInfo(
path="/path/to/image.jpg",
format=ImageFormat.JPEG,
width=800,
height=600,
file_size=100000,
mode="RGB"
)
assert info.path == "/path/to/image.jpg"
assert info.format == ImageFormat.JPEG
assert info.width == 800
assert info.height == 600
def test_aspect_ratio(self):
"""测试宽高比计算"""
info = ImageInfo(
path="/path/to/image.jpg",
format=ImageFormat.JPEG,
width=800,
height=400,
file_size=100000,
mode="RGB"
)
assert info.aspect_ratio == 2.0
def test_is_large(self):
"""测试大图片判断"""
small_info = ImageInfo(
path="/path/to/small.jpg",
format=ImageFormat.JPEG,
width=100,
height=100,
file_size=1000,
mode="RGB"
)
large_info = ImageInfo(
path="/path/to/large.jpg",
format=ImageFormat.JPEG,
width=4000,
height=3000,
file_size=6 * 1024 * 1024, # 6MB
mode="RGB"
)
assert small_info.is_large is False
assert large_info.is_large is True
def test_to_dict(self):
"""测试转换为字典"""
info = ImageInfo(
path="/path/to/image.jpg",
format=ImageFormat.JPEG,
width=800,
height=600,
file_size=100000,
mode="RGB"
)
d = info.to_dict()
assert d["path"] == "/path/to/image.jpg"
assert d["format"] == "jpeg"
assert d["width"] == 800
assert d["height"] == 600
class TestUtilityMethods:
"""工具方法测试"""
def setup_method(self):
"""每个测试前创建处理器"""
self.processor = ImageProcessor()
def teardown_method(self):
"""每个测试后清理"""
self.processor.clear_cache()
def test_get_supported_formats(self):
"""测试获取支持的格式"""
formats = self.processor.get_supported_formats()
assert "jpeg" in formats
assert "png" in formats
assert "gif" in formats
assert "bmp" in formats
def test_get_supported_extensions(self):
"""测试获取支持的扩展名"""
extensions = self.processor.get_supported_extensions()
assert ".jpg" in extensions
assert ".jpeg" in extensions
assert ".png" in extensions
assert ".gif" in extensions
assert ".bmp" in extensions
def test_clear_cache(self):
"""测试清理缓存"""
# 创建一些缓存文件
cache_dir = self.processor._cache_dir
cache_dir.mkdir(parents=True, exist_ok=True)
test_files = [
cache_dir / "test_thumb.jpg",
cache_dir / "test_compressed.jpg",
]
for f in test_files:
f.touch()
count = self.processor.clear_cache()
assert count >= 2
for f in test_files:
assert not f.exists()