style: auto-format with black + isort + prettier
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.concat_engine import ConcatConfig, ConcatSegment
|
||||
|
||||
|
||||
@@ -23,50 +22,62 @@ class TestConcatSegmentFromDict:
|
||||
assert seg.has_audio is True
|
||||
|
||||
def test_full_fields(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/b.mp4",
|
||||
"start_time": 5.5,
|
||||
"duration": 10.0,
|
||||
"has_audio": False,
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/b.mp4",
|
||||
"start_time": 5.5,
|
||||
"duration": 10.0,
|
||||
"has_audio": False,
|
||||
}
|
||||
)
|
||||
assert seg.video_path == "/tmp/b.mp4"
|
||||
assert seg.start_time == 5.5
|
||||
assert seg.duration == 10.0
|
||||
assert seg.has_audio is False
|
||||
|
||||
def test_negative_start_time_clamped(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": -1.0,
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": -1.0,
|
||||
}
|
||||
)
|
||||
assert seg.start_time == 0.0
|
||||
|
||||
def test_negative_duration_clamped(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"duration": -5.0,
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"duration": -5.0,
|
||||
}
|
||||
)
|
||||
assert seg.duration == 0.0
|
||||
|
||||
def test_invalid_start_time_type_falls_back(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": "not_a_number",
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": "not_a_number",
|
||||
}
|
||||
)
|
||||
assert seg.start_time == 0.0
|
||||
|
||||
def test_invalid_duration_type_falls_back(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"duration": "abc",
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"duration": "abc",
|
||||
}
|
||||
)
|
||||
assert seg.duration == 0.0
|
||||
|
||||
def test_start_time_none_falls_back(self):
|
||||
seg = ConcatSegment.from_dict({
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": None,
|
||||
})
|
||||
seg = ConcatSegment.from_dict(
|
||||
{
|
||||
"video_path": "/tmp/a.mp4",
|
||||
"start_time": None,
|
||||
}
|
||||
)
|
||||
assert seg.start_time == 0.0
|
||||
|
||||
def test_empty_video_path_stored(self):
|
||||
@@ -94,97 +105,115 @@ class TestConcatConfigFromConfigDict:
|
||||
assert cfg.segments == []
|
||||
|
||||
def test_single_segment(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [
|
||||
{"video_path": "/tmp/a.mp4", "duration": 5.0},
|
||||
],
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [
|
||||
{"video_path": "/tmp/a.mp4", "duration": 5.0},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.segments) == 1
|
||||
assert cfg.segments[0].video_path == "/tmp/a.mp4"
|
||||
assert cfg.segments[0].duration == 5.0
|
||||
|
||||
def test_multiple_segments(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [
|
||||
{"video_path": "/tmp/a.mp4"},
|
||||
{"video_path": "/tmp/b.mp4", "start_time": 2.0},
|
||||
{"video_path": "/tmp/c.mp4", "duration": 3.0, "has_audio": False},
|
||||
],
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [
|
||||
{"video_path": "/tmp/a.mp4"},
|
||||
{"video_path": "/tmp/b.mp4", "start_time": 2.0},
|
||||
{"video_path": "/tmp/c.mp4", "duration": 3.0, "has_audio": False},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.segments) == 3
|
||||
assert cfg.segments[0].video_path == "/tmp/a.mp4"
|
||||
assert cfg.segments[1].start_time == 2.0
|
||||
assert cfg.segments[2].has_audio is False
|
||||
|
||||
def test_invalid_segments_filtered(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [
|
||||
{"video_path": "/tmp/valid.mp4"},
|
||||
{"video_path": ""}, # 空路径被过滤
|
||||
{"not_video_path": "xxx"}, # 没有video_path被过滤
|
||||
"not_a_dict", # 不是dict被过滤
|
||||
None, # None被过滤
|
||||
],
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [
|
||||
{"video_path": "/tmp/valid.mp4"},
|
||||
{"video_path": ""}, # 空路径被过滤
|
||||
{"not_video_path": "xxx"}, # 没有video_path被过滤
|
||||
"not_a_dict", # 不是dict被过滤
|
||||
None, # None被过滤
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.segments) == 1
|
||||
assert cfg.segments[0].video_path == "/tmp/valid.mp4"
|
||||
|
||||
def test_segments_not_a_list(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": "not_a_list",
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": "not_a_list",
|
||||
}
|
||||
)
|
||||
assert cfg.segments == []
|
||||
|
||||
def test_output_params(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [],
|
||||
"output_width": 1920,
|
||||
"output_height": 1080,
|
||||
"output_fps": 30.0,
|
||||
"force_reencode": True,
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [],
|
||||
"output_width": 1920,
|
||||
"output_height": 1080,
|
||||
"output_fps": 30.0,
|
||||
"force_reencode": True,
|
||||
}
|
||||
)
|
||||
assert cfg.output_width == 1920
|
||||
assert cfg.output_height == 1080
|
||||
assert cfg.output_fps == 30.0
|
||||
assert cfg.force_reencode is True
|
||||
|
||||
def test_negative_output_params_clamped(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [],
|
||||
"output_width": -100,
|
||||
"output_height": -50,
|
||||
"output_fps": -1.0,
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [],
|
||||
"output_width": -100,
|
||||
"output_height": -50,
|
||||
"output_fps": -1.0,
|
||||
}
|
||||
)
|
||||
assert cfg.output_width == 0
|
||||
assert cfg.output_height == 0
|
||||
assert cfg.output_fps == 0.0
|
||||
|
||||
def test_invalid_output_params_fall_back(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [],
|
||||
"output_width": "abc",
|
||||
"output_height": None,
|
||||
"output_fps": "xyz",
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [],
|
||||
"output_width": "abc",
|
||||
"output_height": None,
|
||||
"output_fps": "xyz",
|
||||
}
|
||||
)
|
||||
assert cfg.output_width == 0
|
||||
assert cfg.output_height == 0
|
||||
assert cfg.output_fps == 0.0
|
||||
|
||||
def test_transition_config(self):
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [],
|
||||
"transition": "crossfade",
|
||||
"transition_duration": 1.0,
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [],
|
||||
"transition": "crossfade",
|
||||
"transition_duration": 1.0,
|
||||
}
|
||||
)
|
||||
assert cfg.transition == "crossfade"
|
||||
assert cfg.transition_duration == 1.0
|
||||
|
||||
def test_transition_duration_minimum(self):
|
||||
"""transition_duration 不能小于 0.1."""
|
||||
cfg = ConcatConfig.from_config_dict({
|
||||
"segments": [],
|
||||
"transition_duration": 0.01,
|
||||
})
|
||||
cfg = ConcatConfig.from_config_dict(
|
||||
{
|
||||
"segments": [],
|
||||
"transition_duration": 0.01,
|
||||
}
|
||||
)
|
||||
assert cfg.transition_duration >= 0.1
|
||||
|
||||
def test_default_values(self):
|
||||
@@ -198,16 +227,20 @@ class TestConcatConfigProperties:
|
||||
"""has_effect / total_segments 属性."""
|
||||
|
||||
def test_has_effect_two_or_more_valid(self):
|
||||
cfg = ConcatConfig(segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
ConcatSegment(video_path="/tmp/b.mp4"),
|
||||
])
|
||||
cfg = ConcatConfig(
|
||||
segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
ConcatSegment(video_path="/tmp/b.mp4"),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is True
|
||||
|
||||
def test_no_effect_one_segment(self):
|
||||
cfg = ConcatConfig(segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
])
|
||||
cfg = ConcatConfig(
|
||||
segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
|
||||
def test_no_effect_zero_segments(self):
|
||||
@@ -215,18 +248,22 @@ class TestConcatConfigProperties:
|
||||
assert cfg.has_effect is False
|
||||
|
||||
def test_no_effect_empty_paths(self):
|
||||
cfg = ConcatConfig(segments=[
|
||||
ConcatSegment(video_path=""),
|
||||
ConcatSegment(video_path=""),
|
||||
])
|
||||
cfg = ConcatConfig(
|
||||
segments=[
|
||||
ConcatSegment(video_path=""),
|
||||
ConcatSegment(video_path=""),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
|
||||
def test_total_segments(self):
|
||||
cfg = ConcatConfig(segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
ConcatSegment(video_path=""),
|
||||
ConcatSegment(video_path="/tmp/b.mp4"),
|
||||
])
|
||||
cfg = ConcatConfig(
|
||||
segments=[
|
||||
ConcatSegment(video_path="/tmp/a.mp4"),
|
||||
ConcatSegment(video_path=""),
|
||||
ConcatSegment(video_path="/tmp/b.mp4"),
|
||||
]
|
||||
)
|
||||
assert cfg.total_segments == 2
|
||||
|
||||
def test_total_segments_empty(self):
|
||||
|
||||
@@ -6,12 +6,12 @@ import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.domain.config_schemas import (
|
||||
BGMSource,
|
||||
BGMConfig,
|
||||
CoverConfig,
|
||||
CoverType,
|
||||
DEFAULT_EDIT_PLAN_CONFIG,
|
||||
DEFAULT_EDIT_TEMPLATE_CONFIG,
|
||||
BGMConfig,
|
||||
BGMSource,
|
||||
CoverConfig,
|
||||
CoverType,
|
||||
EditPlanConfigSchema,
|
||||
EditTemplateConfigSchema,
|
||||
ExportConfig,
|
||||
@@ -491,10 +491,12 @@ class TestNormalizeTemplateConfig:
|
||||
assert result["transition_enabled"] is True
|
||||
|
||||
def test_updates_sections(self):
|
||||
result = normalize_template_config({
|
||||
"title": {"text": "模板标题"},
|
||||
"bgm": {"enabled": True},
|
||||
})
|
||||
result = normalize_template_config(
|
||||
{
|
||||
"title": {"text": "模板标题"},
|
||||
"bgm": {"enabled": True},
|
||||
}
|
||||
)
|
||||
assert result["title"]["text"] == "模板标题"
|
||||
assert result["bgm"]["enabled"] is True
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ from packages.domain.template_version import EditTemplateVersion
|
||||
from packages.domain.title_library import TitleLibraryItem
|
||||
from packages.domain.voice_library import VoiceLibraryItem
|
||||
|
||||
|
||||
# ── EditingMode ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -435,6 +435,7 @@ class TestAsset:
|
||||
)
|
||||
old_updated = asset.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
asset.add_tag("tag-1")
|
||||
assert asset.updated_at >= old_updated
|
||||
@@ -450,6 +451,7 @@ class TestAsset:
|
||||
asset.add_tag("tag-1")
|
||||
old_updated = asset.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
asset.remove_tag("tag-1")
|
||||
assert asset.updated_at >= old_updated
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.intro_outro_engine import IntroOutroConfig
|
||||
|
||||
|
||||
|
||||
@@ -280,6 +280,7 @@ class TestJobTransitions:
|
||||
job = Job.create(project_id="p1", job_type=JobType.VIDEO_COMPOSE)
|
||||
old_updated = job.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
job.transition_to(JobStatus.RUNNING)
|
||||
assert job.updated_at >= old_updated
|
||||
@@ -382,6 +383,7 @@ class TestJobProgress:
|
||||
job = Job.create(project_id="p1", job_type=JobType.VIDEO_COMPOSE)
|
||||
old_updated = job.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
job.update_progress(50.0)
|
||||
assert job.updated_at >= old_updated
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.multi_track_mixer import (
|
||||
DEFAULT_VOLUMES,
|
||||
MAX_AUDIO_TRACKS,
|
||||
@@ -47,150 +46,180 @@ class TestAudioTrackFromDict:
|
||||
"""AudioTrack.from_dict 构造逻辑."""
|
||||
|
||||
def test_basic(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
}
|
||||
)
|
||||
assert track.track_id == "t1"
|
||||
assert track.track_type == "bgm"
|
||||
assert track.audio_path == "/tmp/bgm.mp3"
|
||||
assert track.volume == 0.3 # bgm 默认音量
|
||||
|
||||
def test_custom_volume(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "main",
|
||||
"audio_path": "/tmp/main.wav",
|
||||
"volume": 0.8,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "main",
|
||||
"audio_path": "/tmp/main.wav",
|
||||
"volume": 0.8,
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.8
|
||||
|
||||
def test_volume_clamped_to_zero(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": -1.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": -1.0,
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.0
|
||||
|
||||
def test_volume_clamped_to_max(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": 3.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"volume": 3.0,
|
||||
}
|
||||
)
|
||||
assert track.volume == 2.0
|
||||
|
||||
def test_invalid_volume_falls_back_to_default(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": "not_a_number",
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": "not_a_number",
|
||||
}
|
||||
)
|
||||
assert track.volume == 0.3 # bgm 默认
|
||||
|
||||
def test_none_volume_falls_back(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "voiceover",
|
||||
"audio_path": "/tmp/vo.wav",
|
||||
"volume": None,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "voiceover",
|
||||
"audio_path": "/tmp/vo.wav",
|
||||
"volume": None,
|
||||
}
|
||||
)
|
||||
assert track.volume == 1.0 # voiceover 默认
|
||||
|
||||
def test_unknown_track_type_default_volume(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "unknown_type",
|
||||
"audio_path": "/tmp/a.wav",
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "unknown_type",
|
||||
"audio_path": "/tmp/a.wav",
|
||||
}
|
||||
)
|
||||
assert track.volume == 1.0 # 未知类型默认 1.0
|
||||
|
||||
def test_fade_in_fade_out(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": 1.5,
|
||||
"fade_out": 2.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": 1.5,
|
||||
"fade_out": 2.0,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 1.5
|
||||
assert track.fade_out == 2.0
|
||||
|
||||
def test_negative_fade_clamped(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": -0.5,
|
||||
"fade_out": -1.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": -0.5,
|
||||
"fade_out": -1.0,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
|
||||
def test_invalid_fade_falls_back(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": "abc",
|
||||
"fade_out": None,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"fade_in": "abc",
|
||||
"fade_out": None,
|
||||
}
|
||||
)
|
||||
assert track.fade_in == 0.0
|
||||
assert track.fade_out == 0.0
|
||||
|
||||
def test_start_time_and_duration(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"start_time": 5.0,
|
||||
"duration": 3.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "sfx",
|
||||
"audio_path": "/tmp/sfx.wav",
|
||||
"start_time": 5.0,
|
||||
"duration": 3.0,
|
||||
}
|
||||
)
|
||||
assert track.start_time == 5.0
|
||||
assert track.duration == 3.0
|
||||
|
||||
def test_negative_start_time_clamped(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": -10.0,
|
||||
"duration": -2.0,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": -10.0,
|
||||
"duration": -2.0,
|
||||
}
|
||||
)
|
||||
assert track.start_time == 0.0
|
||||
assert track.duration == 0.0
|
||||
|
||||
def test_invalid_time_values_fall_back(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": "invalid",
|
||||
"duration": "bad",
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"start_time": "invalid",
|
||||
"duration": "bad",
|
||||
}
|
||||
)
|
||||
assert track.start_time == 0.0
|
||||
assert track.duration == 0.0
|
||||
|
||||
def test_enabled_default_true(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
}
|
||||
)
|
||||
assert track.enabled is True
|
||||
|
||||
def test_enabled_can_be_false(self):
|
||||
track = AudioTrack.from_dict({
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"enabled": False,
|
||||
})
|
||||
track = AudioTrack.from_dict(
|
||||
{
|
||||
"track_id": "t1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"enabled": False,
|
||||
}
|
||||
)
|
||||
assert track.enabled is False
|
||||
|
||||
|
||||
@@ -212,104 +241,126 @@ class TestMultiTrackMixConfigFromDict:
|
||||
assert cfg.tracks == []
|
||||
|
||||
def test_single_track(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": "bgm1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": 0.5,
|
||||
},
|
||||
],
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{
|
||||
"track_id": "bgm1",
|
||||
"track_type": "bgm",
|
||||
"audio_path": "/tmp/bgm.mp3",
|
||||
"volume": 0.5,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
assert cfg.tracks[0].track_id == "bgm1"
|
||||
assert cfg.tracks[0].volume == 0.5
|
||||
|
||||
def test_multiple_tracks(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
|
||||
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
|
||||
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
|
||||
],
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
|
||||
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
|
||||
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 3
|
||||
assert cfg.tracks[0].track_type == "main"
|
||||
assert cfg.tracks[1].track_type == "bgm"
|
||||
assert cfg.tracks[2].track_type == "voiceover"
|
||||
|
||||
def test_disabled_tracks_filtered(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
|
||||
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
|
||||
],
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
|
||||
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 2
|
||||
assert all(t.track_id != "b" for t in cfg.tracks)
|
||||
|
||||
def test_empty_audio_path_filtered(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
|
||||
],
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
assert cfg.tracks[0].track_id == "valid"
|
||||
|
||||
def test_invalid_tracks_skipped(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [
|
||||
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
"not_a_dict",
|
||||
None,
|
||||
{"no_audio_path": "xxx"},
|
||||
],
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [
|
||||
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
|
||||
"not_a_dict",
|
||||
None,
|
||||
{"no_audio_path": "xxx"},
|
||||
],
|
||||
}
|
||||
)
|
||||
assert len(cfg.tracks) == 1
|
||||
|
||||
def test_tracks_not_a_list(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": "not_a_list",
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": "not_a_list",
|
||||
}
|
||||
)
|
||||
assert cfg.tracks == []
|
||||
|
||||
def test_master_volume(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [],
|
||||
"master_volume": 0.8,
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": 0.8,
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 0.8
|
||||
|
||||
def test_master_volume_clamped(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [],
|
||||
"master_volume": 3.0,
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": 3.0,
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 2.0
|
||||
|
||||
cfg2 = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [],
|
||||
"master_volume": -1.0,
|
||||
})
|
||||
cfg2 = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": -1.0,
|
||||
}
|
||||
)
|
||||
assert cfg2.master_volume == 0.0
|
||||
|
||||
def test_invalid_master_volume_falls_back(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [],
|
||||
"master_volume": "abc",
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"master_volume": "abc",
|
||||
}
|
||||
)
|
||||
assert cfg.master_volume == 1.0
|
||||
|
||||
def test_normalize_and_max_output(self):
|
||||
cfg = MultiTrackMixConfig.from_config_dict({
|
||||
"tracks": [],
|
||||
"normalize": False,
|
||||
"max_output_volume": 2.0,
|
||||
})
|
||||
cfg = MultiTrackMixConfig.from_config_dict(
|
||||
{
|
||||
"tracks": [],
|
||||
"normalize": False,
|
||||
"max_output_volume": 2.0,
|
||||
}
|
||||
)
|
||||
assert cfg.normalize is False
|
||||
assert cfg.max_output_volume == 2.0
|
||||
|
||||
@@ -324,9 +375,11 @@ class TestMultiTrackMixConfigProperties:
|
||||
"""has_effect 属性."""
|
||||
|
||||
def test_has_effect_with_tracks(self):
|
||||
cfg = MultiTrackMixConfig(tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
|
||||
])
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is True
|
||||
|
||||
def test_no_effect_empty(self):
|
||||
@@ -334,13 +387,17 @@ class TestMultiTrackMixConfigProperties:
|
||||
assert cfg.has_effect is False
|
||||
|
||||
def test_no_effect_all_disabled(self):
|
||||
cfg = MultiTrackMixConfig(tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
|
||||
])
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
|
||||
def test_no_effect_empty_paths(self):
|
||||
cfg = MultiTrackMixConfig(tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
|
||||
])
|
||||
cfg = MultiTrackMixConfig(
|
||||
tracks=[
|
||||
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
|
||||
]
|
||||
)
|
||||
assert cfg.has_effect is False
|
||||
|
||||
@@ -6,8 +6,8 @@ import pytest
|
||||
|
||||
from packages.domain.quota import (
|
||||
QUOTA_TIERS,
|
||||
QuotaCheckResult,
|
||||
QuotaChecker,
|
||||
QuotaCheckResult,
|
||||
QuotaDimension,
|
||||
QuotaRegistry,
|
||||
QuotaTier,
|
||||
|
||||
@@ -10,7 +10,6 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.render_subtitles import (
|
||||
_build_ass_style,
|
||||
_escape_ass_text,
|
||||
@@ -49,7 +48,7 @@ class TestHexToAssColor:
|
||||
|
||||
def test_invalid_length_returns_default(self):
|
||||
assert _hex_to_ass_color("#FFF") == "&H000000" # 3位
|
||||
assert _hex_to_ass_color("") == "&H000000" # 空
|
||||
assert _hex_to_ass_color("") == "&H000000" # 空
|
||||
|
||||
def test_mixed_case(self):
|
||||
result = _hex_to_ass_color("#aBcDeF")
|
||||
|
||||
@@ -8,7 +8,6 @@ FFmpeg 抽帧与 OSS 上传由集成测试覆盖.
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.thumbnail_generator import _format_seek_time
|
||||
|
||||
|
||||
|
||||
@@ -104,10 +104,8 @@ class TestTransitionPresetLibrary:
|
||||
def test_duration_constraints_valid(self):
|
||||
"""时长约束:min <= default <= max"""
|
||||
for preset in TRANSITION_PRESET_LIBRARY:
|
||||
assert preset.min_duration <= preset.default_duration, \
|
||||
f"{preset.id}: min > default"
|
||||
assert preset.default_duration <= preset.max_duration, \
|
||||
f"{preset.id}: default > max"
|
||||
assert preset.min_duration <= preset.default_duration, f"{preset.id}: min > default"
|
||||
assert preset.default_duration <= preset.max_duration, f"{preset.id}: default > max"
|
||||
assert preset.min_duration >= 0, f"{preset.id}: min < 0"
|
||||
|
||||
def test_known_categories_exist(self):
|
||||
|
||||
@@ -216,6 +216,7 @@ class TestTTSJobTransitions:
|
||||
job = TTSJob.create(user_id="u1", input_text="test")
|
||||
old = job.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
job.transition_to(TTSJobStatus.PROCESSING)
|
||||
assert job.updated_at >= old
|
||||
|
||||
@@ -69,6 +69,7 @@ class TestHashPassword:
|
||||
def test_hash_includes_salt(self):
|
||||
"""加盐后与直接 SHA-256 不同"""
|
||||
from hashlib import sha256
|
||||
|
||||
direct = sha256("mypass".encode()).hexdigest()
|
||||
salted = _hash_password("mypass")
|
||||
assert direct != salted
|
||||
|
||||
@@ -69,15 +69,11 @@ class TestVoiceCloneProfileCreate:
|
||||
assert profile.name == name
|
||||
|
||||
def test_create_with_description(self):
|
||||
profile = VoiceCloneProfile.create(
|
||||
user_id="u1", name="test", description="温暖男声"
|
||||
)
|
||||
profile = VoiceCloneProfile.create(user_id="u1", name="test", description="温暖男声")
|
||||
assert profile.description == "温暖男声"
|
||||
|
||||
def test_create_with_source_audio(self):
|
||||
profile = VoiceCloneProfile.create(
|
||||
user_id="u1", name="test", source_audio_url="http://audio.com/source.wav"
|
||||
)
|
||||
profile = VoiceCloneProfile.create(user_id="u1", name="test", source_audio_url="http://audio.com/source.wav")
|
||||
assert profile.source_audio_url == "http://audio.com/source.wav"
|
||||
|
||||
def test_create_with_language(self):
|
||||
@@ -251,6 +247,7 @@ class TestVoiceCloneProfileTransitions:
|
||||
p = VoiceCloneProfile.create(user_id="u1", name="test")
|
||||
old = p.updated_at
|
||||
import time
|
||||
|
||||
time.sleep(0.001)
|
||||
p.transition_to(VoiceCloneStatus.PROCESSING)
|
||||
assert p.updated_at >= old
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.watermark_engine import WATERMARK_POSITIONS, WatermarkConfig
|
||||
|
||||
|
||||
@@ -44,46 +43,56 @@ class TestWatermarkConfigFromDict:
|
||||
assert WatermarkConfig.from_dict({"enabled": False}) is None
|
||||
|
||||
def test_image_mode_without_path_returns_none(self):
|
||||
result = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
})
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
}
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_image_mode_with_empty_path_returns_none(self):
|
||||
result = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image_path": "",
|
||||
})
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image_path": "",
|
||||
}
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_text_mode_without_text_returns_none(self):
|
||||
result = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
})
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
}
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_text_mode_with_empty_text_returns_none(self):
|
||||
result = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "",
|
||||
})
|
||||
result = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "",
|
||||
}
|
||||
)
|
||||
assert result is None
|
||||
|
||||
def test_image_mode_success(self):
|
||||
cfg = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image_path": "/tmp/logo.png",
|
||||
"scale": 0.3,
|
||||
"opacity": 0.9,
|
||||
"position": "top_left",
|
||||
"margin_x": 30,
|
||||
"margin_y": 30,
|
||||
})
|
||||
cfg = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image_path": "/tmp/logo.png",
|
||||
"scale": 0.3,
|
||||
"opacity": 0.9,
|
||||
"position": "top_left",
|
||||
"margin_x": 30,
|
||||
"margin_y": 30,
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.mode == "image"
|
||||
assert cfg.image_path == "/tmp/logo.png"
|
||||
@@ -95,25 +104,29 @@ class TestWatermarkConfigFromDict:
|
||||
|
||||
def test_image_mode_image_key_fallback(self):
|
||||
"""image 字段作为 image_path 的 fallback."""
|
||||
cfg = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image": "/tmp/fallback.png",
|
||||
})
|
||||
cfg = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "image",
|
||||
"image": "/tmp/fallback.png",
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.image_path == "/tmp/fallback.png"
|
||||
|
||||
def test_text_mode_success(self):
|
||||
cfg = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "hello world",
|
||||
"font_size": 32,
|
||||
"font_color": "red",
|
||||
"position": "bottom_left",
|
||||
"scroll": True,
|
||||
"scroll_speed": 100,
|
||||
})
|
||||
cfg = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "hello world",
|
||||
"font_size": 32,
|
||||
"font_color": "red",
|
||||
"position": "bottom_left",
|
||||
"scroll": True,
|
||||
"scroll_speed": 100,
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.mode == "text"
|
||||
assert cfg.text == "hello world"
|
||||
@@ -124,21 +137,25 @@ class TestWatermarkConfigFromDict:
|
||||
assert cfg.scroll_speed == 100
|
||||
|
||||
def test_invalid_position_falls_back_to_bottom_right(self):
|
||||
cfg = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "test",
|
||||
"position": "invalid_position",
|
||||
})
|
||||
cfg = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "test",
|
||||
"position": "invalid_position",
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.position == "bottom_right"
|
||||
|
||||
def test_default_values_applied(self):
|
||||
cfg = WatermarkConfig.from_dict({
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "test",
|
||||
})
|
||||
cfg = WatermarkConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"mode": "text",
|
||||
"text": "test",
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.position == "bottom_right"
|
||||
assert cfg.opacity == 0.8
|
||||
|
||||
Reference in New Issue
Block a user