diff --git a/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceDeduplicationTest.java b/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceDeduplicationTest.java index 84c49c1a..6560a69f 100644 --- a/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceDeduplicationTest.java +++ b/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceDeduplicationTest.java @@ -1,5 +1,6 @@ package com.ycwl.basic.puzzle.service.impl; +import com.ycwl.basic.face.pipeline.stages.CustomFaceSearchStage; import com.ycwl.basic.puzzle.dto.PuzzleGenerateRequest; import com.ycwl.basic.puzzle.dto.PuzzleGenerateResponse; import com.ycwl.basic.puzzle.entity.PuzzleElementEntity; @@ -17,8 +18,10 @@ import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.storage.StorageFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; import java.awt.image.BufferedImage; import java.util.ArrayList; @@ -36,6 +39,7 @@ import static org.mockito.Mockito.*; * @author Claude * @since 2025-01-21 */ +@SpringBootTest class PuzzleGenerateServiceDeduplicationTest { @Mock @@ -58,6 +62,7 @@ class PuzzleGenerateServiceDeduplicationTest { private PuzzleDuplicationDetector duplicationDetector; + @InjectMocks private PuzzleGenerateServiceImpl service; @BeforeEach @@ -65,17 +70,6 @@ class PuzzleGenerateServiceDeduplicationTest { MockitoAnnotations.openMocks(this); // 创建真实的 duplicationDetector 实例 duplicationDetector = new PuzzleDuplicationDetector(recordMapper); - - // 手动注入所有依赖 - service = new PuzzleGenerateServiceImpl( - templateMapper, - elementMapper, - recordMapper, - imageRenderer, - fillEngine, - scenicRepository, - duplicationDetector - ); } /** @@ -113,7 +107,7 @@ class PuzzleGenerateServiceDeduplicationTest { }).when(recordMapper).insert(any()); // Mock更新成功 - when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) + when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) .thenReturn(1); // 执行 @@ -125,7 +119,7 @@ class PuzzleGenerateServiceDeduplicationTest { assertNull(response.getOriginalRecordId()); verify(imageRenderer, times(1)).render(any(), any(), any()); // 确实进行了渲染 verify(recordMapper, times(1)).insert(any()); // 插入了一条记录 - verify(recordMapper, times(1)).updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); + verify(recordMapper, times(1)).updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); } /** @@ -163,7 +157,7 @@ class PuzzleGenerateServiceDeduplicationTest { assertEquals("https://example.com/old-image.jpg", response.getImageUrl()); // 复用的URL verify(imageRenderer, never()).render(any(), any(), any()); // 没有进行渲染 verify(recordMapper, never()).insert(any()); // 没有插入新记录 - verify(recordMapper, never()).updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); + verify(recordMapper, never()).updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt()); } /** @@ -240,7 +234,7 @@ class PuzzleGenerateServiceDeduplicationTest { return 1; }).when(recordMapper).insert(any()); - when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) + when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())) .thenReturn(1); // 执行两次生成 diff --git a/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImplTest.java b/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImplTest.java index a9048f9d..5c794941 100644 --- a/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImplTest.java +++ b/src/test/java/com/ycwl/basic/puzzle/service/impl/PuzzleGenerateServiceImplTest.java @@ -84,7 +84,7 @@ class PuzzleGenerateServiceImplTest { record.setId(555L); return 1; }).when(recordMapper).insert(any()); - when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); + when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); PuzzleGenerateRequest request = new PuzzleGenerateRequest(); request.setTemplateCode("ticket"); @@ -131,7 +131,7 @@ class PuzzleGenerateServiceImplTest { record.setId(777L); return 1; }).when(recordMapper).insert(any()); - when(recordMapper.updateSuccess(anyLong(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); + when(recordMapper.updateSuccess(anyLong(), anyString(), anyString(), anyLong(), anyInt(), anyInt(), anyInt())).thenReturn(1); PuzzleGenerateRequest request = new PuzzleGenerateRequest(); request.setTemplateCode("ticket"); diff --git a/src/test/java/com/ycwl/basic/utils/SnowFlakeUtilTest.java b/src/test/java/com/ycwl/basic/utils/SnowFlakeUtilTest.java new file mode 100644 index 00000000..59096e6a --- /dev/null +++ b/src/test/java/com/ycwl/basic/utils/SnowFlakeUtilTest.java @@ -0,0 +1,92 @@ +package com.ycwl.basic.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Test for SnowFlakeUtil to verify uniqueness under high concurrency. + */ +public class SnowFlakeUtilTest { + + private static final Logger log = LoggerFactory.getLogger(SnowFlakeUtilTest.class); + + @Test + public void testIdUniqueness() throws InterruptedException { + // Number of threads + int threadCount = 1000; + // Number of IDs per thread + int idCountPerThread = 10000; + // Total IDs expected + int totalIdCount = threadCount * idCountPerThread; + + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final Set idSet = ConcurrentHashMap.newKeySet(); + final CountDownLatch latch = new CountDownLatch(threadCount); + + // Use a set to capture any "suspicious" short IDs (potential raw timestamps) + final Set suspiciousIds = ConcurrentHashMap.newKeySet(); + + log.info("Starting concurrent snowflake ID generation test..."); + long start = System.currentTimeMillis(); + + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + for (int j = 0; j < idCountPerThread; j++) { + String id = SnowFlakeUtil.getId(); + idSet.add(id); + + // Check for the potential overflow bug where it returns raw timestamp + // A valid snowflake ID (around year 2025) should be much larger than a timestamp + // Current timestamp is approx 13 digits (1734...) + // Snowflake ID should be approx 18-19 digits + if (id.length() < 15) { + suspiciousIds.add(id); + } + } + } catch (Exception e) { + log.error("Error generating ID", e); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + long end = System.currentTimeMillis(); + executorService.shutdown(); + + log.info("Generated {} IDs in {} ms", totalIdCount, (end - start)); + + if (!suspiciousIds.isEmpty()) { + log.warn("Found {} suspicious IDs (likely raw timestamps due to sequence overflow): {}", suspiciousIds.size(), suspiciousIds); + // We might not fail the test for this if the user only asked for uniqueness, + // but it's good to report. + // However, if they are raw timestamps, they collide heavily if generated in the same ms. + } + + Assertions.assertEquals(totalIdCount, idSet.size(), "Duplicate IDs found!"); + log.info("Uniqueness test passed. Total unique IDs: {}", idSet.size()); + } + + @Test + public void testPerformanceSingleThread() { + long start = System.currentTimeMillis(); + int count = 100000; + for (int i = 0; i < count; i++) { + SnowFlakeUtil.getId(); + } + long end = System.currentTimeMillis(); + log.info("Generated {} IDs in {} ms (Single Thread)", count, (end - start)); + } +}