@@ -0,0 +1,523 @@
""" job 单测.
domain 层统一异步任务纯逻辑模块,0 外部依赖。
覆盖:枚举、create工厂、状态机、进度更新、重试机制、序列化。
"""
from __future__ import annotations
from packages . domain . job import TERMINAL_STATUSES , Job , JobStatus , JobType
class TestJobType :
""" JobType 枚举测试. """
def test_six_types ( self ) :
""" 六种任务类型. """
assert len ( JobType ) == 6
def test_video_compose ( self ) :
assert JobType . VIDEO_COMPOSE == " video_compose "
def test_render_edit_plan ( self ) :
assert JobType . RENDER_EDIT_PLAN == " render_edit_plan "
def test_asset_ingest ( self ) :
assert JobType . ASSET_INGEST == " asset_ingest "
def test_classification ( self ) :
assert JobType . CLASSIFICATION == " classification "
def test_voice_extraction ( self ) :
assert JobType . VOICE_EXTRACTION == " voice_extraction "
def test_generation ( self ) :
assert JobType . GENERATION == " generation "
class TestJobStatus :
""" JobStatus 枚举测试. """
def test_five_statuses ( self ) :
assert len ( JobStatus ) == 5
def test_pending ( self ) :
assert JobStatus . PENDING == " pending "
def test_running ( self ) :
assert JobStatus . RUNNING == " running "
def test_success ( self ) :
assert JobStatus . SUCCESS == " success "
def test_failed ( self ) :
assert JobStatus . FAILED == " failed "
def test_cancelled ( self ) :
assert JobStatus . CANCELLED == " cancelled "
class TestTerminalStatuses :
""" 终态集合测试. """
def test_success_is_terminal ( self ) :
assert JobStatus . SUCCESS in TERMINAL_STATUSES
def test_failed_is_terminal ( self ) :
assert JobStatus . FAILED in TERMINAL_STATUSES
def test_cancelled_is_terminal ( self ) :
assert JobStatus . CANCELLED in TERMINAL_STATUSES
def test_pending_not_terminal ( self ) :
assert JobStatus . PENDING not in TERMINAL_STATUSES
def test_running_not_terminal ( self ) :
assert JobStatus . RUNNING not in TERMINAL_STATUSES
class TestJobCreate :
""" Job.create 工厂测试. """
def test_create_minimal ( self ) :
""" 最简创建. """
job = Job . create ( project_id = " proj1 " , job_type = JobType . VIDEO_COMPOSE )
assert job . project_id == " proj1 "
assert job . job_type == JobType . VIDEO_COMPOSE
assert job . status == JobStatus . PENDING
assert job . progress == 0.0
assert job . current_stage == " "
assert job . payload == { }
assert job . result == { }
assert job . error_message == " "
assert job . retry_count == 0
assert job . max_retries == 3
assert job . source_id == " "
assert job . created_by_user_id == " "
assert job . started_at is None
assert job . completed_at is None
assert isinstance ( job . id , str )
assert len ( job . id ) > 0
def test_create_with_string_job_type ( self ) :
""" 用字符串传 job_type. """
job = Job . create ( project_id = " p1 " , job_type = " video_compose " )
assert job . job_type == JobType . VIDEO_COMPOSE
def test_create_invalid_job_type_string ( self ) :
""" 无效的 job_type 字符串. """
try :
Job . create ( project_id = " p1 " , job_type = " invalid_type " )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 不支持的任务类型 " in str ( e )
def test_create_full ( self ) :
""" 带全部字段. """
job = Job . create (
project_id = " proj1 " ,
job_type = JobType . CLASSIFICATION ,
payload = { " asset_id " : " a1 " } ,
source_id = " src1 " ,
created_by_user_id = " user1 " ,
max_retries = 5 ,
)
assert job . project_id == " proj1 " # strip
assert job . job_type == JobType . CLASSIFICATION
assert job . payload == { " asset_id " : " a1 " }
assert job . source_id == " src1 " # strip
assert job . created_by_user_id == " user1 " # strip
assert job . max_retries == 5
def test_create_empty_project_id ( self ) :
""" 空 project_id 无效. """
try :
Job . create ( project_id = " " , job_type = JobType . VIDEO_COMPOSE )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " project_id " in str ( e )
def test_create_whitespace_project_id ( self ) :
""" 空白 project_id 无效. """
try :
Job . create ( project_id = " " , job_type = JobType . VIDEO_COMPOSE )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " project_id " in str ( e )
def test_create_payload_none_defaults_empty ( self ) :
""" payload=None 默认为空 dict. """
job = Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE , payload = None )
assert job . payload == { }
def test_create_unique_id ( self ) :
""" 不同任务 id 不同. """
j1 = Job . create ( " p " , JobType . VIDEO_COMPOSE )
j2 = Job . create ( " p " , JobType . VIDEO_COMPOSE )
assert j1 . id != j2 . id
def test_create_has_timestamps ( self ) :
""" 有创建和更新时间. """
job = Job . create ( " p " , JobType . VIDEO_COMPOSE )
assert job . created_at is not None
assert job . updated_at is not None
class TestJobIsTerminal :
""" is_terminal 属性测试. """
def _make_job ( self ) :
return Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
def test_pending_not_terminal ( self ) :
job = self . _make_job ( )
assert job . is_terminal is False
def test_running_not_terminal ( self ) :
job = self . _make_job ( )
job . mark_running ( )
assert job . is_terminal is False
def test_success_is_terminal ( self ) :
job = self . _make_job ( )
job . mark_running ( )
job . mark_success ( )
assert job . is_terminal is True
def test_failed_is_terminal ( self ) :
job = self . _make_job ( )
job . mark_running ( )
job . mark_failed ( " error " )
assert job . is_terminal is True
def test_cancelled_is_terminal ( self ) :
job = self . _make_job ( )
job . mark_cancelled ( )
assert job . is_terminal is True
class TestJobStatusTransitions :
""" 状态转换测试. """
def _make_job ( self ) :
return Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
def test_pending_to_running ( self ) :
""" pending → running. """
job = self . _make_job ( )
job . transition_to ( JobStatus . RUNNING )
assert job . status == JobStatus . RUNNING
def test_pending_to_success ( self ) :
""" pending → success(瞬时任务). """
job = self . _make_job ( )
job . transition_to ( JobStatus . SUCCESS )
assert job . status == JobStatus . SUCCESS
def test_pending_to_cancelled ( self ) :
""" pending → cancelled. """
job = self . _make_job ( )
job . transition_to ( JobStatus . CANCELLED )
assert job . status == JobStatus . CANCELLED
def test_running_to_success ( self ) :
""" running → success. """
job = self . _make_job ( )
job . transition_to ( JobStatus . RUNNING )
job . transition_to ( JobStatus . SUCCESS )
assert job . status == JobStatus . SUCCESS
def test_running_to_failed ( self ) :
""" running → failed. """
job = self . _make_job ( )
job . transition_to ( JobStatus . RUNNING )
job . transition_to ( JobStatus . FAILED )
assert job . status == JobStatus . FAILED
def test_running_to_cancelled ( self ) :
""" running → cancelled. """
job = self . _make_job ( )
job . transition_to ( JobStatus . RUNNING )
job . transition_to ( JobStatus . CANCELLED )
assert job . status == JobStatus . CANCELLED
def test_failed_to_pending_retry ( self ) :
""" failed → pending(重试). """
job = self . _make_job ( )
job . transition_to ( JobStatus . RUNNING )
job . transition_to ( JobStatus . FAILED )
job . transition_to ( JobStatus . PENDING )
assert job . status == JobStatus . PENDING
def test_cannot_success_to_running ( self ) :
""" success 不能回 running. """
job = self . _make_job ( )
job . mark_running ( )
job . mark_success ( )
try :
job . transition_to ( JobStatus . RUNNING )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 非法状态转换 " in str ( e )
def test_cannot_pending_to_failed_directly ( self ) :
""" pending 不能直接到 failed(必须经过 running) . """
job = self . _make_job ( )
try :
job . transition_to ( JobStatus . FAILED )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 非法状态转换 " in str ( e )
def test_transition_with_string ( self ) :
""" 用字符串传状态. """
job = self . _make_job ( )
job . transition_to ( " running " )
assert job . status == JobStatus . RUNNING
def test_transition_invalid_string ( self ) :
""" 无效状态字符串. """
job = self . _make_job ( )
try :
job . transition_to ( " invalid " )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 无效状态 " in str ( e )
def test_transition_sets_started_at ( self ) :
""" 第一次到 running 设置 started_at. """
job = self . _make_job ( )
assert job . started_at is None
job . transition_to ( JobStatus . RUNNING )
assert job . started_at is not None
def test_transition_sets_completed_at_on_success ( self ) :
""" success 设置 completed_at. """
job = self . _make_job ( )
job . mark_running ( )
assert job . completed_at is None
job . mark_success ( )
assert job . completed_at is not None
def test_transition_sets_completed_at_on_failed ( self ) :
""" failed 设置 completed_at. """
job = self . _make_job ( )
job . mark_running ( )
job . mark_failed ( " err " )
assert job . completed_at is not None
class TestJobMarkMethods :
""" 便捷标记方法测试. """
def _make_job ( self ) :
return Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
def test_mark_running ( self ) :
job = self . _make_job ( )
job . mark_running ( )
assert job . status == JobStatus . RUNNING
def test_mark_running_with_stage ( self ) :
job = self . _make_job ( )
job . mark_running ( stage = " 正在合成视频 " )
assert job . status == JobStatus . RUNNING
assert job . current_stage == " 正在合成视频 "
def test_mark_success ( self ) :
job = self . _make_job ( )
job . mark_running ( )
job . mark_success ( )
assert job . status == JobStatus . SUCCESS
assert job . progress == 100.0
assert job . current_stage == " 完成 "
def test_mark_success_with_result ( self ) :
job = self . _make_job ( )
job . mark_running ( )
job . mark_success ( result = { " video_url " : " http://x/v.mp4 " } )
assert job . result == { " video_url " : " http://x/v.mp4 " }
def test_mark_failed ( self ) :
job = self . _make_job ( )
job . mark_running ( )
job . mark_failed ( " 网络超时 " )
assert job . status == JobStatus . FAILED
assert job . error_message == " 网络超时 "
assert job . current_stage == " 失败 "
def test_mark_cancelled ( self ) :
job = self . _make_job ( )
job . mark_cancelled ( )
assert job . status == JobStatus . CANCELLED
assert job . current_stage == " 已取消 "
class TestJobProgress :
""" 进度更新测试. """
def _make_job ( self ) :
return Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
def test_update_progress ( self ) :
""" 正常更新进度. """
job = self . _make_job ( )
job . update_progress ( 50.0 )
assert job . progress == 50.0
def test_update_progress_with_stage ( self ) :
""" 更新进度同时更新阶段. """
job = self . _make_job ( )
job . update_progress ( 30.0 , stage = " 合成中 " )
assert job . progress == 30.0
assert job . current_stage == " 合成中 "
def test_update_progress_zero ( self ) :
""" 0 % 合法. """
job = self . _make_job ( )
job . update_progress ( 0.0 )
assert job . progress == 0.0
def test_update_progress_100 ( self ) :
""" 100 % 合法. """
job = self . _make_job ( )
job . update_progress ( 100.0 )
assert job . progress == 100.0
def test_update_progress_negative ( self ) :
""" 负进度无效. """
job = self . _make_job ( )
try :
job . update_progress ( - 1.0 )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 进度 " in str ( e )
def test_update_progress_over_100 ( self ) :
""" 超过100 % 无效. """
job = self . _make_job ( )
try :
job . update_progress ( 101.0 )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 进度 " in str ( e )
def test_update_progress_updates_timestamp ( self ) :
""" 更新进度时更新 updated_at. """
job = self . _make_job ( )
old = job . updated_at
job . update_progress ( 50.0 )
assert job . updated_at > = old
class TestJobRetry :
""" 重试机制测试. """
def _make_failed_job ( self , max_retries = 3 ) :
job = Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE , max_retries = max_retries )
job . mark_running ( )
job . mark_failed ( " error " )
return job
def test_is_retryable_true ( self ) :
""" 失败且未超过重试次数时可重试. """
job = self . _make_failed_job ( max_retries = 3 )
assert job . is_retryable is True
def test_is_retryable_false_not_failed ( self ) :
""" 非失败状态不可重试. """
job = Job . create ( " p " , JobType . VIDEO_COMPOSE )
assert job . is_retryable is False
def test_is_retryable_false_exceeded ( self ) :
""" 超过重试次数不可重试. """
job = self . _make_failed_job ( max_retries = 0 )
assert job . is_retryable is False
def test_prepare_retry ( self ) :
""" 准备重试. """
job = self . _make_failed_job ( max_retries = 3 )
job . prepare_retry ( )
assert job . status == JobStatus . PENDING
assert job . retry_count == 1
assert job . progress == 0.0
assert " 第 1 次重试 " in job . current_stage
assert job . error_message == " "
assert job . started_at is None
assert job . completed_at is None
assert job . celery_task_id == " "
def test_prepare_retry_multiple_times ( self ) :
""" 多次重试. """
job = self . _make_failed_job ( max_retries = 3 )
job . prepare_retry ( )
job . mark_running ( )
job . mark_failed ( " err2 " )
assert job . retry_count == 1
job . prepare_retry ( )
assert job . retry_count == 2
assert " 第 2 次重试 " in job . current_stage
def test_prepare_retry_not_retryable ( self ) :
""" 不可重试时报错. """
job = self . _make_failed_job ( max_retries = 0 )
try :
job . prepare_retry ( )
raise AssertionError ( " unexpected success " )
except ValueError as e :
assert " 不可重试 " in str ( e )
class TestJobToDict :
""" to_dict 序列化测试. """
def test_to_dict_keys ( self ) :
""" 包含所有必要字段. """
job = Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
d = job . to_dict ( )
assert isinstance ( d , dict )
keys = [
" id " ,
" project_id " ,
" job_type " ,
" status " ,
" progress " ,
" payload " ,
" result " ,
" error_message " ,
" retry_count " ,
" max_retries " ,
" source_id " ,
" is_retryable " ,
" created_at " ,
" updated_at " ,
]
for k in keys :
assert k in d , f " 缺少字段: { k } "
def test_to_dict_enums_as_strings ( self ) :
""" 枚举值序列化为字符串. """
job = Job . create ( project_id = " p1 " , job_type = JobType . CLASSIFICATION )
d = job . to_dict ( )
assert d [ " job_type " ] == " classification "
assert d [ " status " ] == " pending "
def test_to_dict_none_timestamps ( self ) :
""" None 时间戳序列化为 None. """
job = Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
d = job . to_dict ( )
assert d [ " started_at " ] is None
assert d [ " completed_at " ] is None
def test_to_dict_after_success ( self ) :
""" 成功后的序列化. """
job = Job . create ( project_id = " p1 " , job_type = JobType . VIDEO_COMPOSE )
job . mark_running ( " test " )
job . mark_success ( { " url " : " http://x " } )
d = job . to_dict ( )
assert d [ " status " ] == " success "
assert d [ " progress " ] == 100.0
assert d [ " result " ] == { " url " : " http://x " }
assert d [ " started_at " ] is not None
assert d [ " completed_at " ] is not None
assert d [ " is_retryable " ] is False