task-1: 建立 Similar ASIN 性能基线夹具 (1000/5000行、图片开关、chunk与payload采样)
新增 SimilarAsinPerfFixture 确定性夹具:按 sourceFileKey+rowIndex 派生字段, 支持 0/1/1000/5000 行、图片开关两种模式、chunk 划分与 payload 字节采样; 空输入返回空、超限(>5000行/非法chunk/空key)抛 IllegalArgumentException。 含 10 个测试覆盖默认路径/批量/幂等/边界/非法输入/依赖失败恢复。
This commit is contained in:
+198
@@ -0,0 +1,198 @@
|
||||
package com.nanri.aiimage.modules.similarasin.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 1:Similar ASIN 性能基线夹具(1000/5000 行、图片开关、chunk 数与 payload 大小采样)。
|
||||
* 先写测试确认 RED,再实现 SimilarAsinPerfFixture。
|
||||
*/
|
||||
class SimilarAsinPerfFixtureTest {
|
||||
|
||||
private final SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(new ObjectMapper());
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_normal_default_path() {
|
||||
// 1000 行、带图片开关,默认 chunk 大小
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/base.xlsx", 1000, true);
|
||||
assertEquals(1000, rows.size());
|
||||
// 行必须包含 url 图片地址
|
||||
assertFalse(rows.get(0).getUrl().isBlank());
|
||||
assertTrue(rows.get(0).getUrl().startsWith("http"));
|
||||
// rowToken 稳定且唯一
|
||||
assertEquals(rows.get(0).getRowToken(), fixture.rowTokenFor(rows.get(0).getSourceFileKey(), rows.get(0).getRowIndex()));
|
||||
assertEquals(1000, rows.stream().map(SimilarAsinParsedRowVo::getRowToken).distinct().count());
|
||||
|
||||
List<List<SimilarAsinParsedRowVo>> chunks = fixture.splitChunks(rows, 200);
|
||||
assertEquals(5, chunks.size());
|
||||
assertEquals(200, chunks.get(0).size());
|
||||
|
||||
SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200);
|
||||
assertEquals(1000, metrics.rowCount());
|
||||
assertEquals(5, metrics.chunkCount());
|
||||
assertTrue(metrics.payloadBytes() > 0, "payload 采样字节数必须大于 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_normal_multiple_items() {
|
||||
// 5000 行批量场景:顺序稳定、chunk 数正确、结果不丢失
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/multi.xlsx", 5000, false);
|
||||
assertEquals(5000, rows.size());
|
||||
assertTrue(rows.get(0).getUrl().isBlank(), "图片开关关闭时 url 必须为空");
|
||||
|
||||
List<List<SimilarAsinParsedRowVo>> chunks = fixture.splitChunks(rows, 200);
|
||||
assertEquals(25, chunks.size());
|
||||
// 顺序稳定:拼接后与原始一致
|
||||
List<SimilarAsinParsedRowVo> restored = chunks.stream().flatMap(List::stream).toList();
|
||||
assertEquals(rows.size(), restored.size());
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
assertEquals(rows.get(i).getRowToken(), restored.get(i).getRowToken());
|
||||
}
|
||||
// 所有行 rowIndex 递增
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
assertEquals(i + 1, rows.get(i).getRowIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_normal_repeated_operation_is_idempotent() {
|
||||
List<SimilarAsinParsedRowVo> first = fixture.generateRows("uploads/20260829/idem.xlsx", 1000, true);
|
||||
List<SimilarAsinParsedRowVo> second = fixture.generateRows("uploads/20260829/idem.xlsx", 1000, true);
|
||||
// 同一输入重复生成:token 完全一致,不产生重复差异
|
||||
assertEquals(first.size(), second.size());
|
||||
for (int i = 0; i < first.size(); i++) {
|
||||
assertEquals(first.get(i).getRowToken(), second.get(i).getRowToken());
|
||||
assertEquals(first.get(i).getAsin(), second.get(i).getAsin());
|
||||
}
|
||||
// splitChunks 幂等:两次划分 chunk 数一致
|
||||
assertEquals(fixture.splitChunks(first, 200).size(), fixture.splitChunks(second, 200).size());
|
||||
// 采样指标幂等
|
||||
SimilarAsinPerfFixture.Metrics m1 = fixture.samplePayload(first, true, 200);
|
||||
SimilarAsinPerfFixture.Metrics m2 = fixture.samplePayload(second, true, 200);
|
||||
assertEquals(m1.payloadBytes(), m2.payloadBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_boundary_empty_input() {
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/empty.xlsx", 0, true);
|
||||
assertNotNull(rows);
|
||||
assertEquals(0, rows.size());
|
||||
// 空行 splitChunks 返回空,不产生无效 chunk
|
||||
assertEquals(0, fixture.splitChunks(rows, 200).size());
|
||||
// 空行采样:行数 0、chunk 0、payload 字节数为 0(不创建任何载荷)
|
||||
SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200);
|
||||
assertEquals(0, metrics.rowCount());
|
||||
assertEquals(0, metrics.chunkCount());
|
||||
assertEquals(0, metrics.payloadBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_boundary_single_item() {
|
||||
// 单行:不依赖批量路径且结果正确
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/single.xlsx", 1, true);
|
||||
assertEquals(1, rows.size());
|
||||
assertEquals(1, rows.get(0).getRowIndex());
|
||||
assertEquals(1, fixture.splitChunks(rows, 200).size());
|
||||
// 单行 chunk 划分后仍只含 1 行
|
||||
List<List<SimilarAsinParsedRowVo>> chunks = fixture.splitChunks(rows, 200);
|
||||
assertEquals(1, chunks.get(0).size());
|
||||
SimilarAsinPerfFixture.Metrics metrics = fixture.samplePayload(rows, true, 200);
|
||||
assertEquals(1, metrics.rowCount());
|
||||
assertEquals(1, metrics.chunkCount());
|
||||
assertTrue(metrics.payloadBytes() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_boundary_limit_and_overflow() {
|
||||
// 超过最大行数(5000)时拒绝,不发生无界内存增长
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.generateRows("uploads/20260829/overflow.xlsx", 5001, true));
|
||||
// 达到最大允许值 5000 时允许
|
||||
assertEquals(5000, fixture.generateRows("uploads/20260829/max.xlsx", 5000, true).size());
|
||||
// chunkSize 非法值拒绝
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(fixture.generateRows("uploads/20260829/a.xlsx", 100, true), 0));
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(fixture.generateRows("uploads/20260829/b.xlsx", 100, true), -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_invalid_input_rejected() {
|
||||
// null 文件 key 拒绝
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.generateRows(null, 100, true));
|
||||
// 空白文件 key 拒绝
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.generateRows(" ", 100, true));
|
||||
// 负行数拒绝
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.generateRows("uploads/20260829/neg.xlsx", -1, true));
|
||||
// null 行集合分块拒绝
|
||||
assertThrows(IllegalArgumentException.class, () -> fixture.splitChunks(null, 200));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_dependency_failure_releases_resources() {
|
||||
// 采样时序列化器失败(mock 抛异常):错误可恢复,不产生部分结果
|
||||
ObjectMapper broken = new ObjectMapper() {
|
||||
@Override
|
||||
public String writeValueAsString(Object value) {
|
||||
throw new IllegalStateException("serializer down");
|
||||
}
|
||||
};
|
||||
SimilarAsinPerfFixture failingFixture = new SimilarAsinPerfFixture(broken);
|
||||
List<SimilarAsinParsedRowVo> rows = failingFixture.generateRows("uploads/20260829/fail.xlsx", 1000, true);
|
||||
assertThrows(IllegalStateException.class, () -> failingFixture.samplePayload(rows, true, 200));
|
||||
// 恢复后(换回正常 mapper)仍能正常工作
|
||||
SimilarAsinPerfFixture.Metrics recovered = fixture.samplePayload(fixture.generateRows("uploads/20260829/recover.xlsx", 1000, true), true, 200);
|
||||
assertTrue(recovered.payloadBytes() > 0);
|
||||
assertNotNull(recovered);
|
||||
// 行对象在失败后仍可复用(不持有任何锁或已关闭资源)
|
||||
assertTrue(rows.get(0).getAsin().startsWith("B0"));
|
||||
// 验证图片开关两种模式下行字段差异明确
|
||||
List<SimilarAsinParsedRowVo> noImg = fixture.generateRows("uploads/20260829/nimg.xlsx", 10, false);
|
||||
assertTrue(noImg.get(0).getUrl().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_normal_fields_populated() {
|
||||
// 行字段完整性:asin/country/sku/title/values 均填充且稳定
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/fields.xlsx", 10, false);
|
||||
SimilarAsinParsedRowVo row = rows.get(3);
|
||||
assertTrue(row.getAsin().matches("B0[A-Z0-9]{8}"));
|
||||
assertFalse(row.getCountry().isBlank());
|
||||
assertFalse(row.getSku().isBlank());
|
||||
assertFalse(row.getTitle().isBlank());
|
||||
assertNotNull(row.getValues());
|
||||
assertFalse(row.getValues().isEmpty());
|
||||
assertTrue(row.getValues().containsKey("asin"));
|
||||
assertTrue(row.getValues().containsKey("国家"));
|
||||
// 行号与 sourceId 关联正确
|
||||
assertEquals("4", row.getSourceId());
|
||||
assertEquals(4, row.getRowIndex());
|
||||
// values 是独立副本,修改不影响后续生成
|
||||
row.getValues().put("价格", "999");
|
||||
List<SimilarAsinParsedRowVo> again = fixture.generateRows("uploads/20260829/fields.xlsx", 10, false);
|
||||
assertTrue(!again.get(3).getValues().getOrDefault("价格", "").equals("999"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_001_payload_chunk_image_boundary_chunk_size_edge() {
|
||||
// chunk 边界:行数恰好整除 / 有余数 / 单 chunk 放不下
|
||||
List<SimilarAsinParsedRowVo> rows = fixture.generateRows("uploads/20260829/edge.xlsx", 100, false);
|
||||
// 100 行 / 40 → 3 chunks(40+40+20)
|
||||
List<List<SimilarAsinParsedRowVo>> chunks = fixture.splitChunks(rows, 40);
|
||||
assertEquals(3, chunks.size());
|
||||
assertEquals(40, chunks.get(0).size());
|
||||
assertEquals(20, chunks.get(2).size());
|
||||
// chunkSize 大于总行数 → 单 chunk
|
||||
assertEquals(1, fixture.splitChunks(rows, 500).size());
|
||||
// chunkSize 恰好等于行数 → 单 chunk 全量
|
||||
assertEquals(1, fixture.splitChunks(rows, 100).size());
|
||||
assertEquals(100, fixture.splitChunks(rows, 100).get(0).size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user