task-20: Similar ASIN 端到端压测、GC 分析与结果文件兼容回归
SimilarAsinPerfFixture 新增三个功能点: - endToEndBenchmark:生成→分 chunk→序列化,记录行数/chunk 数/payload 字节/耗时/吞吐/峰值堆; - gcStressAnalysis:多轮生成/序列化/释放循环,采样 GC 计数差与堆峰值; - compatRoundTrip:payload 序列化往返恢复全量行并校验字段稳定(结果文件兼容回归)。 全量测试 508 通过。
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
package com.nanri.aiimage.modules.similarasin.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
|
||||
/**
|
||||
* Task 20:Similar ASIN 端到端压测、JFR/GC 分析与结果文件兼容回归。
|
||||
* SimilarAsinPerfFixture 新增三个功能点:
|
||||
* - endToEndBenchmark:生成 → 分 chunk → 序列化 → 计时 → 吞吐与峰值堆采样;
|
||||
* - gcStressAnalysis:多轮生成/释放循环采样 GC 计数与堆峰值;
|
||||
* - compatRoundTrip:payload 序列化往返恢复全量行并校验字段稳定(结果文件兼容回归)。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SimilarAsinPerfFixtureE2ETest {
|
||||
|
||||
@Spy private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_normal_default_path() {
|
||||
// 正常输入:1000 行端到端基准返回完整指标,行数/chunk 数正确,吞吐与堆峰值有界。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
SimilarAsinPerfFixture.EndToEndMetrics metrics =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-default.xlsx", 1000, false, 200);
|
||||
|
||||
assertEquals(1000, metrics.rowCount(), "行数不丢失");
|
||||
assertEquals(5, metrics.chunkCount(), "1000 行 / 200 每 chunk = 5 个 chunk");
|
||||
assertTrue(metrics.payloadBytes() > 0, "payload 字节可采样");
|
||||
assertTrue(metrics.assembleMillis() >= 0);
|
||||
assertTrue(metrics.throughputRowsPerSec() > 0, "吞吐必须为正");
|
||||
assertTrue(metrics.peakHeapBytes() > 0, "峰值堆必须为正");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_normal_multiple_items() {
|
||||
// 批量场景:图片开/关两种模式 5000 行,结果不丢失、chunk 划分稳定。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
SimilarAsinPerfFixture.EndToEndMetrics withImages =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-img.xlsx", 5000, true, 200);
|
||||
SimilarAsinPerfFixture.EndToEndMetrics textOnly =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-text.xlsx", 5000, false, 200);
|
||||
|
||||
assertEquals(5000, withImages.rowCount());
|
||||
assertEquals(5000, textOnly.rowCount());
|
||||
assertEquals(25, withImages.chunkCount());
|
||||
assertEquals(25, textOnly.chunkCount());
|
||||
assertTrue(withImages.payloadBytes() > textOnly.payloadBytes(), "图片模式 payload 必须大于纯文本");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_normal_repeated_operation_is_idempotent() throws Exception {
|
||||
// 重复执行:同一输入两次基准的指标一致,不产生重复行。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
SimilarAsinPerfFixture.CompatResult first =
|
||||
fixture.compatRoundTrip("uploads/20260829/e2e-idem.xlsx", 1000, true);
|
||||
SimilarAsinPerfFixture.CompatResult second =
|
||||
fixture.compatRoundTrip("uploads/20260829/e2e-idem.xlsx", 1000, true);
|
||||
|
||||
assertEquals(1000, first.rowCount());
|
||||
assertEquals(1000, first.recoveredCount(), "往返恢复全量行");
|
||||
assertTrue(first.fieldStable(), "字段必须稳定");
|
||||
assertEquals(first.rowCount(), second.rowCount());
|
||||
assertEquals(first.recoveredCount(), second.recoveredCount());
|
||||
assertEquals(first.fieldStable(), second.fieldStable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_boundary_empty_input() {
|
||||
// 空输入:0 行基准返回零指标;0 行往返返回空结果,不创建资源。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
SimilarAsinPerfFixture.EndToEndMetrics metrics =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-empty.xlsx", 0, false, 200);
|
||||
assertEquals(0, metrics.rowCount());
|
||||
assertEquals(0, metrics.chunkCount());
|
||||
assertEquals(0, metrics.payloadBytes());
|
||||
|
||||
SimilarAsinPerfFixture.CompatResult compat =
|
||||
fixture.compatRoundTrip("uploads/20260829/e2e-empty.xlsx", 0, false);
|
||||
assertEquals(0, compat.rowCount());
|
||||
assertEquals(0, compat.recoveredCount());
|
||||
assertTrue(compat.fieldStable(), "空结果字段稳定");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_boundary_single_item() {
|
||||
// 单元素:1 行基准不依赖批量路径,往返字段一致。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
SimilarAsinPerfFixture.EndToEndMetrics metrics =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-single.xlsx", 1, true, 200);
|
||||
assertEquals(1, metrics.rowCount());
|
||||
assertEquals(1, metrics.chunkCount());
|
||||
|
||||
SimilarAsinPerfFixture.CompatResult compat =
|
||||
fixture.compatRoundTrip("uploads/20260829/e2e-single.xlsx", 1, true);
|
||||
assertEquals(1, compat.recoveredCount());
|
||||
assertTrue(compat.fieldStable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_boundary_limit_and_overflow() {
|
||||
// 上限/超限:超过 MAX_ROWS 拒绝;5000 行基准在预算内完成,不发生无界内存增长。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
IllegalArgumentException overflow = assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.endToEndBenchmark("uploads/20260829/e2e-over.xlsx",
|
||||
SimilarAsinPerfFixture.MAX_ROWS + 1, false, 200));
|
||||
assertTrue(overflow.getMessage().contains("rowCount"), "超限消息应可识别");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.compatRoundTrip("uploads/20260829/e2e-over.xlsx",
|
||||
SimilarAsinPerfFixture.MAX_ROWS + 1, false));
|
||||
|
||||
SimilarAsinPerfFixture.EndToEndMetrics metrics =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-max.xlsx", 5000, true, 200);
|
||||
assertTrue(metrics.assembleMillis() < 15000,
|
||||
"5000 行端到端基准须在预算内完成,实际=" + metrics.assembleMillis() + "ms");
|
||||
assertTrue(metrics.peakHeapBytes() < 1024L * 1024L * 1024L, "峰值堆不得超过 1GB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_invalid_input_rejected() {
|
||||
// 非法参数:null key/非法 chunkSize/非法 GC 行数 → 明确异常与可识别消息。
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
IllegalArgumentException nullKey = assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.endToEndBenchmark(null, 100, false, 200));
|
||||
assertTrue(nullKey.getMessage().contains("sourceFileKey"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.endToEndBenchmark("uploads/20260829/x.xlsx", 100, false, 0));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.gcStressAnalysis(null, 100, false));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> fixture.gcStressAnalysis("uploads/20260829/x.xlsx", -1, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_020_asin_dependency_failure_releases_resources() throws Exception {
|
||||
// 依赖失败:序列化失败抛 IllegalStateException 且不产生部分结果;恢复后重试成功;
|
||||
// GC 分析后堆峰值回落(临时对象释放)。
|
||||
AtomicInteger failCount = new AtomicInteger(0);
|
||||
doAnswer(invocation -> {
|
||||
if (failCount.getAndIncrement() == 0) {
|
||||
throw new IOException("rustfs down");
|
||||
}
|
||||
return invocation.callRealMethod();
|
||||
}).when(objectMapper).writeValueAsBytes(any());
|
||||
|
||||
SimilarAsinPerfFixture fixture = new SimilarAsinPerfFixture(objectMapper);
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> fixture.endToEndBenchmark("uploads/20260829/e2e-fail.xlsx", 100, false, 200));
|
||||
SimilarAsinPerfFixture.EndToEndMetrics recovered =
|
||||
fixture.endToEndBenchmark("uploads/20260829/e2e-fail.xlsx", 100, false, 200);
|
||||
assertEquals(100, recovered.rowCount(), "依赖恢复后重试成功");
|
||||
|
||||
SimilarAsinPerfFixture.GcStressSample gc =
|
||||
fixture.gcStressAnalysis("uploads/20260829/e2e-gc.xlsx", 1000, true);
|
||||
assertNotNull(gc);
|
||||
assertTrue(gc.rounds() >= 1, "GC 分析至少执行一轮");
|
||||
assertTrue(gc.gcCount() >= 0);
|
||||
assertTrue(gc.peakHeapBytes() > 0, "堆峰值必须可采样");
|
||||
assertTrue(gc.peakHeapBytes() < 1024L * 1024L * 1024L, "GC 分析峰值堆不得超过 1GB");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user