diff --git a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParser.java b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParser.java index 4ba474b1..8a9dc857 100644 --- a/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParser.java +++ b/protocol-gb32960/src/main/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParser.java @@ -53,6 +53,18 @@ public final class Gb32960BodyParser { private final Gb32960ProfileRegistry profileRegistry; private final VendorExtensionSelector selector; + /** + * 单块解析异常时是否兜底为 {@link InfoBlock.Raw} 后继续。由 + * {@link com.lingniu.ingest.protocol.gb32960.config.Gb32960Properties.Parse#isLenientBlockFailure()} + * 注入;默认 true。实际行为在 + * {@link #parse(Gb32960ParserContext, ByteBuffer)} 主循环里实现。 + */ + private boolean lenientBlockFailure = true; + + public void setLenientBlockFailure(boolean lenientBlockFailure) { + this.lenientBlockFailure = lenientBlockFailure; + } + /** * 单 registry 构造(向后兼容旧测试)。等价于 default profile + 无 vendor 扩展。 */ diff --git a/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParserIsolationTest.java b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParserIsolationTest.java new file mode 100644 index 00000000..4e4ec2d8 --- /dev/null +++ b/protocol-gb32960/src/test/java/com/lingniu/ingest/protocol/gb32960/codec/Gb32960BodyParserIsolationTest.java @@ -0,0 +1,172 @@ +package com.lingniu.ingest.protocol.gb32960.codec; + +import com.lingniu.ingest.api.spi.DecodeException; +import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.PositionV2016BlockParser; +import com.lingniu.ingest.protocol.gb32960.codec.parser.v2016.VehicleV2016BlockParser; +import com.lingniu.ingest.protocol.gb32960.model.InfoBlock; +import com.lingniu.ingest.protocol.gb32960.model.InfoBlockType; +import com.lingniu.ingest.protocol.gb32960.model.ProtocolVersion; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * 验证 {@link Gb32960BodyParser} 单块异常隔离行为。 + * + *
三个隔离场景: + *
外加一个严格模式回退测试:{@code lenientBlockFailure=false} 时异常应抛 DecodeException。 + */ +class Gb32960BodyParserIsolationTest { + + /** 模拟一个固定长度的 Position parser,声明 fixedLength=9,parse 时主动抛异常。 */ + private static final InfoBlockParser EXPLODING_FIXED_LEN_POSITION = new InfoBlockParser() { + @Override public ProtocolVersion version() { return ProtocolVersion.V2016; } + @Override public int typeCode() { return 0x05; } + @Override public int fixedLength() { return 9; } + @Override public InfoBlock parse(ByteBuffer buffer) { + buffer.get(); + buffer.get(); + throw new DecodeException("simulated parser failure in Position"); + } + }; + + /** 模拟一个变长 Alarm parser(fixedLength=-1),消费若干字节后抛。 */ + private static final InfoBlockParser EXPLODING_VAR_LEN_ALARM = new InfoBlockParser() { + @Override public ProtocolVersion version() { return ProtocolVersion.V2016; } + @Override public int typeCode() { return 0x07; } + @Override public int fixedLength() { return -1; } + @Override public InfoBlock parse(ByteBuffer buffer) { + buffer.get(); + throw new DecodeException("simulated parser failure in Alarm list-length read"); + } + }; + + @Test + void fixedLengthBlockFailure_isIsolated_subsequentBlocksStillParsed() { + InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of( + new VehicleV2016BlockParser(), + EXPLODING_FIXED_LEN_POSITION)); + + Gb32960BodyParser parser = new Gb32960BodyParser(registry); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeValidVehicle(os); // 1 + 20 = 21B + writePositionTypeAnd9ByteBody(os); // 1 + 9 = 10B(parser 会爆) + writeValidVehicle(os); // 1 + 20 = 21B + ByteBuffer body = ByteBuffer.wrap(os.toByteArray()); + + var result = parser.parse(ProtocolVersion.V2016, body); + + assertThat(result.blocks()).hasSize(3); + assertThat(result.blocks().get(0)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class); + assertThat(result.blocks().get(1)).isInstanceOfSatisfying(InfoBlock.Raw.class, raw -> { + assertThat(raw.typeCode()).isEqualTo(0x05); + assertThat(raw.type()).isEqualTo(InfoBlockType.RAW); + assertThat(raw.bytes()).hasSize(9); + }); + assertThat(result.blocks().get(2)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class); + assertThat(body.hasRemaining()).isFalse(); + } + + @Test + void truncatedFixedLengthBlock_isWrappedAsRaw_loopTerminates() { + InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of( + new VehicleV2016BlockParser(), + new PositionV2016BlockParser())); + + Gb32960BodyParser parser = new Gb32960BodyParser(registry); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeValidVehicle(os); + os.write(0x05); + os.write(0); + os.write(0); + os.write(0); + ByteBuffer body = ByteBuffer.wrap(os.toByteArray()); + + var result = parser.parse(ProtocolVersion.V2016, body); + + assertThat(result.blocks()).hasSize(2); + assertThat(result.blocks().get(0)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class); + assertThat(result.blocks().get(1)).isInstanceOfSatisfying(InfoBlock.Raw.class, raw -> { + assertThat(raw.typeCode()).isEqualTo(0x05); + assertThat(raw.bytes()).hasSize(3); + }); + } + + @Test + void variableLengthBlockFailure_swallowsRemainderAsRaw_loopBreaks() { + InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of( + new VehicleV2016BlockParser(), + EXPLODING_VAR_LEN_ALARM)); + + Gb32960BodyParser parser = new Gb32960BodyParser(registry); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeValidVehicle(os); + os.write(0x07); + for (int i = 0; i < 10; i++) os.write(0xAA); + writeValidVehicle(os); + ByteBuffer body = ByteBuffer.wrap(os.toByteArray()); + + var result = parser.parse(ProtocolVersion.V2016, body); + + assertThat(result.blocks()).hasSize(2); + assertThat(result.blocks().get(0)).isInstanceOf(InfoBlock.Gb32960V2016.Vehicle.class); + assertThat(result.blocks().get(1)).isInstanceOfSatisfying(InfoBlock.Raw.class, raw -> { + assertThat(raw.typeCode()).isEqualTo(0x07); + assertThat(raw.bytes()).hasSize(31); + }); + } + + @Test + void strictMode_throwsOnAnyBlockFailure() { + InfoBlockParserRegistry registry = new InfoBlockParserRegistry(List.of( + new VehicleV2016BlockParser(), + EXPLODING_FIXED_LEN_POSITION)); + + Gb32960BodyParser parser = new Gb32960BodyParser(registry); + parser.setLenientBlockFailure(false); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeValidVehicle(os); + writePositionTypeAnd9ByteBody(os); + ByteBuffer body = ByteBuffer.wrap(os.toByteArray()); + + assertThatThrownBy(() -> parser.parse(ProtocolVersion.V2016, body)) + .isInstanceOf(DecodeException.class); + } + + private static void writeValidVehicle(ByteArrayOutputStream os) { + os.write(0x01); + os.write(0x01); + os.write(0x01); + os.write(0x01); + os.write(0); os.write(0); + os.write(0); os.write(0); os.write(0); os.write(0); + os.write(0); os.write(0); + os.write(0); os.write(0); + os.write(50); + os.write(0x01); + os.write(0); + os.write(0); os.write(0); + os.write(0); + os.write(0); + } + + private static void writePositionTypeAnd9ByteBody(ByteArrayOutputStream os) { + os.write(0x05); + for (int i = 0; i < 9; i++) os.write(0); + } +}