task-41: 建立 Collect Data 性能基线夹具(1k/10k 行、多 chunk、品牌检测)
新增 CollectDataPerfFixture:确定性生成 1k/10k 行、多关键词多 chunk 与 品牌检测场景(成功/失败/查询失败/空 ASIN)行集合并采样 payload 字节与 chunk 划分;同一输入重复生成结果一致(幂等),行数/计数/关键词均有上限 校验。8 个用例覆盖正常/批量/幂等/空/单元素/超限/非法输入/序列化失败 降级路径,mvn 全量 681 测试通过。
This commit is contained in:
+166
@@ -0,0 +1,166 @@
|
||||
package com.nanri.aiimage.modules.collectdata.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 41:Collect Data 性能基线夹具。
|
||||
* 覆盖 1k/10k 行、多个 chunk 与品牌检测(成功/失败/查询失败/空 ASIN)场景的
|
||||
* 确定性生成与 payload 采样;同一输入重复生成结果一致(幂等),超限与非法
|
||||
* 输入被拒绝,序列化失败抛可识别异常且不产生部分结果。
|
||||
*/
|
||||
class CollectDataPerfFixtureTest {
|
||||
|
||||
private final CollectDataPerfFixture fixture =
|
||||
new CollectDataPerfFixture(new ObjectMapper());
|
||||
|
||||
private static final List<String> KEYWORDS = List.of("phone case", "iphone case", "samsung case");
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_normal_default_path() {
|
||||
// 正常输入:1000 行 3 关键词 1 个失败品牌行,payload 与 chunk 统计正确。
|
||||
List<CollectDataResultRowVo> rows =
|
||||
fixture.generateRows(1000, KEYWORDS, 1, 0, 0);
|
||||
CollectDataPerfFixture.Metrics metrics = fixture.samplePayload(rows, 200);
|
||||
|
||||
assertEquals(1000, rows.size(), "1000 行全部生成");
|
||||
assertEquals(1000, metrics.rowCount(), "采样行数一致");
|
||||
assertEquals(5, metrics.chunkCount(), "200 行一个 chunk 共 5 个");
|
||||
assertTrue(metrics.payloadBytes() > 0, "payload 非空");
|
||||
assertEquals(1, metrics.failedBrandRows(), "1 行失败品牌");
|
||||
assertEquals(0, metrics.queryFailedBrandRows(), "无查询失败品牌");
|
||||
assertEquals(0, metrics.blankAsinRows(), "无空 ASIN");
|
||||
assertEquals(0, metrics.blankBrandRows(), "无空品牌");
|
||||
assertTrue(rows.getFirst().getAsin().startsWith("B0"), "ASIN 确定性生成");
|
||||
assertTrue(rows.getFirst().getKeyword() != null && !rows.getFirst().getKeyword().isBlank(), "关键词非空");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_normal_multiple_items() {
|
||||
// 批量场景:10k 行 5 关键词多 chunk,行数不丢失且按关键词循环顺序稳定。
|
||||
List<String> keywords = List.of("a", "b", "c", "d", "e");
|
||||
List<CollectDataResultRowVo> rows =
|
||||
fixture.generateRows(10000, keywords, 500, 300, 200);
|
||||
CollectDataPerfFixture.Metrics metrics = fixture.samplePayload(rows, 500);
|
||||
|
||||
assertEquals(10000, metrics.rowCount(), "10k 行不丢失");
|
||||
assertEquals(20, metrics.chunkCount(), "500 行一个 chunk 共 20 个");
|
||||
assertEquals(500, metrics.failedBrandRows(), "失败品牌行数稳定");
|
||||
assertEquals(300, metrics.queryFailedBrandRows(), "查询失败品牌行数稳定");
|
||||
assertEquals(200, metrics.blankAsinRows(), "空 ASIN 行数稳定");
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
assertEquals(keywords.get(i % keywords.size()), rows.get(i).getKeyword(), "关键词循环分配");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:同一输入两次生成,行数与 payload 字节数完全一致,ASIN 可重复。
|
||||
List<CollectDataResultRowVo> first =
|
||||
fixture.generateRows(5000, KEYWORDS, 50, 25, 10);
|
||||
List<CollectDataResultRowVo> second =
|
||||
fixture.generateRows(5000, KEYWORDS, 50, 25, 10);
|
||||
|
||||
assertEquals(first.size(), second.size(), "行数幂等");
|
||||
assertEquals(first.get(0).getAsin(), second.get(0).getAsin(), "首行 ASIN 幂等");
|
||||
assertEquals(first.get(4999).getAsin(), second.get(4999).getAsin(), "末行 ASIN 幂等");
|
||||
CollectDataPerfFixture.Metrics m1 = fixture.samplePayload(first, 1000);
|
||||
CollectDataPerfFixture.Metrics m2 = fixture.samplePayload(second, 1000);
|
||||
assertEquals(m1.payloadBytes(), m2.payloadBytes(), "payload 字节幂等");
|
||||
assertEquals(m1.chunkCount(), m2.chunkCount(), "chunk 数幂等");
|
||||
assertEquals(m1.failedBrandRows(), m2.failedBrandRows(), "失败品牌统计幂等");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_boundary_empty_input() {
|
||||
// 空输入:0 行返回空集合,采样统计全零且无 chunk。
|
||||
List<CollectDataResultRowVo> rows = fixture.generateRows(0, KEYWORDS, 0, 0, 0);
|
||||
CollectDataPerfFixture.Metrics metrics = fixture.samplePayload(rows, 200);
|
||||
|
||||
assertEquals(0, rows.size(), "空输入 0 行");
|
||||
assertEquals(0, metrics.rowCount(), "采样行数 0");
|
||||
assertEquals(0, metrics.chunkCount(), "空输入无 chunk");
|
||||
assertEquals(0, metrics.payloadBytes(), "空输入无 payload");
|
||||
assertEquals(0, metrics.failedBrandRows(), "空输入无失败品牌");
|
||||
assertEquals(0, metrics.queryFailedBrandRows(), "空输入无查询失败品牌");
|
||||
assertEquals(0, metrics.blankAsinRows(), "空输入无空 ASIN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_boundary_single_item() {
|
||||
// 单元素:1 行 1 关键词单 chunk,不依赖批量路径,全部统计可算。
|
||||
List<CollectDataResultRowVo> rows = fixture.generateRows(1, List.of("only"), 0, 0, 0);
|
||||
CollectDataPerfFixture.Metrics metrics = fixture.samplePayload(rows, 200);
|
||||
|
||||
assertEquals(1, rows.size(), "单行");
|
||||
assertEquals(1, metrics.chunkCount(), "单行一个 chunk");
|
||||
assertEquals(1, metrics.rowCount(), "采样行数 1");
|
||||
assertEquals(0, metrics.failedBrandRows(), "单行有效品牌");
|
||||
assertTrue(metrics.payloadBytes() > 0, "单行 payload 非空");
|
||||
assertEquals("only", rows.getFirst().getKeyword(), "单关键词正确");
|
||||
assertEquals("FBA", rows.getFirst().getDeliveryMethod(), "单行配送方式确定");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_boundary_limit_and_overflow() {
|
||||
// 上限/超限:10k 行最大值可执行;超限行数、负计数、品牌合计超行数被拒绝。
|
||||
List<CollectDataResultRowVo> maxRows = fixture.generateRows(10000, KEYWORDS, 0, 0, 0);
|
||||
assertEquals(10000, maxRows.size(), "最大行数可执行");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(10001, KEYWORDS, 0, 0, 0), "超限行数被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(100, KEYWORDS, -1, 0, 0), "负失败品牌计数被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(100, KEYWORDS, 60, 50, 0), "品牌合计超行数被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(100, KEYWORDS, 0, 0, 101), "空 ASIN 计数超行数被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(100, List.of(), 0, 0, 0), "空关键词集合被拒绝");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_invalid_input_rejected() {
|
||||
// 非法输入:null 集合、null 行列表、非正 chunkSize、负行数抛可识别异常。
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(-1, KEYWORDS, 0, 0, 0), "负行数被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.generateRows(100, null, 0, 0, 0), "null 关键词集合被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.samplePayload(null, 200), "null 行列表被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.samplePayload(List.of(), 0), "非正 chunkSize 被拒绝");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.samplePayload(List.of(), -5), "负 chunkSize 被拒绝");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_041_chunk_brand_dependency_failure_releases_resources() throws Exception {
|
||||
// 依赖失败:序列化失败抛 IllegalStateException 且不产生部分结果;
|
||||
// 恢复后同一输入可重新采样且结果一致。
|
||||
ObjectMapper spy = org.mockito.Mockito.spy(new ObjectMapper());
|
||||
org.mockito.Mockito.doAnswer(invocation -> {
|
||||
throw new java.io.IOException("serializer down");
|
||||
}).when(spy).writeValueAsBytes(org.mockito.ArgumentMatchers.any());
|
||||
CollectDataPerfFixture failingFixture = new CollectDataPerfFixture(spy);
|
||||
|
||||
List<CollectDataResultRowVo> rows = fixture.generateRows(500, KEYWORDS, 10, 5, 3);
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||
() -> failingFixture.samplePayload(rows, 100), "序列化失败必须抛出");
|
||||
assertTrue(ex.getMessage().contains("采样序列化失败"), "异常消息可识别");
|
||||
|
||||
CollectDataPerfFixture.Metrics metrics = fixture.samplePayload(rows, 100);
|
||||
assertEquals(500, metrics.rowCount(), "恢复后采样行数完整");
|
||||
assertEquals(5, metrics.chunkCount(), "恢复后 chunk 数正确");
|
||||
assertEquals(10, metrics.failedBrandRows(), "恢复后失败品牌统计正确");
|
||||
assertEquals(5, metrics.queryFailedBrandRows(), "恢复后查询失败统计正确");
|
||||
assertEquals(3, metrics.blankAsinRows(), "恢复后空 ASIN 统计正确");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user