From ecd5378b26c24e01d392be929918a5237f2ecf91 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 3 Dec 2025 19:23:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(pipeline):=20=E5=A2=9E=E5=8A=A0=E9=98=B2?= =?UTF-8?q?=E5=BE=A1=E6=80=A7=E6=A3=80=E6=9F=A5=E9=81=BF=E5=85=8D=E7=A9=BA?= =?UTF-8?q?=E6=8C=87=E9=92=88=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多个阶段中增加对 memberSourceList 和 searchResult 的空值检查 - 当 memberSourceList 为空时跳过视频重切和购买状态处理逻辑 - 当 searchResult 为空时跳过人脸补救逻辑 - 增加对自定义匹配场景的判断,非该场景则跳过指标记录 - 为各个阶段添加详细的单元测试覆盖各种边界条件 --- .../pipeline/stages/FaceRecoveryStage.java | 6 + .../stages/HandleVideoRecreationStage.java | 6 + .../stages/ProcessBuyStatusStage.java | 6 + .../stages/ProcessFreeSourceStage.java | 6 + .../stages/RecordCustomMatchMetricsStage.java | 6 + .../pipeline/stages/CreateTaskStageTest.java | 185 ++++++++++++ .../stages/FaceRecoveryStageTest.java | 216 +++++++++++++ .../stages/GeneratePuzzleStageTest.java | 223 ++++++++++++++ .../HandleVideoRecreationStageTest.java | 285 ++++++++++++++++++ .../stages/ProcessBuyStatusStageTest.java | 262 ++++++++++++++++ .../stages/ProcessFreeSourceStageTest.java | 244 +++++++++++++++ .../RecordCustomMatchMetricsStageTest.java | 153 ++++++++++ .../stages/RecordMetricsStageTest.java | 162 ++++++++++ 13 files changed, 1760 insertions(+) create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/GeneratePuzzleStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/HandleVideoRecreationStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessFreeSourceStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStageTest.java create mode 100644 src/test/java/com/ycwl/basic/face/pipeline/stages/RecordMetricsStageTest.java diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStage.java index fe40ec79..7d4ab2da 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStage.java @@ -54,6 +54,12 @@ public class FaceRecoveryStage extends AbstractFaceMatchingStage 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( diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStage.java index 9f3f6d18..357f51b9 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStage.java @@ -62,6 +62,12 @@ public class ProcessBuyStatusStage extends AbstractFaceMatchingStage freeSourceIds = sourceRelationProcessor.processFreeSourceLogic( diff --git a/src/main/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStage.java b/src/main/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStage.java index 0aefe180..98641ae8 100644 --- a/src/main/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStage.java +++ b/src/main/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStage.java @@ -51,6 +51,12 @@ public class RecordCustomMatchMetricsStage extends AbstractFaceMatchingStage 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); diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java new file mode 100644 index 00000000..6dd4a2c6 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/CreateTaskStageTest.java @@ -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 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 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 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 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 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 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 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 result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("任务创建失败")); + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStageTest.java new file mode 100644 index 00000000..3cf79278 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/FaceRecoveryStageTest.java @@ -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 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 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 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 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 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 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 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 result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("补救逻辑执行失败")); + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/GeneratePuzzleStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/GeneratePuzzleStageTest.java new file mode 100644 index 00000000..2c0ef3c7 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/GeneratePuzzleStageTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 result = stage.execute(context); + + // Then + assertTrue(result.isSuccess()); + verify(puzzleOrchestrator, times(1)) + .generateAllTemplatesAsync(10L, 1L, 100L, "http://example.com/face1.jpg"); + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/HandleVideoRecreationStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/HandleVideoRecreationStageTest.java new file mode 100644 index 00000000..36a16b1e --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/HandleVideoRecreationStageTest.java @@ -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 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 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L, 102L, 103L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + context.setSampleListIds(null); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + context.setSampleListIds(new ArrayList<>()); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L); + context.setSampleListIds(sampleListIds); + + doThrow(new RuntimeException("Handler error")) + .when(videoRecreationHandler).handleVideoRecreation( + 10L, memberSourceList, 1L, 100L, sampleListIds, true); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(20); + context.setMemberSourceList(memberSourceList); + + List sampleListIds = Arrays.asList(101L, 102L, 103L, 104L, 105L); + context.setSampleListIds(sampleListIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + doThrow(new NullPointerException("Null handler")) + .when(videoRecreationHandler).handleVideoRecreation( + anyLong(), anyList(), anyLong(), anyLong(), anyList(), anyBoolean()); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("视频重切处理失败")); + } + + private List createMemberSourceList(int count) { + List 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; + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStageTest.java new file mode 100644 index 00000000..1a0e61d1 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessBuyStatusStageTest.java @@ -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 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 result = stage.execute(context); + + // Then + assertTrue(result.isSkipped()); + verify(buyStatusProcessor, never()).processBuyStatus(anyList(), anyList(), anyLong(), anyLong(), anyLong()); + } + + @Test + void testExecute_Success_WithFreeSource() { + // Given: 有免费源文件 + List memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L, 2L, 3L); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = new ArrayList<>(); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + context.setFreeSourceIds(null); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L); + context.setFreeSourceIds(freeSourceIds); + + doThrow(new RuntimeException("Processor error")) + .when(buyStatusProcessor).processBuyStatus( + memberSourceList, freeSourceIds, 100L, 10L, 1L); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isSuccess()); + verify(buyStatusProcessor, times(1)) + .processBuyStatus(memberSourceList, freeSourceIds, 100L, 10L, 777L); + } + + @Test + void testExecute_AllSourcesFree() { + // Given: 所有源文件都免费 + List memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L, 2L, 3L, 4L, 5L); + context.setFreeSourceIds(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + doThrow(new NullPointerException("Null processor")) + .when(buyStatusProcessor).processBuyStatus( + anyList(), anyList(), anyLong(), anyLong(), anyLong()); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("购买状态处理失败")); + } + + private List createMemberSourceList(int count) { + List 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; + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessFreeSourceStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessFreeSourceStageTest.java new file mode 100644 index 00000000..9ae3722e --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/ProcessFreeSourceStageTest.java @@ -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 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 result = stage.execute(context); + + // Then + assertTrue(result.isSkipped()); + verify(sourceRelationProcessor, never()) + .processFreeSourceLogic(anyList(), anyLong(), anyBoolean()); + } + + @Test + void testExecute_Success_WithFreeSource() { + // Given: 有免费源文件 + List memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L, 2L, 3L); + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenReturn(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = new ArrayList<>(); + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenReturn(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenReturn(null); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L); + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, false)) + .thenReturn(freeSourceIds); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isSuccess()); + verify(sourceRelationProcessor, times(1)) + .processFreeSourceLogic(memberSourceList, 10L, false); + } + + @Test + void testExecute_ProcessorFailed_Degraded() { + // Given: 处理器执行失败 + List memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenThrow(new RuntimeException("Processor error")); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L, 2L); + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 888L, true)) + .thenReturn(freeSourceIds); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isSuccess()); + verify(sourceRelationProcessor, times(1)) + .processFreeSourceLogic(memberSourceList, 888L, true); + } + + @Test + void testExecute_AllSourcesFree() { + // Given: 所有源文件都免费 + List memberSourceList = createMemberSourceList(5); + context.setMemberSourceList(memberSourceList); + + List freeSourceIds = Arrays.asList(1L, 2L, 3L, 4L, 5L); + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenReturn(freeSourceIds); + + // When + StageResult 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 memberSourceList = createMemberSourceList(3); + context.setMemberSourceList(memberSourceList); + + when(sourceRelationProcessor.processFreeSourceLogic(memberSourceList, 10L, true)) + .thenThrow(new NullPointerException("Null processor")); + + // When + StageResult result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("免费源文件处理失败")); + } + + private List createMemberSourceList(int count) { + List 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; + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStageTest.java new file mode 100644 index 00000000..512d78e9 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordCustomMatchMetricsStageTest.java @@ -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 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 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 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 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 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 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 result = stage.execute(context); + + // Then + assertTrue(result.isSuccess()); + verify(metricsRecorder, times(1)).recordCustomMatchCount(1L); + } +} diff --git a/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordMetricsStageTest.java b/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordMetricsStageTest.java new file mode 100644 index 00000000..c198d604 --- /dev/null +++ b/src/test/java/com/ycwl/basic/face/pipeline/stages/RecordMetricsStageTest.java @@ -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 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 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 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 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 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 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 result = stage.execute(context); + + // Then + assertTrue(result.isDegraded()); + assertTrue(result.getMessage().contains("指标记录失败")); + } +}