📝 新增完整项目文档、测试套件与构建配置

This commit is contained in:
2026-02-11 22:00:05 +08:00
parent 121c165d62
commit 04dfc9e8e0
10 changed files with 1032 additions and 85 deletions

49
tests/README.md Normal file
View File

@@ -0,0 +1,49 @@
# 测试
此目录包含 pptopic 库的测试文件。
## 运行测试
运行所有测试:
```bash
pytest
```
运行测试并生成覆盖率报告:
```bash
pytest --cov=pptopic --cov-report=html
```
运行特定测试文件:
```bash
pytest tests/test_convert.py
```
运行特定测试类:
```bash
pytest tests/test_convert.py::TestConvertPPTClass
```
运行特定测试函数:
```bash
pytest tests/test_convert.py::TestConvertPPTClass::test_init_with_valid_file
```
## 测试结构
- `test_convert.py` - `convertPPT` 类的测试
- `test_longpic.py` - `PPT_longPic` 函数的测试
- `test_optimization.py` - `imageOptimization` 函数的测试
## 测试标记
测试可以使用以下标记:
- `slow` - 慢速测试(可以使用 `-m "not slow"` 跳过)
- `integration` - 集成测试
- `unit` - 单元测试

3
tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Test suite for pptopic library."""
__version__ = "0.1.0"

150
tests/test_convert.py Normal file
View File

@@ -0,0 +1,150 @@
"""Tests for convertPPT class."""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from pptopic.lib import convertPPT
class TestConvertPPTClass:
"""Test convertPPT class initialization and properties."""
def test_ttypes_mapping(self):
"""Test that TTYPES mapping is correct."""
assert convertPPT.TTYPES["JPG"] == 17
assert convertPPT.TTYPES["PNG"] == 18
assert convertPPT.TTYPES["PDF"] == 32
assert convertPPT.TTYPES["XPS"] == 33
@patch('pptopic.lib.sys.platform', 'win32')
def test_init_with_valid_file(self, tmp_path):
"""Test initialization with a valid file path."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch') as mock_dispatch:
mock_app = MagicMock()
mock_dispatch.return_value = mock_app
ppt = convertPPT(test_file, trans="JPG")
assert ppt.savetype == "JPG"
mock_dispatch.assert_called_once_with('PowerPoint.Application')
@patch('pptopic.lib.sys.platform', 'win32')
def test_init_with_path_object(self, tmp_path):
"""Test initialization with Path object."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT(Path(test_file), trans="PNG")
assert ppt.savetype == "PNG"
@patch('pptopic.lib.sys.platform', 'win32')
def test_init_with_nonexistent_file(self):
"""Test initialization with nonexistent file raises FileNotFoundError."""
with pytest.raises(FileNotFoundError, match="File not found"):
convertPPT("nonexistent.pptx")
@patch('pptopic.lib.sys.platform', 'win32')
def test_init_with_invalid_trans_type(self, tmp_path):
"""Test initialization with invalid save type raises ValueError."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with pytest.raises(ValueError, match="Save type is not supported"):
convertPPT(test_file, trans="BMP")
@patch('pptopic.lib.sys.platform', 'win32')
def test_init_with_case_insensitive_trans(self, tmp_path):
"""Test that trans parameter is case insensitive."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT(test_file, trans="jpg")
assert ppt.savetype == "JPG"
ppt = convertPPT(test_file, trans="Pdf")
assert ppt.savetype == "PDF"
class TestConvertPTTNonWindows:
"""Test convertPPT behavior on non-Windows systems."""
@patch('pptopic.lib.sys.platform', 'linux')
def test_init_on_non_windows_raises_error(self):
"""Test that initialization on non-Windows raises SystemError."""
with pytest.raises(SystemError, match="Only support Windows system"):
convertPPT("test.pptx")
class TestConvertPPTContextManager:
"""Test convertPPT as context manager."""
@patch('pptopic.lib.sys.platform', 'win32')
def test_context_manager_enter_exit(self, tmp_path):
"""Test __enter__ and __exit__ methods."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch') as mock_dispatch:
mock_app = MagicMock()
mock_dispatch.return_value = mock_app
with convertPPT(test_file) as ppt:
assert ppt.savetype == "JPG"
mock_app.Quit.assert_called_once()
class TestConvertPPTSaveAs:
"""Test saveAs method."""
@patch('pptopic.lib.sys.platform', 'win32')
def test_saveas_default(self, tmp_path):
"""Test saveAs with default parameter."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT(test_file, trans="PNG")
ppt.saveAs()
assert ppt.savetype == "JPG"
@patch('pptopic.lib.sys.platform', 'win32')
def test_saveas_with_valid_type(self, tmp_path):
"""Test saveAs with valid save type."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT(test_file, trans="JPG")
ppt.saveAs("PDF")
assert ppt.savetype == "PDF"
@patch('pptopic.lib.sys.platform', 'win32')
def test_saveas_with_invalid_type(self, tmp_path):
"""Test saveAs with invalid save type raises ValueError."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT(test_file, trans="JPG")
with pytest.raises(ValueError, match="Save type is not supported"):
ppt.saveAs("BMP")
class TestConvertPTTOpenClassmethod:
"""Test open class method."""
@patch('pptopic.lib.sys.platform', 'win32')
def test_open_classmethod(self, tmp_path):
"""Test open class method creates instance."""
test_file = tmp_path / "test.pptx"
test_file.touch()
with patch('pptopic.lib.wcc.Dispatch'):
ppt = convertPPT.open(test_file, trans="PNG")
assert ppt.savetype == "PNG"
assert isinstance(ppt, convertPPT)

244
tests/test_longpic.py Normal file
View File

@@ -0,0 +1,244 @@
"""Tests for PPT_longPic function."""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from PIL import Image
from pptopic.lib import PPT_longPic
class TestPPTLongPic:
"""Test PPT_longPic function."""
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_save_name(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with saveName parameter."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveName="output.jpg", saveto=tmp_path)
mock_canvas.save.assert_called_once()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_without_save_name(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic without saveName uses default JPG."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=tmp_path)
mock_canvas.save.assert_called_once()
def test_ppt_longpic_invalid_image_type(self, tmp_path):
"""Test PPT_longPic with invalid image type raises ValueError."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
with pytest.raises(ValueError, match="Unable to save this type"):
PPT_longPic(ppt_file, saveName="output.bmp")
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_no_images_generated(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic when no images are generated raises ValueError."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob', return_value=[]):
with pytest.raises(ValueError, match="No images generated"):
PPT_longPic(ppt_file)
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_percentage_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with percentage width string."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width="50%", saveto=tmp_path)
mock_new.assert_called_once()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_pixel_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with pixel width."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width=50, saveto=tmp_path)
mock_new.assert_called_once()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_none_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with None width (no scaling)."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width=None, saveto=tmp_path)
mock_new.assert_called_once()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_save_to_current_dir(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic saving to current directory."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=".")
mock_canvas.save.assert_called_once()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_multiple_images(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with multiple images."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path1 = MagicMock()
mock_img_path1.suffix = '.jpg'
mock_img_path2 = MagicMock()
mock_img_path2.suffix = '.jpg'
mock_glob.return_value = [mock_img_path1, mock_img_path2]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=tmp_path)
mock_canvas.paste.assert_called()

227
tests/test_optimization.py Normal file
View File

@@ -0,0 +1,227 @@
"""Tests for imageOptimization function."""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from PIL import Image
import numpy as np
from pptopic.lib import imageOptimization
class TestImageOptimization:
"""Test imageOptimization function."""
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_with_file_path(self, mock_last_file, mock_subprocess,
mock_resize, mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with file path input."""
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.array([[[255, 0, 0]]])
mock_cvtcolor.return_value = mock_array
mock_resized = np.array([[[255, 0, 0]]])
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png")
assert result is not None
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_with_pil_image(self, mock_last_file, mock_subprocess,
mock_resize, mock_cvtcolor, tmp_path):
"""Test imageOptimization with PIL Image input."""
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.mode = 'RGB'
mock_array = np.array([[[255, 0, 0]]])
mock_cvtcolor.return_value = mock_array
mock_resized = np.array([[[255, 0, 0]]])
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(mock_img)
assert result is not None
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.shutil.copyfile')
def test_image_optimization_with_save_file(self, mock_copyfile, mock_subprocess,
mock_resize, mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with saveFile parameter."""
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.array([[[255, 0, 0]]])
mock_cvtcolor.return_value = mock_array
mock_resized = np.array([[[255, 0, 0]]])
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.lastFile') as mock_last_file:
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png", saveFile=tmp_path / "output.png")
assert result is None
mock_copyfile.assert_called_once()
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_with_max_width(self, mock_last_file, mock_subprocess,
mock_resize, mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with max_width parameter."""
mock_img = MagicMock()
mock_img.size = (200, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.zeros((100, 200, 3), dtype=np.uint8)
mock_cvtcolor.return_value = mock_array
mock_resized = np.zeros((50, 100, 3), dtype=np.uint8)
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png", max_width=100)
mock_resize.assert_called_once()
assert result is not None
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_with_max_height(self, mock_last_file, mock_subprocess,
mock_resize, mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with max_height parameter."""
mock_img = MagicMock()
mock_img.size = (100, 200)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.zeros((200, 100, 3), dtype=np.uint8)
mock_cvtcolor.return_value = mock_array
mock_resized = np.zeros((100, 50, 3), dtype=np.uint8)
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png", max_height=100)
mock_resize.assert_called_once()
assert result is not None
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.cv2.resize')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_with_fractional_width(self, mock_last_file, mock_subprocess,
mock_resize, mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with fractional max_width (< 1)."""
mock_img = MagicMock()
mock_img.size = (200, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.zeros((100, 200, 3), dtype=np.uint8)
mock_cvtcolor.return_value = mock_array
mock_resized = np.zeros((50, 100, 3), dtype=np.uint8)
mock_resize.return_value = mock_resized
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png", max_width=0.5)
mock_resize.assert_called_once()
assert result is not None
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.subprocess.run')
@patch('pptopic.lib.lastFile')
def test_image_optimization_without_engine(self, mock_last_file, mock_subprocess,
mock_cvtcolor, mock_open, tmp_path):
"""Test imageOptimization with engine=None."""
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.array([[[255, 0, 0]]])
mock_cvtcolor.return_value = mock_array
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with patch('pptopic.lib.Image.open', return_value=mock_img):
mock_last_file.return_value = tmp_path / "optimized.png"
result = imageOptimization(tmp_path / "test.png", engine=None)
mock_subprocess.assert_not_called()
assert result is not None
@patch('pptopic.lib.Image.open')
@patch('pptopic.lib.cv2.cvtColor')
@patch('pptopic.lib.subprocess.run')
def test_image_optimization_engine_failure(self, mock_subprocess, mock_cvtcolor, mock_open):
"""Test imageOptimization when engine subprocess fails."""
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.mode = 'RGB'
mock_open.return_value = mock_img
mock_array = np.array([[[255, 0, 0]]])
mock_cvtcolor.return_value = mock_array
mock_subprocess.side_effect = Exception("Engine not found")
mock_pil_img = MagicMock()
with patch('pptopic.lib.Image.fromarray', return_value=mock_pil_img):
with pytest.raises(Exception):
imageOptimization("test.png", engine="pngquant", engine_conf="--quality=85")