task-49: 结果明细从逐行 RustFS 对象改为 chunk 级 payload 存储,引用 JSON 兼容旧格式
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 49:将结果明细从逐行 RustFS 对象改为 chunk 级 payload 存储。
|
||||
* CollectDataResultDetailCodec 负责:把整 chunk 的 accepted 行序列化为
|
||||
* 一个明细数组 JSON(每 chunk 一个 RustFS 对象),并为每行生成
|
||||
* {chunk, offset, payload} 引用 JSON(写入 biz_task_result_item.payload_json);
|
||||
* 读侧按 chunk 引用一次解析数组、offset 取行。旧格式(payload_json 直接是
|
||||
* 行 JSON)解析返回 null,由调用方按逐行旧路径兼容读取。
|
||||
*/
|
||||
class CollectDataResultDetailCodecTest {
|
||||
|
||||
private CollectDataResultDetailCodec codec;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
codec = new CollectDataResultDetailCodec(new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_normal_default_path() {
|
||||
// 正常输入:encodeRef → parseRef 往返一致;chunk 明细数组序列化后
|
||||
// rowAt 按 offset 取回同一行。
|
||||
String ref = codec.encodeRef(2, 3, "rustfs:detail/abc");
|
||||
|
||||
CollectDataResultDetailCodec.ChunkRef parsed = codec.parseRef(ref);
|
||||
|
||||
assertEquals(2, parsed.chunkIndex(), "chunk 序号保留");
|
||||
assertEquals(3, parsed.offset(), "行 offset 保留");
|
||||
assertEquals("rustfs:detail/abc", parsed.pointer(), "RustFS 对象指针保留");
|
||||
|
||||
String detailJson = codec.encodeChunk(List.of(row("B000000001", "Nike"), row("B000000002", "Zara")));
|
||||
CollectDataResultRowVo at1 = codec.rowAt(detailJson, 1);
|
||||
assertEquals("B000000002", at1.getAsin(), "按 offset 取回第二行");
|
||||
assertEquals("Zara", at1.getBrand(), "行字段完整保留(不做规范化)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_normal_multiple_items() {
|
||||
// 批量场景:多 chunk 多行,各自引用指向自己的 chunk 与 offset,互不串行。
|
||||
String refChunk0 = codec.encodeRef(0, 1, "rustfs:detail/a");
|
||||
String refChunk2 = codec.encodeRef(2, 0, "rustfs:detail/b");
|
||||
List<CollectDataResultRowVo> rows0 = List.of(row("B000000001", "A"), row("B000000002", "B"));
|
||||
List<CollectDataResultRowVo> rows2 = List.of(row("B000000101", "C"));
|
||||
|
||||
CollectDataResultDetailCodec.ChunkRef parsed0 = codec.parseRef(refChunk0);
|
||||
CollectDataResultDetailCodec.ChunkRef parsed2 = codec.parseRef(refChunk2);
|
||||
assertEquals(0, parsed0.chunkIndex(), "chunk0 序号");
|
||||
assertEquals(1, parsed0.offset(), "chunk0 offset");
|
||||
assertEquals(2, parsed2.chunkIndex(), "chunk2 序号");
|
||||
assertEquals(0, parsed2.offset(), "chunk2 offset");
|
||||
|
||||
assertEquals("B000000002", codec.rowAt(codec.encodeChunk(rows0), parsed0.offset()).getAsin(), "chunk0 行");
|
||||
assertEquals("B000000101", codec.rowAt(codec.encodeChunk(rows2), parsed2.offset()).getAsin(), "chunk2 行");
|
||||
assertNull(codec.rowAt(codec.encodeChunk(rows2), 5), "chunk2 中越界 offset 返回 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:同输入重复编码结果逐字节一致;重复解析引用结果一致;不产生新对象。
|
||||
String first = codec.encodeChunk(List.of(row("B000000001", "Nike"), row("B000000002", "Zara")));
|
||||
String second = codec.encodeChunk(List.of(row("B000000001", "Nike"), row("B000000002", "Zara")));
|
||||
assertEquals(first, second, "重复序列化结果一致");
|
||||
|
||||
String ref = codec.encodeRef(1, 0, "rustfs:detail/x");
|
||||
assertEquals(ref, codec.encodeRef(1, 0, "rustfs:detail/x"), "重复引用编码一致");
|
||||
assertEquals(codec.parseRef(ref), codec.parseRef(ref), "重复解析一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_boundary_empty_input() {
|
||||
// 空输入:空 chunk 明细编码为空数组;空/空白引用解析为 null(旧格式兼容)。
|
||||
assertEquals("[]", codec.encodeChunk(List.of()), "空 chunk 编码为空数组");
|
||||
assertEquals("[]", codec.encodeChunk(null), "null 明细编码为空数组");
|
||||
assertNull(codec.parseRef(null), "null 引用返回 null");
|
||||
assertNull(codec.parseRef(" "), "空白引用返回 null");
|
||||
assertNull(codec.rowAt("[]", 0), "空数组取行返回 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_boundary_single_item() {
|
||||
// 单元素:单行单 chunk offset 0 正确;单行明细取行正确。
|
||||
String ref = codec.encodeRef(0, 0, "rustfs:detail/solo");
|
||||
CollectDataResultDetailCodec.ChunkRef parsed = codec.parseRef(ref);
|
||||
assertEquals(0, parsed.chunkIndex(), "单 chunk 序号");
|
||||
assertEquals(0, parsed.offset(), "单行 offset 0");
|
||||
|
||||
String detailJson = codec.encodeChunk(List.of(row("B000000001", "solo")));
|
||||
CollectDataResultRowVo value = codec.rowAt(detailJson, 0);
|
||||
assertEquals("B000000001", value.getAsin(), "单行取回正确");
|
||||
assertEquals("solo", value.getBrand(), "单行字段完整");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_boundary_limit_and_overflow() {
|
||||
// 上限/超限:offset 越界或为负时 rowAt 安全返回 null,不抛异常、不越界。
|
||||
String detailJson = codec.encodeChunk(List.of(row("B000000001", "Nike")));
|
||||
assertNull(codec.rowAt(detailJson, 1), "offset 越界返回 null");
|
||||
assertNull(codec.rowAt(detailJson, 100), "offset 远超行数返回 null");
|
||||
assertNull(codec.rowAt(detailJson, -1), "负 offset 返回 null");
|
||||
assertNull(codec.rowAt(null, 0), "null 明细返回 null");
|
||||
assertNull(codec.rowAt("not-json", 0), "损坏明细返回 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_invalid_input_rejected() {
|
||||
// 非法参数:非法 chunk/offset 编码引用时抛可识别异常;
|
||||
// 非引用格式(旧逐行 JSON、数组、裸文本)解析返回 null 而非抛错。
|
||||
assertThrows(IllegalArgumentException.class, () -> codec.encodeRef(-1, 0, "rustfs:x"), "负 chunk 拒绝");
|
||||
assertThrows(IllegalArgumentException.class, () -> codec.encodeRef(0, -1, "rustfs:x"), "负 offset 拒绝");
|
||||
assertThrows(IllegalArgumentException.class, () -> codec.encodeRef(0, 0, null), "null 指针拒绝");
|
||||
|
||||
assertNull(codec.parseRef("{\"brand\":\"nike\"}"), "旧格式行 JSON 返回 null");
|
||||
assertNull(codec.parseRef("[1,2,3]"), "数组 JSON 返回 null");
|
||||
assertNull(codec.parseRef("plain-text"), "裸文本返回 null");
|
||||
assertNull(codec.parseRef("{\"chunk\":1,\"offset\":2}"), "缺 payload 字段返回 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_049_payload_chunk_rustfs_dependency_failure_releases_resources() {
|
||||
// 依赖失败:引用 JSON 损坏(readTree 抛错)时解析返回 null,
|
||||
// 调用方可按旧格式逐行读取兜底,错误可恢复不泄漏。
|
||||
String malformed = "{\"chunk\":1,\"offset\":";
|
||||
assertNull(codec.parseRef(malformed), "损坏引用解析为 null 不抛异常");
|
||||
|
||||
String detailJson = codec.encodeChunk(new ArrayList<>());
|
||||
CollectDataResultRowVo value = codec.rowAt(detailJson + "{broken", 0);
|
||||
assertNull(value, "损坏明细取行返回 null 不抛异常");
|
||||
assertTrue(codec.parseRef(codec.encodeRef(0, 0, "rustfs:x")) != null, "修复后可正常解析");
|
||||
}
|
||||
|
||||
private static CollectDataResultRowVo row(String asin, String brand) {
|
||||
CollectDataResultRowVo row = new CollectDataResultRowVo();
|
||||
row.setAsin(asin);
|
||||
row.setBrand(brand);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user