Files
capcut-mate/tests/test_add_masks.py
2026-03-28 09:57:14 +08:00

547 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试 add_masks 功能的单元测试
包含正常场景、边界场景和异常场景的完整测试用例
"""
import unittest
from unittest.mock import Mock, MagicMock, patch
from typing import List, Tuple
import sys
import os
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.service.add_masks import (
add_masks,
add_mask_to_segment,
calculate_mask_size_params,
find_segment_by_id,
find_mask_type_by_name
)
from src.pyJianYingDraft import ScriptFile, MaskType
from src.pyJianYingDraft.video_segment import VideoSegment
from exceptions import CustomException, CustomError
class TestCalculateMaskSizeParams(unittest.TestCase):
"""测试遮罩尺寸参数计算函数"""
def test_circle_mask_size_calculation(self):
"""测试圆形遮罩尺寸计算(非矩形)"""
# 准备测试数据:素材尺寸 1920x1080遮罩尺寸 512x512
mask_type = MaskType.圆形
width = 512
height = 512
material_width = 1920
material_height = 1080
# 调用函数
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
# 验证结果
expected_size = height / material_height # 512/1080 ≈ 0.474
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width, "非矩形遮罩的 rect_width 应为 None")
def test_rectangle_mask_size_calculation(self):
"""测试矩形遮罩尺寸计算"""
# 准备测试数据
mask_type = MaskType.矩形
width = 800
height = 600
material_width = 1920
material_height = 1080
# 调用函数
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
# 验证结果
expected_size = height / material_height # 600/1080 ≈ 0.556
expected_rect_width = width / material_width # 800/1920 ≈ 0.417
self.assertAlmostEqual(size, expected_size, places=4)
self.assertAlmostEqual(rect_width, expected_rect_width, places=4)
def test_heart_mask_size_calculation(self):
"""测试爱心遮罩尺寸计算"""
mask_type = MaskType.爱心
width = 400
height = 400
material_width = 1920
material_height = 1080
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
expected_size = height / material_height
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width)
def test_star_mask_size_calculation(self):
"""测试星形遮罩尺寸计算"""
mask_type = MaskType.星形
width = 300
height = 300
material_width = 1280
material_height = 720
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
expected_size = height / material_height # 300/720 ≈ 0.417
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width)
def test_linear_mask_size_calculation(self):
"""测试线性遮罩尺寸计算"""
mask_type = MaskType.线性
width = 1000
height = 1000
material_width = 1920
material_height = 1080
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
expected_size = height / material_height
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width)
def test_mirror_mask_size_calculation(self):
"""测试镜面遮罩尺寸计算"""
mask_type = MaskType.镜面
width = 600
height = 800
material_width = 1920
material_height = 1080
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
expected_size = height / material_height
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width)
def test_zero_dimensions(self):
"""测试零尺寸的边界情况"""
mask_type = MaskType.圆形
width = 0
height = 0
material_width = 1920
material_height = 1080
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
self.assertEqual(size, 0.0)
self.assertIsNone(rect_width)
def test_large_dimensions(self):
"""测试大尺寸的边界情况"""
mask_type = MaskType.圆形
width = 4096
height = 4096
material_width = 1920
material_height = 1080
size, rect_width = calculate_mask_size_params(
mask_type=mask_type,
width=width,
height=height,
material_width=material_width,
material_height=material_height
)
expected_size = height / material_height # 4096/1080 ≈ 3.793
self.assertGreater(size, 1.0, "当遮罩尺寸大于素材时size 应大于 1")
self.assertAlmostEqual(size, expected_size, places=4)
self.assertIsNone(rect_width)
class TestFindMaskTypeByName(unittest.TestCase):
"""测试遮罩类型查找函数"""
def test_find_circle_mask(self):
"""测试查找圆形遮罩"""
result = find_mask_type_by_name("圆形")
self.assertEqual(result, MaskType.圆形)
def test_find_rectangle_mask(self):
"""测试查找矩形遮罩"""
result = find_mask_type_by_name("矩形")
self.assertEqual(result, MaskType.矩形)
def test_find_linear_mask(self):
"""测试查找线性遮罩"""
result = find_mask_type_by_name("线性")
self.assertEqual(result, MaskType.线性)
def test_find_mirror_mask(self):
"""测试查找镜面遮罩"""
result = find_mask_type_by_name("镜面")
self.assertEqual(result, MaskType.镜面)
def test_find_heart_mask(self):
"""测试查找爱心遮罩"""
result = find_mask_type_by_name("爱心")
self.assertEqual(result, MaskType.爱心)
def test_find_star_mask(self):
"""测试查找星形遮罩"""
result = find_mask_type_by_name("星形")
self.assertEqual(result, MaskType.星形)
def test_find_invalid_mask(self):
"""测试查找不存在的遮罩类型"""
result = find_mask_type_by_name("不存在的遮罩")
self.assertIsNone(result)
def test_case_sensitive_search(self):
"""测试大小写敏感性"""
result = find_mask_type_by_name("圆形")
self.assertIsNotNone(result)
# 小写应该找不到
result_lower = find_mask_type_by_name("圆形")
self.assertEqual(result, result_lower)
class TestAddMaskToSegment(unittest.TestCase):
"""测试添加遮罩到片段函数"""
def setUp(self):
"""测试前准备"""
# 创建模拟的 ScriptFile
self.mock_script = Mock(spec=ScriptFile)
self.mock_script.materials = Mock()
self.mock_script.materials.masks = []
self.mock_segment = Mock(spec=VideoSegment)
self.mock_segment.segment_id = "test-segment-id"
self.mock_segment.mask = None # 初始无遮罩
self.mock_segment.material_size = (1920, 1080)
self.mock_segment.add_mask = Mock()
# 模拟轨道和片段查找
self.mock_track = Mock()
self.mock_track.segments = [self.mock_segment]
self.mock_script.tracks = {"video": self.mock_track}
def test_add_circle_mask_success(self):
"""测试成功添加圆形遮罩"""
with patch('src.service.add_masks.find_segment_by_id') as mock_find_segment:
mock_find_segment.return_value = self.mock_segment
# 模拟 add_mask 方法成功执行,设置 mask 属性
def mock_add_mask_impl(*args, **kwargs):
self.mock_segment.mask = Mock()
self.mock_segment.mask.global_id = "generated-mask-id"
return self.mock_segment
self.mock_segment.add_mask = Mock(side_effect=mock_add_mask_impl)
mask_id = add_mask_to_segment(
script=self.mock_script,
segment_id="test-segment-id",
mask_type=MaskType.圆形,
center_x=100,
center_y=200,
width=512,
height=512,
feather=20,
rotation=0,
invert=False,
round_corner=0
)
# 验证调用了 add_mask 方法
self.mock_segment.add_mask.assert_called_once()
call_args = self.mock_segment.add_mask.call_args
self.assertEqual(call_args.kwargs['mask_type'], MaskType.圆形)
self.assertEqual(call_args.kwargs['center_x'], 100.0)
self.assertEqual(call_args.kwargs['center_y'], 200.0)
self.assertEqual(call_args.kwargs['feather'], 20.0)
self.assertEqual(call_args.kwargs['rotation'], 0.0)
self.assertFalse(call_args.kwargs['invert'])
# 验证返回值
self.assertIsInstance(mask_id, str)
self.assertTrue(len(mask_id) > 0)
def test_add_rectangle_mask_with_all_params(self):
"""测试添加矩形遮罩(使用所有参数)"""
with patch('src.service.add_masks.find_segment_by_id') as mock_find_segment:
mock_find_segment.return_value = self.mock_segment
# 模拟 add_mask 方法成功执行,设置 mask 属性
def mock_add_mask_impl(*args, **kwargs):
self.mock_segment.mask = Mock()
self.mock_segment.mask.global_id = "rectangle-mask-id"
return self.mock_segment
self.mock_segment.add_mask = Mock(side_effect=mock_add_mask_impl)
mask_id = add_mask_to_segment(
script=self.mock_script,
segment_id="test-segment-id",
mask_type=MaskType.矩形,
center_x=50,
center_y=100,
width=800,
height=600,
feather=30,
rotation=45,
invert=True,
round_corner=25
)
# 验证调用了 add_mask 方法且包含所有参数
self.mock_segment.add_mask.assert_called_once()
call_args = self.mock_segment.add_mask.call_args
self.assertEqual(call_args.kwargs['mask_type'], MaskType.矩形)
self.assertIn('rect_width', call_args.kwargs)
self.assertIn('round_corner', call_args.kwargs)
def test_segment_not_found(self):
"""测试片段未找到的异常情况"""
with patch('src.service.add_masks.find_segment_by_id') as mock_find_segment:
mock_find_segment.return_value = None
with self.assertRaises(CustomException) as context:
add_mask_to_segment(
script=self.mock_script,
segment_id="non-existent-segment",
mask_type=MaskType.圆形
)
self.assertEqual(context.exception.err, CustomError.SEGMENT_NOT_FOUND)
def test_non_video_segment(self):
"""测试非视频片段类型异常"""
with patch('src.service.add_masks.find_segment_by_id') as mock_find_segment:
mock_non_video_segment = Mock()
mock_non_video_segment.segment_id = "audio-segment"
mock_find_segment.return_value = mock_non_video_segment
with self.assertRaises(CustomException) as context:
add_mask_to_segment(
script=self.mock_script,
segment_id="audio-segment",
mask_type=MaskType.圆形
)
self.assertEqual(context.exception.err, CustomError.INVALID_SEGMENT_TYPE)
@patch('src.service.add_masks.find_segment_by_id')
def test_segment_already_has_mask(self, mock_find_segment):
"""测试片段已有遮罩的情况"""
mock_existing_mask = Mock()
mock_existing_mask.global_id = "existing-mask-id"
self.mock_segment.mask = mock_existing_mask
mock_find_segment.return_value = self.mock_segment
# 应该返回现有遮罩 ID 而不是抛出异常
mask_id = add_mask_to_segment(
script=self.mock_script,
segment_id="test-segment-id",
mask_type=MaskType.圆形
)
self.assertEqual(mask_id, "existing-mask-id")
# 不应调用 add_mask
self.mock_segment.add_mask.assert_not_called()
class TestAddMasksIntegration(unittest.TestCase):
"""测试 add_masks 集成函数"""
@patch('src.service.add_masks.DRAFT_CACHE')
@patch('src.service.add_masks.helper.get_url_param')
@patch('src.service.add_masks.find_mask_type_by_name')
@patch('src.service.add_masks.add_mask_to_segment')
def test_add_single_circle_mask(self, mock_add_mask, mock_find_mask_type, mock_get_param, mock_cache):
"""测试添加单个圆形遮罩的正常场景"""
# 设置模拟返回值
mock_get_param.return_value = "test-draft-id"
mock_find_mask_type.return_value = MaskType.圆形
mock_add_mask.return_value = "generated-mask-id"
# 创建模拟草稿
mock_script = Mock(spec=ScriptFile)
mock_script.save = Mock()
# 正确设置 DRAFT_CACHE 的模拟
mock_cache.__contains__ = Mock(return_value=True)
mock_cache.__getitem__ = Mock(return_value=mock_script)
# 调用函数
draft_url = "https://example.com/draft?draft_id=test-draft-id"
segment_ids = ["segment-1"]
result = add_masks(
draft_url=draft_url,
segment_ids=segment_ids,
name="圆形",
X=100,
Y=200,
width=512,
height=512,
feather=20,
rotation=0,
invert=False,
roundCorner=0
)
# 验证结果
self.assertEqual(result[0], draft_url)
self.assertEqual(result[1], 1) # masks_added
self.assertEqual(result[2], segment_ids) # affected_segments
self.assertEqual(result[3], ["generated-mask-id"]) # mask_ids
# 验证保存了草稿
mock_script.save.assert_called_once()
@patch('src.service.add_masks.DRAFT_CACHE')
@patch('src.service.add_masks.helper.get_url_param')
@patch('src.service.add_masks.find_mask_type_by_name')
@patch('src.service.add_masks.add_mask_to_segment')
def test_add_multiple_masks(self, mock_add_mask, mock_find_mask_type, mock_get_param, mock_cache):
"""测试批量添加多个遮罩"""
mock_get_param.return_value = "test-draft-id"
mock_find_mask_type.return_value = MaskType.圆形
mock_add_mask.side_effect = ["mask-id-1", "mask-id-2", "mask-id-3"]
mock_script = Mock(spec=ScriptFile)
mock_script.save = Mock()
# 正确设置 DRAFT_CACHE 的模拟
mock_cache.__contains__ = Mock(return_value=True)
mock_cache.__getitem__ = Mock(return_value=mock_script)
draft_url = "https://example.com/draft?draft_id=test-draft-id"
segment_ids = ["segment-1", "segment-2", "segment-3"]
result = add_masks(
draft_url=draft_url,
segment_ids=segment_ids,
name="圆形"
)
self.assertEqual(result[1], 3) # masks_added
self.assertEqual(len(result[2]), 3) # affected_segments
self.assertEqual(len(result[3]), 3) # mask_ids
self.assertEqual(mock_add_mask.call_count, 3)
@patch('src.service.add_masks.helper.get_url_param')
@patch('src.service.add_masks.DRAFT_CACHE')
def test_invalid_draft_url(self, mock_cache, mock_get_param):
"""测试无效的 draft_url 异常"""
mock_get_param.return_value = None
with self.assertRaises(CustomException) as context:
add_masks(
draft_url="invalid-url",
segment_ids=["segment-1"]
)
self.assertEqual(context.exception.err, CustomError.INVALID_DRAFT_URL)
@patch('src.service.add_masks.DRAFT_CACHE')
@patch('src.service.add_masks.helper.get_url_param')
def test_empty_segment_ids(self, mock_get_param, mock_cache):
"""测试空的 segment_ids 异常"""
mock_get_param.return_value = "test-draft-id"
# 设置缓存,这样会通过第一个检查
mock_cache.__contains__ = Mock(return_value=True)
with self.assertRaises(CustomException) as context:
add_masks(
draft_url="https://example.com/draft?draft_id=test-draft-id",
segment_ids=[]
)
self.assertEqual(context.exception.err, CustomError.INVALID_MASK_INFO)
@patch('src.service.add_masks.DRAFT_CACHE')
@patch('src.service.add_masks.helper.get_url_param')
@patch('src.service.add_masks.find_mask_type_by_name')
def test_invalid_mask_type(self, mock_find_mask_type, mock_get_param, mock_cache):
"""测试无效的遮罩类型异常"""
mock_get_param.return_value = "test-draft-id"
mock_cache.__contains__ = Mock(return_value=True)
mock_find_mask_type.return_value = None
with self.assertRaises(CustomException) as context:
add_masks(
draft_url="https://example.com/draft?draft_id=test-draft-id",
segment_ids=["segment-1"],
name="无效遮罩类型"
)
self.assertEqual(context.exception.err, CustomError.MASK_NOT_FOUND)
@patch('src.service.add_masks.DRAFT_CACHE')
@patch('src.service.add_masks.helper.get_url_param')
@patch('src.service.add_masks.find_mask_type_by_name')
@patch('src.service.add_masks.add_mask_to_segment')
def test_partial_failure_handling(self, mock_add_mask, mock_find_mask_type, mock_get_param, mock_cache):
"""测试部分失败的处理"""
mock_get_param.return_value = "test-draft-id"
mock_find_mask_type.return_value = MaskType.圆形
# 第一个成功,第二个失败
mock_add_mask.side_effect = Exception("添加失败")
mock_script = Mock(spec=ScriptFile)
mock_script.save = Mock()
mock_cache.__getitem__.return_value = mock_script
with self.assertRaises(Exception):
add_masks(
draft_url="https://example.com/draft?draft_id=test-draft-id",
segment_ids=["segment-1", "segment-2"]
)
if __name__ == '__main__':
unittest.main()