test(media): 覆盖转码入队与重转幂等
This commit is contained in:
1 parent
cd51aabe7e
commit
7e6075a2e9
5 files changed
+435
No files matched your search
+146
@@ -0,0 +1,146 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.worker;
|
||||
|
||||
import com.fruitshorts.media.config.MediaProperties;
|
||||
import com.fruitshorts.media.dal.entity.MediaAsset;
|
||||
import com.fruitshorts.media.dal.entity.MediaUploadTask;
|
||||
import com.fruitshorts.media.dal.mapper.MediaAssetMapper;
|
||||
import com.fruitshorts.media.dal.mapper.MediaUploadTaskMapper;
|
||||
import com.fruitshorts.media.service.MediaService;
|
||||
import com.fruitshorts.media.service.MediaStorageService;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.media.vo.TranscodeTriggerRespVO;
|
||||
import com.fruitshorts.media.vo.UploadCompleteReqVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* 媒资完成上传与后台手动重转的入队语义。
|
||||
*/
|
||||
class MediaServiceTranscodePipelineTest {
|
||||
|
||||
private static final Long ASSET_ID = 88L;
|
||||
private static final String SOURCE_URL = "/files/episode/202610/video.mp4";
|
||||
|
||||
private MediaAssetMapper mediaAssetMapper;
|
||||
private MediaUploadTaskMapper uploadTaskMapper;
|
||||
private MediaStorageService storageService;
|
||||
private TranscodeService transcodeService;
|
||||
private MediaService mediaService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mediaAssetMapper = mock(MediaAssetMapper.class);
|
||||
uploadTaskMapper = mock(MediaUploadTaskMapper.class);
|
||||
storageService = mock(MediaStorageService.class);
|
||||
transcodeService = mock(TranscodeService.class);
|
||||
mediaService = new MediaService(
|
||||
new MediaProperties(), storageService, mediaAssetMapper,
|
||||
uploadTaskMapper, transcodeService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_enqueue_video_transcode_when_upload_completes() {
|
||||
givenCompletedUpload("video.mp4");
|
||||
given(storageService.countChunks("upload-1")).willReturn(1);
|
||||
given(storageService.merge("upload-1", "episode/202610/video.mp4", 1))
|
||||
.willReturn(new MediaStorageService.MergeResult(
|
||||
Path.of("/tmp/video.mp4"), 1024L, "md5"));
|
||||
given(storageService.toPublicUrl("episode/202610/video.mp4"))
|
||||
.willReturn(SOURCE_URL);
|
||||
given(mediaAssetMapper.selectById(ASSET_ID)).willReturn(asset("video.mp4"));
|
||||
|
||||
mediaService.complete(completeReq(), 10L);
|
||||
|
||||
verify(transcodeService).enqueue(ASSET_ID, 0L, 0L, SOURCE_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_enqueue_image_when_upload_completes() {
|
||||
givenCompletedUpload("cover.jpg");
|
||||
given(storageService.countChunks("upload-1")).willReturn(1);
|
||||
given(storageService.merge("upload-1", "episode/202610/cover.jpg", 1))
|
||||
.willReturn(new MediaStorageService.MergeResult(
|
||||
Path.of("/tmp/cover.jpg"), 512L, "md5"));
|
||||
given(storageService.toPublicUrl("episode/202610/cover.jpg"))
|
||||
.willReturn("/files/episode/202610/cover.jpg");
|
||||
given(mediaAssetMapper.selectById(ASSET_ID)).willReturn(asset("cover.jpg"));
|
||||
|
||||
mediaService.complete(completeReq(), 10L);
|
||||
|
||||
verify(transcodeService, never()).enqueue(any(), any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_not_found_when_triggering_transcode_for_missing_media() {
|
||||
given(mediaAssetMapper.selectOne(any())).willReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> mediaService.triggerTranscode(404L))
|
||||
.isInstanceOf(com.fruitshorts.common.exception.ServiceException.class)
|
||||
.hasMessage("媒资不存在");
|
||||
verify(transcodeService, never()).trigger(any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_trigger_transcode_and_mark_asset_queued() {
|
||||
MediaAsset asset = asset("video.mp4");
|
||||
given(mediaAssetMapper.selectOne(any())).willReturn(asset);
|
||||
given(transcodeService.trigger(ASSET_ID, SOURCE_URL))
|
||||
.willReturn(new TranscodeTriggerRespVO(100L, TranscodeService.STATUS_QUEUED));
|
||||
|
||||
TranscodeTriggerRespVO result = mediaService.triggerTranscode(ASSET_ID);
|
||||
|
||||
assertThat(result.getTaskId()).isEqualTo(100L);
|
||||
assertThat(result.getStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
assertThat(asset.getStatus()).isEqualTo("transcoding");
|
||||
assertThat(asset.getTranscodeStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
verify(mediaAssetMapper).updateById(asset);
|
||||
}
|
||||
|
||||
private void givenCompletedUpload(String fileName) {
|
||||
String objectKey = "episode/202610/" + fileName;
|
||||
MediaUploadTask task = new MediaUploadTask();
|
||||
task.setUploadId("upload-1");
|
||||
task.setFileName(fileName);
|
||||
task.setObjectKey(objectKey);
|
||||
task.setTotalChunks(1);
|
||||
task.setStatus("uploading");
|
||||
task.setCreateBy(10L);
|
||||
given(uploadTaskMapper.selectOne(any())).willReturn(task);
|
||||
doAnswer(invocation -> {
|
||||
MediaAsset inserted = invocation.getArgument(0);
|
||||
inserted.setId(ASSET_ID);
|
||||
return 1;
|
||||
}).when(mediaAssetMapper).insert(any(MediaAsset.class));
|
||||
}
|
||||
|
||||
private UploadCompleteReqVO completeReq() {
|
||||
UploadCompleteReqVO req = new UploadCompleteReqVO();
|
||||
req.setUploadId("upload-1");
|
||||
req.setDurationSec(120);
|
||||
return req;
|
||||
}
|
||||
|
||||
private MediaAsset asset(String fileName) {
|
||||
MediaAsset asset = new MediaAsset();
|
||||
asset.setId(ASSET_ID);
|
||||
asset.setFileName(fileName);
|
||||
asset.setObjectKey("episode/202610/" + fileName);
|
||||
asset.setSourceUrl(SOURCE_URL);
|
||||
asset.setStatus("uploaded");
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.worker;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fruitshorts.media.dal.entity.MediaRendition;
|
||||
import com.fruitshorts.media.dal.entity.MediaThumbnail;
|
||||
import com.fruitshorts.media.dal.entity.TranscodeTask;
|
||||
import com.fruitshorts.media.dal.mapper.MediaRenditionMapper;
|
||||
import com.fruitshorts.media.dal.mapper.MediaThumbnailMapper;
|
||||
import com.fruitshorts.media.dal.mapper.TranscodeTaskMapper;
|
||||
import com.fruitshorts.media.service.ThumbnailRecordService;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.media.vo.TranscodeTriggerRespVO;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* 后台重转的幂等规则:进行中复用、失败重置、成功才新建。
|
||||
*/
|
||||
class TranscodeServiceTriggerTest {
|
||||
|
||||
private static final Long ASSET_ID = 88L;
|
||||
|
||||
private TranscodeTaskMapper taskMapper;
|
||||
private TranscodeService transcodeService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfoCache() {
|
||||
MapperBuilderAssistant assistant =
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, TranscodeTask.class);
|
||||
TableInfoHelper.initTableInfo(assistant, MediaRendition.class);
|
||||
TableInfoHelper.initTableInfo(assistant, MediaThumbnail.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
taskMapper = mock(TranscodeTaskMapper.class);
|
||||
transcodeService = new TranscodeService(
|
||||
taskMapper, mock(MediaRenditionMapper.class),
|
||||
mock(MediaThumbnailMapper.class), mock(ThumbnailRecordService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_create_once_and_reuse_queued_task_when_retrying_successful_asset() {
|
||||
TranscodeTask success = task(10L, TranscodeService.STATUS_SUCCESS);
|
||||
TranscodeTask queued = task(100L, TranscodeService.STATUS_QUEUED);
|
||||
given(taskMapper.selectOne(any())).willReturn(success, null, queued);
|
||||
doAnswer(invocation -> {
|
||||
TranscodeTask inserted = invocation.getArgument(0);
|
||||
inserted.setId(100L);
|
||||
return 1;
|
||||
}).when(taskMapper).insert(any(TranscodeTask.class));
|
||||
|
||||
TranscodeTriggerRespVO first = transcodeService.trigger(ASSET_ID, "/files/video.mp4");
|
||||
TranscodeTriggerRespVO second = transcodeService.trigger(ASSET_ID, "/files/video.mp4");
|
||||
|
||||
assertThat(first.getTaskId()).isEqualTo(100L);
|
||||
assertThat(first.getStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
assertThat(second.getTaskId()).isEqualTo(100L);
|
||||
assertThat(second.getStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
verify(taskMapper).insert(any(TranscodeTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reuse_queued_task_for_repeated_trigger() {
|
||||
TranscodeTask queued = task(21L, TranscodeService.STATUS_QUEUED);
|
||||
given(taskMapper.selectOne(any())).willReturn(queued);
|
||||
|
||||
TranscodeTriggerRespVO first = transcodeService.trigger(ASSET_ID, "/files/video.mp4");
|
||||
TranscodeTriggerRespVO second = transcodeService.trigger(ASSET_ID, "/files/video.mp4");
|
||||
|
||||
assertThat(first.getTaskId()).isEqualTo(21L);
|
||||
assertThat(second.getTaskId()).isEqualTo(21L);
|
||||
assertThat(second.getStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
verify(taskMapper, never()).insert(any(TranscodeTask.class));
|
||||
verify(taskMapper, never()).updateById(any(TranscodeTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reset_the_same_failed_task_for_manual_retry() {
|
||||
TranscodeTask failed = task(31L, TranscodeService.STATUS_FAILED);
|
||||
failed.setRetryCount(3);
|
||||
failed.setErrorMsg("ffmpeg failed");
|
||||
given(taskMapper.selectOne(any())).willReturn(failed);
|
||||
given(taskMapper.update(any(), any())).willReturn(1);
|
||||
|
||||
TranscodeTriggerRespVO result =
|
||||
transcodeService.trigger(ASSET_ID, "/files/video.mp4");
|
||||
|
||||
assertThat(result.getTaskId()).isEqualTo(31L);
|
||||
assertThat(result.getStatus()).isEqualTo(TranscodeService.STATUS_QUEUED);
|
||||
verify(taskMapper).update(any(), any());
|
||||
verify(taskMapper, never()).insert(any(TranscodeTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_persist_failure_reason_and_status_after_max_retry() {
|
||||
TranscodeTask running = task(41L, TranscodeService.STATUS_RUNNING);
|
||||
running.setRetryCount(2);
|
||||
given(taskMapper.selectById(41L)).willReturn(running);
|
||||
|
||||
String status = transcodeService.fail(
|
||||
41L, "ffmpeg 退出码 1:Invalid data found");
|
||||
|
||||
assertThat(status).isEqualTo(TranscodeService.STATUS_FAILED);
|
||||
assertThat(running.getStatus()).isEqualTo(TranscodeService.STATUS_FAILED);
|
||||
assertThat(running.getRetryCount()).isEqualTo(3);
|
||||
assertThat(running.getErrorMsg()).contains("Invalid data found");
|
||||
verify(taskMapper).updateById(running);
|
||||
}
|
||||
|
||||
private TranscodeTask task(Long id, String status) {
|
||||
TranscodeTask task = new TranscodeTask();
|
||||
task.setId(id);
|
||||
task.setMediaAssetId(ASSET_ID);
|
||||
task.setDramaId(7L);
|
||||
task.setEpisodeId(31L);
|
||||
task.setSourceUrl("/files/episode/202610/video.mp4");
|
||||
task.setStatus(status);
|
||||
task.setProgress(100);
|
||||
task.setRetryCount(0);
|
||||
task.setErrorMsg("");
|
||||
return task;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user