fix(pipeline): 增加防御性检查避免空指针异常

- 在多个阶段中增加对 memberSourceList 和 searchResult 的空值检查
- 当 memberSourceList 为空时跳过视频重切和购买状态处理逻辑
- 当 searchResult 为空时跳过人脸补救逻辑
- 增加对自定义匹配场景的判断,非该场景则跳过指标记录
- 为各个阶段添加详细的单元测试覆盖各种边界条件
This commit is contained in:
2025-12-03 19:23:54 +08:00
parent 8c08c8947e
commit ecd5378b26
13 changed files with 1760 additions and 0 deletions

View File

@@ -54,6 +54,12 @@ public class FaceRecoveryStage extends AbstractFaceMatchingStage<FaceMatchingCon
SearchFaceRespVo searchResult = context.getSearchResult();
Long faceId = context.getFaceId();
// 防御性检查:searchResult为空
if (searchResult == null) {
log.debug("searchResult为空,跳过补救逻辑,faceId={}", faceId);
return StageResult.skipped("searchResult为空");
}
try {
// 执行补救逻辑(补救逻辑内部会判断是否需要触发)
SearchFaceRespVo recoveredResult = faceRecoveryStrategy.executeFaceRecoveryLogic(

View File

@@ -62,6 +62,12 @@ public class HandleVideoRecreationStage extends AbstractFaceMatchingStage<FaceMa
List<Long> sampleListIds = context.getSampleListIds();
boolean isNew = context.isNew();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过视频重切,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理视频重切
videoRecreationHandler.handleVideoRecreation(

View File

@@ -62,6 +62,12 @@ public class ProcessBuyStatusStage extends AbstractFaceMatchingStage<FaceMatchin
Long scenicId = context.getFace().getScenicId();
Long faceId = context.getFaceId();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过购买状态处理,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理购买状态
buyStatusProcessor.processBuyStatus(

View File

@@ -61,6 +61,12 @@ public class ProcessFreeSourceStage extends AbstractFaceMatchingStage<FaceMatchi
boolean isNew = context.isNew();
Long faceId = context.getFaceId();
// 防御性检查:memberSourceList为空
if (memberSourceEntityList == null || memberSourceEntityList.isEmpty()) {
log.debug("memberSourceList为空,跳过免费逻辑,faceId={}", faceId);
return StageResult.skipped("memberSourceList为空");
}
try {
// 处理免费逻辑
List<Long> freeSourceIds = sourceRelationProcessor.processFreeSourceLogic(

View File

@@ -51,6 +51,12 @@ public class RecordCustomMatchMetricsStage extends AbstractFaceMatchingStage<Fac
protected StageResult<FaceMatchingContext> doExecute(FaceMatchingContext context) {
Long faceId = context.getFaceId();
// 防御性检查:只有自定义匹配场景才执行
if (context.getScene() != FaceMatchingScene.CUSTOM_MATCHING) {
log.debug("非自定义匹配场景,跳过记录,faceId={}", faceId);
return StageResult.skipped("非自定义匹配场景");
}
try {
metricsRecorder.recordCustomMatchCount(faceId);
log.debug("记录自定义匹配次数: faceId={}", faceId);

View File

@@ -0,0 +1,185 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.biz.TaskStatusBiz;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.service.pc.helper.ScenicConfigFacade;
import com.ycwl.basic.service.task.TaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* CreateTaskStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class CreateTaskStageTest {
@Mock
private ScenicConfigFacade scenicConfigFacade;
@Mock
private TaskService taskService;
@Mock
private TaskStatusBiz taskStatusBiz;
@InjectMocks
private CreateTaskStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
context.setFace(face);
}
@Test
void testExecute_AutoCreateTask_Success() {
// Given: 配置为自动创建任务(faceSelectFirst = false)
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(false);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("自动创建任务成功"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
}
@Test
void testExecute_WaitForUserSelection_Skipped() {
// Given: 配置为等待用户选择(faceSelectFirst = true)
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(true);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
assertTrue(result.getMessage().contains("等待用户手动选择"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
}
@Test
void testExecute_CheckConfigFailed_Degraded() {
// Given: 查询配置失败
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenThrow(new RuntimeException("Config service error"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("任务创建失败"));
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(10L);
verify(taskService, never()).autoCreateTaskByFaceId(anyLong());
verify(taskStatusBiz, never()).setFaceCutStatus(anyLong(), anyInt());
}
@Test
void testExecute_AutoCreateTaskFailed_Degraded() {
// Given: 自动创建任务失败
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(false);
doThrow(new RuntimeException("Task creation error"))
.when(taskService).autoCreateTaskByFaceId(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("任务创建失败"));
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
}
@Test
void testExecute_SetStatusFailed_Degraded() {
// Given: 设置状态失败
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(true);
doThrow(new RuntimeException("Status set error"))
.when(taskStatusBiz).setFaceCutStatus(1L, 2);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("任务创建失败"));
verify(taskStatusBiz, times(1)).setFaceCutStatus(1L, 2);
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(999L);
when(scenicConfigFacade.isFaceSelectFirst(999L))
.thenReturn(false);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(scenicConfigFacade, times(1)).isFaceSelectFirst(999L);
verify(taskService, times(1)).autoCreateTaskByFaceId(1L);
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forAutoMatching(888L, true);
face.setId(888L);
context.setFace(face);
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenReturn(false);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(taskService, times(1)).autoCreateTaskByFaceId(888L);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
when(scenicConfigFacade.isFaceSelectFirst(10L))
.thenThrow(new NullPointerException("Null scenic config"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("任务创建失败"));
}
}

View File

@@ -0,0 +1,216 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.processor.FaceRecoveryStrategy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
/**
* FaceRecoveryStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class FaceRecoveryStageTest {
@Mock
private FaceRecoveryStrategy faceRecoveryStrategy;
@InjectMocks
private FaceRecoveryStage stage;
private FaceMatchingContext context;
private FaceEntity face;
private ScenicConfigManager scenicConfig;
private IFaceBodyAdapter faceBodyAdapter;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
scenicConfig = mock(ScenicConfigManager.class);
faceBodyAdapter = mock(IFaceBodyAdapter.class);
context.setFace(face);
context.setScenicConfig(scenicConfig);
context.setFaceBodyAdapter(faceBodyAdapter);
}
@Test
void testExecute_SearchResultNull_Skipped() {
// Given: searchResult为null
context.setSearchResult(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(faceRecoveryStrategy, never()).executeFaceRecoveryLogic(any(), any(), any(), anyLong());
}
@Test
void testExecute_NoRecoveryNeeded_Success() {
// Given: 不需要补救,返回原searchResult
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setScore(0.85f);
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, scenicConfig, faceBodyAdapter, 10L))
.thenReturn(searchResult); // 返回同一个对象
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("无需补救"));
assertEquals(searchResult, context.getSearchResult());
verify(faceRecoveryStrategy, times(1))
.executeFaceRecoveryLogic(searchResult, scenicConfig, faceBodyAdapter, 10L);
}
@Test
void testExecute_RecoveryTriggered_Degraded() {
// Given: 触发补救,返回新的searchResult
SearchFaceRespVo originalResult = new SearchFaceRespVo();
originalResult.setScore(0.50f);
context.setSearchResult(originalResult);
SearchFaceRespVo recoveredResult = new SearchFaceRespVo();
recoveredResult.setScore(0.85f);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
originalResult, scenicConfig, faceBodyAdapter, 10L))
.thenReturn(recoveredResult); // 返回新对象
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("触发补救逻辑,重新搜索"));
assertEquals(recoveredResult, context.getSearchResult());
assertNotEquals(originalResult, context.getSearchResult());
}
@Test
void testExecute_RecoveryStrategyFailed_Degraded() {
// Given: 补救策略执行失败
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setScore(0.50f);
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, scenicConfig, faceBodyAdapter, 10L))
.thenThrow(new RuntimeException("Recovery strategy error"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("补救逻辑执行失败"));
verify(faceRecoveryStrategy, times(1))
.executeFaceRecoveryLogic(searchResult, scenicConfig, faceBodyAdapter, 10L);
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(888L);
SearchFaceRespVo searchResult = new SearchFaceRespVo();
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, scenicConfig, faceBodyAdapter, 888L))
.thenReturn(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(faceRecoveryStrategy, times(1))
.executeFaceRecoveryLogic(searchResult, scenicConfig, faceBodyAdapter, 888L);
}
@Test
void testExecute_NullFaceBodyAdapter() {
// Given: faceBodyAdapter为null
context.setFaceBodyAdapter(null);
SearchFaceRespVo searchResult = new SearchFaceRespVo();
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, scenicConfig, null, 10L))
.thenReturn(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(faceRecoveryStrategy, times(1))
.executeFaceRecoveryLogic(searchResult, scenicConfig, null, 10L);
}
@Test
void testExecute_NullScenicConfig() {
// Given: scenicConfig为null
context.setScenicConfig(null);
SearchFaceRespVo searchResult = new SearchFaceRespVo();
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, null, faceBodyAdapter, 10L))
.thenReturn(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(faceRecoveryStrategy, times(1))
.executeFaceRecoveryLogic(searchResult, null, faceBodyAdapter, 10L);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
SearchFaceRespVo searchResult = new SearchFaceRespVo();
context.setSearchResult(searchResult);
when(faceRecoveryStrategy.executeFaceRecoveryLogic(
searchResult, scenicConfig, faceBodyAdapter, 10L))
.thenThrow(new NullPointerException("Null strategy"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("补救逻辑执行失败"));
}
}

View File

@@ -0,0 +1,223 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.helper.PuzzleGenerationOrchestrator;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
* GeneratePuzzleStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class GeneratePuzzleStageTest {
@Mock
private PuzzleGenerationOrchestrator puzzleOrchestrator;
@InjectMocks
private GeneratePuzzleStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
face.setFaceUrl("http://example.com/face1.jpg");
context.setFace(face);
}
@Test
void testExecute_Success() {
// Given: 正常执行
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("拼图模板已提交异步生成"));
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, "http://example.com/face1.jpg");
}
@Test
void testExecute_EmptyFaceUrl_Success() {
// Given: faceUrl为空字符串
face.setFaceUrl("");
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, "");
}
@Test
void testExecute_NullFaceUrl_Success() {
// Given: faceUrl为null
face.setFaceUrl(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, null);
}
@Test
void testExecute_OrchestratorFailed_Degraded() {
// Given: orchestrator执行失败
doThrow(new RuntimeException("Orchestrator error"))
.when(puzzleOrchestrator).generateAllTemplatesAsync(
10L, 1L, 100L, "http://example.com/face1.jpg");
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("提交拼图生成任务失败"));
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, "http://example.com/face1.jpg");
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(888L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(888L, 1L, 100L, "http://example.com/face1.jpg");
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forAutoMatching(777L, true);
face.setId(777L);
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 777L, 100L, "http://example.com/face1.jpg");
}
@Test
void testExecute_DifferentMemberId() {
// Given: 不同的memberId
face.setMemberId(999L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 999L, "http://example.com/face1.jpg");
}
@Test
void testExecute_DifferentFaceUrl() {
// Given: 不同的faceUrl
face.setFaceUrl("http://cdn.example.com/face999.jpg");
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, "http://cdn.example.com/face999.jpg");
}
@Test
void testExecute_LongFaceUrl() {
// Given: 很长的faceUrl
String longUrl = "http://cdn.example.com/very/long/path/to/face/image/with/many/segments/" +
"face_id_1234567890_scenic_10_member_100_timestamp_1234567890.jpg";
face.setFaceUrl(longUrl);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, longUrl);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
doThrow(new NullPointerException("Null orchestrator"))
.when(puzzleOrchestrator).generateAllTemplatesAsync(
anyLong(), anyLong(), anyLong(), anyString());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("提交拼图生成任务失败"));
}
@Test
void testExecute_IllegalArgumentException_Degraded() {
// Given: 非法参数异常
doThrow(new IllegalArgumentException("Invalid face URL"))
.when(puzzleOrchestrator).generateAllTemplatesAsync(
10L, 1L, 100L, "http://example.com/face1.jpg");
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("提交拼图生成任务失败"));
}
@Test
void testExecute_OldUser() {
// Given: 老用户
context = FaceMatchingContext.forAutoMatching(1L, false);
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(puzzleOrchestrator, times(1))
.generateAllTemplatesAsync(10L, 1L, 100L, "http://example.com/face1.jpg");
}
}

View File

@@ -0,0 +1,285 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.VideoRecreationHandler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
/**
* HandleVideoRecreationStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class HandleVideoRecreationStageTest {
@Mock
private VideoRecreationHandler videoRecreationHandler;
@InjectMocks
private HandleVideoRecreationStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
context.setFace(face);
}
@Test
void testExecute_MemberSourceListNull_Skipped() {
// Given: memberSourceList为null
context.setMemberSourceList(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(videoRecreationHandler, never()).handleVideoRecreation(
anyLong(), anyList(), anyLong(), anyLong(), anyList(), anyBoolean());
}
@Test
void testExecute_MemberSourceListEmpty_Skipped() {
// Given: memberSourceList为空
context.setMemberSourceList(new ArrayList<>());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(videoRecreationHandler, never()).handleVideoRecreation(
anyLong(), anyList(), anyLong(), anyLong(), anyList(), anyBoolean());
}
@Test
void testExecute_Success_WithSampleListIds() {
// Given: 有memberSourceList和sampleListIds
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L, 102L, 103L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("视频重切处理完成"));
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, sampleListIds, true);
}
@Test
void testExecute_Success_NullSampleListIds() {
// Given: sampleListIds为null
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
context.setSampleListIds(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, null, true);
}
@Test
void testExecute_Success_EmptySampleListIds() {
// Given: sampleListIds为空
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
context.setSampleListIds(new ArrayList<>());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, new ArrayList<>(), true);
}
@Test
void testExecute_OldUser() {
// Given: 老用户
context = FaceMatchingContext.forAutoMatching(1L, false);
context.setFace(face);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, sampleListIds, false);
}
@Test
void testExecute_HandlerFailed_Degraded() {
// Given: 处理器执行失败
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L);
context.setSampleListIds(sampleListIds);
doThrow(new RuntimeException("Handler error"))
.when(videoRecreationHandler).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, sampleListIds, true);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("视频重切处理失败"));
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, sampleListIds, true);
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(888L);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
888L, memberSourceList, 1L, 100L, sampleListIds, true);
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forAutoMatching(777L, true);
face.setId(777L);
context.setFace(face);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 777L, 100L, sampleListIds, true);
}
@Test
void testExecute_DifferentMemberId() {
// Given: 不同的memberId
face.setMemberId(999L);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 999L, sampleListIds, true);
}
@Test
void testExecute_ManySources() {
// Given: 大量源文件
List<MemberSourceEntity> memberSourceList = createMemberSourceList(20);
context.setMemberSourceList(memberSourceList);
List<Long> sampleListIds = Arrays.asList(101L, 102L, 103L, 104L, 105L);
context.setSampleListIds(sampleListIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(videoRecreationHandler, times(1)).handleVideoRecreation(
10L, memberSourceList, 1L, 100L, sampleListIds, true);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
doThrow(new NullPointerException("Null handler"))
.when(videoRecreationHandler).handleVideoRecreation(
anyLong(), anyList(), anyLong(), anyLong(), anyList(), anyBoolean());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("视频重切处理失败"));
}
private List<MemberSourceEntity> createMemberSourceList(int count) {
List<MemberSourceEntity> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
MemberSourceEntity entity = new MemberSourceEntity();
entity.setMemberId(100L);
entity.setSourceId((long) (i + 1));
list.add(entity);
}
return list;
}
}

View File

@@ -0,0 +1,262 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.BuyStatusProcessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.*;
/**
* ProcessBuyStatusStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class ProcessBuyStatusStageTest {
@Mock
private BuyStatusProcessor buyStatusProcessor;
@InjectMocks
private ProcessBuyStatusStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
context.setFace(face);
}
@Test
void testExecute_MemberSourceListNull_Skipped() {
// Given: memberSourceList为null
context.setMemberSourceList(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(buyStatusProcessor, never()).processBuyStatus(anyList(), anyList(), anyLong(), anyLong(), anyLong());
}
@Test
void testExecute_MemberSourceListEmpty_Skipped() {
// Given: memberSourceList为空
context.setMemberSourceList(new ArrayList<>());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(buyStatusProcessor, never()).processBuyStatus(anyList(), anyList(), anyLong(), anyLong(), anyLong());
}
@Test
void testExecute_Success_WithFreeSource() {
// Given: 有免费源文件
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L, 2L, 3L);
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("购买状态处理完成"));
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 1L);
}
@Test
void testExecute_Success_NoFreeSource() {
// Given: 没有免费源文件(freeSourceIds为空)
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = new ArrayList<>();
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 1L);
}
@Test
void testExecute_Success_NullFreeSource() {
// Given: freeSourceIds为null
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
context.setFreeSourceIds(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, null, 100L, 10L, 1L);
}
@Test
void testExecute_ProcessorFailed_Degraded() {
// Given: 处理器执行失败
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L);
context.setFreeSourceIds(freeSourceIds);
doThrow(new RuntimeException("Processor error"))
.when(buyStatusProcessor).processBuyStatus(
memberSourceList, freeSourceIds, 100L, 10L, 1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("购买状态处理失败"));
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 1L);
}
@Test
void testExecute_DifferentMemberId() {
// Given: 不同的memberId
face.setMemberId(999L);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L);
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 999L, 10L, 1L);
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(888L);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L);
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 888L, 1L);
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forAutoMatching(777L, true);
face.setId(777L);
context.setFace(face);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L);
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 777L);
}
@Test
void testExecute_AllSourcesFree() {
// Given: 所有源文件都免费
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);
context.setFreeSourceIds(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(buyStatusProcessor, times(1))
.processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 1L);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
doThrow(new NullPointerException("Null processor"))
.when(buyStatusProcessor).processBuyStatus(
anyList(), anyList(), anyLong(), anyLong(), anyLong());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("购买状态处理失败"));
}
private List<MemberSourceEntity> createMemberSourceList(int count) {
List<MemberSourceEntity> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
MemberSourceEntity entity = new MemberSourceEntity();
entity.setMemberId(100L);
entity.setSourceId((long) (i + 1));
list.add(entity);
}
return list;
}
}

View File

@@ -0,0 +1,244 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.service.pc.processor.SourceRelationProcessor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* ProcessFreeSourceStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class ProcessFreeSourceStageTest {
@Mock
private SourceRelationProcessor sourceRelationProcessor;
@InjectMocks
private ProcessFreeSourceStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
context.setFace(face);
}
@Test
void testExecute_MemberSourceListNull_Skipped() {
// Given: memberSourceList为null
context.setMemberSourceList(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(sourceRelationProcessor, never())
.processFreeSourceLogic(anyList(), anyLong(), anyBoolean());
}
@Test
void testExecute_MemberSourceListEmpty_Skipped() {
// Given: memberSourceList为空
context.setMemberSourceList(new ArrayList<>());
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
verify(sourceRelationProcessor, never())
.processFreeSourceLogic(anyList(), anyLong(), anyBoolean());
}
@Test
void testExecute_Success_WithFreeSource() {
// Given: 有免费源文件
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L, 2L, 3L);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenReturn(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("确定了3个免费源文件"));
assertEquals(freeSourceIds, context.getFreeSourceIds());
verify(sourceRelationProcessor, times(1))
.processFreeSourceLogic(memberSourceList, 10L, true);
}
@Test
void testExecute_Success_NoFreeSource() {
// Given: 没有免费源文件
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = new ArrayList<>();
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenReturn(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("确定了0个免费源文件"));
assertEquals(freeSourceIds, context.getFreeSourceIds());
}
@Test
void testExecute_Success_NullFreeSource() {
// Given: 返回null(无免费源文件)
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenReturn(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("确定了0个免费源文件"));
assertNull(context.getFreeSourceIds());
}
@Test
void testExecute_OldUser() {
// Given: 老用户
context = FaceMatchingContext.forAutoMatching(1L, false);
context.setFace(face);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, false))
.thenReturn(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(sourceRelationProcessor, times(1))
.processFreeSourceLogic(memberSourceList, 10L, false);
}
@Test
void testExecute_ProcessorFailed_Degraded() {
// Given: 处理器执行失败
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenThrow(new RuntimeException("Processor error"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("免费源文件处理失败"));
verify(sourceRelationProcessor, times(1))
.processFreeSourceLogic(memberSourceList, 10L, true);
}
@Test
void testExecute_DifferentScenicId() {
// Given: 不同的景区ID
face.setScenicId(888L);
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L, 2L);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 888L, true))
.thenReturn(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(sourceRelationProcessor, times(1))
.processFreeSourceLogic(memberSourceList, 888L, true);
}
@Test
void testExecute_AllSourcesFree() {
// Given: 所有源文件都免费
List<MemberSourceEntity> memberSourceList = createMemberSourceList(5);
context.setMemberSourceList(memberSourceList);
List<Long> freeSourceIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenReturn(freeSourceIds);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("确定了5个免费源文件"));
assertEquals(5, context.getFreeSourceIds().size());
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
List<MemberSourceEntity> memberSourceList = createMemberSourceList(3);
context.setMemberSourceList(memberSourceList);
when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true))
.thenThrow(new NullPointerException("Null processor"));
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("免费源文件处理失败"));
}
private List<MemberSourceEntity> createMemberSourceList(int count) {
List<MemberSourceEntity> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
MemberSourceEntity entity = new MemberSourceEntity();
entity.setMemberId(100L);
entity.setSourceId((long) (i + 1));
list.add(entity);
}
return list;
}
}

View File

@@ -0,0 +1,153 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* RecordCustomMatchMetricsStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class RecordCustomMatchMetricsStageTest {
@Mock
private FaceMetricsRecorder metricsRecorder;
@InjectMocks
private RecordCustomMatchMetricsStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
}
@Test
void testExecute_CustomMatchingScene_Success() {
// Given: 自定义匹配场景
context = FaceMatchingContext.forCustomMatching(1L, Arrays.asList(101L, 102L));
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("自定义匹配指标记录完成"));
assertEquals(FaceMatchingScene.CUSTOM_MATCHING, context.getScene());
verify(metricsRecorder, times(1)).recordCustomMatchCount(1L);
}
@Test
void testExecute_AutoMatchingScene_Skipped() {
// Given: 自动匹配场景
context = FaceMatchingContext.forAutoMatching(1L, true);
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSkipped());
assertEquals(FaceMatchingScene.AUTO_MATCHING, context.getScene());
verify(metricsRecorder, never()).recordCustomMatchCount(anyLong());
}
@Test
void testExecute_RecordFailed_Degraded() {
// Given: 自定义匹配场景,但记录失败
context = FaceMatchingContext.forCustomMatching(1L, Arrays.asList(101L, 102L));
context.setFace(face);
doThrow(new RuntimeException("Metrics record error"))
.when(metricsRecorder).recordCustomMatchCount(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("指标记录失败"));
verify(metricsRecorder, times(1)).recordCustomMatchCount(1L);
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forCustomMatching(888L, Arrays.asList(101L, 102L));
face.setId(888L);
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(metricsRecorder, times(1)).recordCustomMatchCount(888L);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
context = FaceMatchingContext.forCustomMatching(1L, Arrays.asList(101L, 102L));
context.setFace(face);
doThrow(new NullPointerException("Null recorder"))
.when(metricsRecorder).recordCustomMatchCount(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("指标记录失败"));
}
@Test
void testExecute_MultipleFaceSamples() {
// Given: 多个faceSample
context = FaceMatchingContext.forCustomMatching(
1L,
Arrays.asList(101L, 102L, 103L, 104L, 105L));
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(metricsRecorder, times(1)).recordCustomMatchCount(1L);
}
@Test
void testExecute_SingleFaceSample() {
// Given: 单个faceSample
context = FaceMatchingContext.forCustomMatching(1L, Arrays.asList(101L));
context.setFace(face);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(metricsRecorder, times(1)).recordCustomMatchCount(1L);
}
}

View File

@@ -0,0 +1,162 @@
package com.ycwl.basic.face.pipeline.stages;
import com.ycwl.basic.face.pipeline.core.FaceMatchingContext;
import com.ycwl.basic.face.pipeline.core.StageResult;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.pc.helper.FaceMetricsRecorder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* RecordMetricsStage 单元测试
*/
@ExtendWith(MockitoExtension.class)
class RecordMetricsStageTest {
@Mock
private FaceMetricsRecorder metricsRecorder;
@InjectMocks
private RecordMetricsStage stage;
private FaceMatchingContext context;
private FaceEntity face;
@BeforeEach
void setUp() {
context = FaceMatchingContext.forAutoMatching(1L, true);
face = new FaceEntity();
face.setId(1L);
face.setMemberId(100L);
face.setScenicId(10L);
context.setFace(face);
}
@Test
void testExecute_NoSearchResult_Success() {
// Given: searchResult为null
context.setSearchResult(null);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("识别指标记录完成"));
verify(metricsRecorder, times(1)).recordRecognitionCount(1L);
verify(metricsRecorder, never()).recordLowThreshold(anyLong());
}
@Test
void testExecute_SearchResultNotLowThreshold_Success() {
// Given: searchResult不为null,但未触发低阈值
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setLowThreshold(false);
context.setSearchResult(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(metricsRecorder, times(1)).recordRecognitionCount(1L);
verify(metricsRecorder, never()).recordLowThreshold(anyLong());
}
@Test
void testExecute_LowThresholdTriggered_Success() {
// Given: searchResult触发低阈值
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setLowThreshold(true);
context.setSearchResult(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
assertTrue(result.getMessage().contains("识别指标记录完成"));
verify(metricsRecorder, times(1)).recordRecognitionCount(1L);
verify(metricsRecorder, times(1)).recordLowThreshold(1L);
}
@Test
void testExecute_RecordCountFailed_Degraded() {
// Given: 记录识别次数失败
doThrow(new RuntimeException("Record error"))
.when(metricsRecorder).recordRecognitionCount(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("指标记录失败"));
verify(metricsRecorder, times(1)).recordRecognitionCount(1L);
verify(metricsRecorder, never()).recordLowThreshold(anyLong());
}
@Test
void testExecute_RecordLowThresholdFailed_Degraded() {
// Given: 记录低阈值失败
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setLowThreshold(true);
context.setSearchResult(searchResult);
doThrow(new RuntimeException("Low threshold record error"))
.when(metricsRecorder).recordLowThreshold(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("指标记录失败"));
verify(metricsRecorder, times(1)).recordRecognitionCount(1L);
verify(metricsRecorder, times(1)).recordLowThreshold(1L);
}
@Test
void testExecute_DifferentFaceId() {
// Given: 不同的faceId
context = FaceMatchingContext.forAutoMatching(999L, true);
face.setId(999L);
context.setFace(face);
SearchFaceRespVo searchResult = new SearchFaceRespVo();
searchResult.setLowThreshold(true);
context.setSearchResult(searchResult);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isSuccess());
verify(metricsRecorder, times(1)).recordRecognitionCount(999L);
verify(metricsRecorder, times(1)).recordLowThreshold(999L);
}
@Test
void testExecute_NullPointerException_Degraded() {
// Given: 空指针异常
doThrow(new NullPointerException("Null recorder"))
.when(metricsRecorder).recordRecognitionCount(1L);
// When
StageResult<FaceMatchingContext> result = stage.execute(context);
// Then
assertTrue(result.isDegraded());
assertTrue(result.getMessage().contains("指标记录失败"));
}
}