You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/08/21 10:14:38 UTC
(plc4x) 02/04: test(plc4go): parse every truncation of every testsuite message
This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit 0bac2b525edb4b69b331e33ceb206d84d556c06b
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Fri Aug 21 11:45:14 2026 +0200
test(plc4go): parse every truncation of every testsuite message
The remote end decides where a message ends, so every prefix of a well formed
message is something a driver can be handed. Generalise the bacnetip specific
truncation test into a testutils runner and point it at all 14 parser-serializer
corpora, so a protocol earns the coverage by having reference messages.
Replaces BVLCTruncation_test.go, whose corpus this subsumes.
---
.../readwrite/model/BVLCTruncation_test.go | 123 ---------------------
plc4go/spi/testutils/ParserTruncationTestRunner.go | 97 ++++++++++++++++
.../tests/drivers/tests/parser_truncation_test.go | 73 ++++++++++++
3 files changed, 170 insertions(+), 123 deletions(-)
diff --git a/plc4go/protocols/bacnetip/readwrite/model/BVLCTruncation_test.go b/plc4go/protocols/bacnetip/readwrite/model/BVLCTruncation_test.go
deleted file mode 100644
index 7c088fbd5f..0000000000
--- a/plc4go/protocols/bacnetip/readwrite/model/BVLCTruncation_test.go
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package model
-
-import (
- "context"
- "encoding/hex"
- "encoding/xml"
- "fmt"
- "os"
- "strings"
- "testing"
-
- "github.com/apache/plc4x/plc4go/spi/utils"
-)
-
-const parserSerializerTestsuite = "../../../../assets/testing/protocols/bacnet/ParserSerializerTestsuite.xml"
-
-// A datagram arrives over UDP, so the peer decides where it ends. Every prefix of a well formed
-// message is therefore something a driver can be handed, and none of them may take a parser down:
-// an optional field whose read runs out of bytes is reported as absent, and anything deriving a
-// value from it has to cope with that. Truncating the corpus of real messages at every length
-// exercises exactly that, across every type the corpus reaches.
-func TestBVLCTruncatedAtEveryLengthDoesNotPanic(t *testing.T) {
- messages := loadTestsuiteMessages(t)
- if len(messages) == 0 {
- t.Fatal("no messages loaded from the parser-serializer testsuite")
- }
- t.Logf("truncating %d messages from the parser-serializer testsuite", len(messages))
-
- parsed := 0
- for _, message := range messages {
- // The corpus is only useful as a truncation base if it parses whole.
- if _, err := parseBVLC(message.data); err != nil {
- t.Errorf("%s: the untruncated message did not parse: %v", message.name, err)
- continue
- }
- parsed++
-
- for length := 0; length < len(message.data); length++ {
- truncated := message.data[:length]
- if r := parseBVLCRecovering(truncated); r != nil {
- t.Errorf("%s truncated to %d of %d bytes (%s) panicked: %v",
- message.name, length, len(message.data), hex.EncodeToString(truncated), r)
- }
- }
- }
- t.Logf("%d messages parsed whole and were truncated at every length", parsed)
-}
-
-func parseBVLC(data []byte) (BVLC, error) {
- return BVLCParseWithBuffer[BVLC](context.Background(), utils.NewReadBufferByteBased(data))
-}
-
-// parseBVLCRecovering returns the recovered panic value, or nil if parsing did not panic. An error
-// is a permitted outcome - the point is only that the failure stays a value we can handle.
-func parseBVLCRecovering(data []byte) (recovered any) {
- defer func() { recovered = recover() }()
- _, _ = parseBVLC(data)
- return nil
-}
-
-type testsuiteMessage struct {
- name string
- data []byte
-}
-
-// loadTestsuiteMessages reads the raw hex of every BVLC rooted case in the parser-serializer
-// testsuite. Going through encoding/xml rather than matching text keeps the commented out cases -
-// which hold raw bytes the suite deliberately does not use - out of the corpus.
-func loadTestsuiteMessages(t *testing.T) []testsuiteMessage {
- t.Helper()
-
- content, err := os.ReadFile(parserSerializerTestsuite)
- if err != nil {
- t.Fatalf("could not read the parser-serializer testsuite: %v", err)
- }
-
- var suite struct {
- Testcases []struct {
- Name string `xml:"name"`
- Raw string `xml:"raw"`
- RootType string `xml:"root-type"`
- } `xml:"testcase"`
- }
- if err := xml.Unmarshal(content, &suite); err != nil {
- t.Fatalf("could not parse the parser-serializer testsuite: %v", err)
- }
-
- var messages []testsuiteMessage
- for i, testcase := range suite.Testcases {
- if strings.TrimSpace(testcase.RootType) != "BVLC" {
- continue
- }
- data, err := hex.DecodeString(strings.TrimSpace(testcase.Raw))
- if err != nil {
- t.Errorf("testcase %d (%s) has raw bytes that are not hex: %v", i, testcase.Name, err)
- continue
- }
- messages = append(messages, testsuiteMessage{
- name: fmt.Sprintf("case %d %q", i, strings.TrimSpace(testcase.Name)),
- data: data,
- })
- }
- return messages
-}
diff --git a/plc4go/spi/testutils/ParserTruncationTestRunner.go b/plc4go/spi/testutils/ParserTruncationTestRunner.go
new file mode 100644
index 0000000000..0da5e9754d
--- /dev/null
+++ b/plc4go/spi/testutils/ParserTruncationTestRunner.go
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package testutils
+
+import (
+ "encoding/binary"
+ "encoding/hex"
+ "testing"
+
+ "github.com/apache/plc4x/plc4go/spi/options"
+ "github.com/apache/plc4x/plc4go/spi/utils"
+)
+
+// RunParserTruncationTestsuite parses every prefix of every message in a parser-serializer
+// testsuite and requires each one to fail, if it fails, as an error rather than as a panic.
+//
+// The remote end decides where a message ends: a datagram can arrive short, and a stream can be
+// closed mid-message, so every prefix of a well formed message is something a driver can be handed.
+// Truncation is worth exercising in particular because a field read that runs out of bytes is
+// reported as an absent optional rather than as a failure, so anything deriving a value from that
+// field sees it unset in a case the surrounding condition says cannot happen.
+//
+// The reference messages of a testsuite make a far better corpus for this than hand-written input,
+// because they reach every type the protocol's own tests reach. Whether a prefix parses is left
+// open - a truncated message may legitimately parse into a smaller one. Only the manner of failure
+// is asserted.
+func RunParserTruncationTestsuite(t *testing.T, testPath string, parser Parser, options ...options.WithOption) {
+ skippedTestCases := map[string]bool{}
+ for _, withOption := range options {
+ if option, ok := withOption.(withSkippedTestCases); ok {
+ for _, skippedTestCase := range option.skippedTestCases {
+ t.Logf("Skipping %s", skippedTestCase)
+ skippedTestCases[skippedTestCase] = true
+ }
+ }
+ }
+
+ rootNode := ParseParserSerializerTestSuiteXml(t, testPath)
+ testsuite := ParseParserSerializerTestSuite(t, *rootNode, parser, nil)
+
+ messages, prefixes := 0, 0
+ for _, testcase := range testsuite.testcases {
+ if skippedTestCases[testcase.name] {
+ continue
+ }
+ rawInput, err := hex.DecodeString(testcase.rawInputText)
+ if err != nil {
+ t.Errorf("testcase %s has raw input that is not hex: %v", testcase.name, err)
+ continue
+ }
+ messages++
+
+ t.Run(testcase.name, func(t *testing.T) {
+ // Up to and including the whole message, so the untruncated case is covered too.
+ for length := 0; length <= len(rawInput); length++ {
+ prefixes++
+ if recovered := testsuite.parseRecovering(testcase, rawInput[:length]); recovered != nil {
+ t.Errorf("truncated to %d of %d bytes (%s) panicked: %v",
+ length, len(rawInput), hex.EncodeToString(rawInput[:length]), recovered)
+ }
+ }
+ })
+ }
+ t.Logf("parsed every prefix of %d messages of testsuite %s (%d prefixes)", messages, testsuite.name, prefixes)
+}
+
+// parseRecovering returns the value recovered from a panic while parsing, or nil if parsing did not
+// panic. A returned error is a permitted outcome; only a panic is not.
+func (p *ParserSerializerTestsuite) parseRecovering(testcase ParserSerializerTestcase, rawInput []byte) (recovered any) {
+ defer func() { recovered = recover() }()
+
+ var readBuffer utils.ReadBuffer
+ if p.byteOrder == binary.LittleEndian {
+ readBuffer = utils.NewReadBufferByteBased(rawInput, utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))
+ } else {
+ readBuffer = utils.NewReadBufferByteBased(rawInput)
+ }
+ _, _ = p.parser.Parse(testcase.rootType, testcase.parserArguments, readBuffer)
+ return nil
+}
diff --git a/plc4go/tests/drivers/tests/parser_truncation_test.go b/plc4go/tests/drivers/tests/parser_truncation_test.go
new file mode 100644
index 0000000000..78b14f96d6
--- /dev/null
+++ b/plc4go/tests/drivers/tests/parser_truncation_test.go
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package tests
+
+import (
+ "testing"
+
+ abethIO "github.com/apache/plc4x/plc4go/protocols/abeth/readwrite"
+ adsIO "github.com/apache/plc4x/plc4go/protocols/ads/readwrite"
+ bacnetIO "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite"
+ df1IO "github.com/apache/plc4x/plc4go/protocols/df1/readwrite"
+ eipIO "github.com/apache/plc4x/plc4go/protocols/eip/readwrite"
+ firmataIO "github.com/apache/plc4x/plc4go/protocols/firmata/readwrite"
+ iecIO "github.com/apache/plc4x/plc4go/protocols/iec608705104/readwrite"
+ knxIO "github.com/apache/plc4x/plc4go/protocols/knxnetip/readwrite"
+ modbusIO "github.com/apache/plc4x/plc4go/protocols/modbus/readwrite"
+ s7IO "github.com/apache/plc4x/plc4go/protocols/s7/readwrite"
+ slmpIO "github.com/apache/plc4x/plc4go/protocols/slmp/readwrite"
+ "github.com/apache/plc4x/plc4go/spi/testutils"
+)
+
+// The remote end decides where a message ends, so every prefix of a well formed message is
+// something a driver can be handed. Running the reference messages of each protocol through
+// RunParserTruncationTestsuite requires all of those prefixes to fail as errors rather than as
+// panics - see that function for why truncation in particular is worth exercising.
+//
+// The corpora are the same ones the parser-serializer tests use, so a protocol earns this coverage
+// simply by having reference messages.
+func TestParserTruncation(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ parser testutils.Parser
+ }{
+ {"abeth", "assets/testing/protocols/abeth/ParserSerializerTestsuite.xml", abethIO.AbethParserHelper{}},
+ {"ads", "assets/testing/protocols/ads/ParserSerializerTestsuite.xml", adsIO.AdsParserHelper{}},
+ {"bacnet", "assets/testing/protocols/bacnet/ParserSerializerTestsuite.xml", bacnetIO.BacnetipParserHelper{}},
+ {"df1", "assets/testing/protocols/df1/ParserSerializerTestsuite.xml", df1IO.Df1ParserHelper{}},
+ {"eip big endian", "assets/testing/protocols/eip/ParserSerializerTestsuiteBigEndian.xml", eipIO.EipParserHelper{}},
+ {"eip little endian", "assets/testing/protocols/eip/ParserSerializerTestsuiteLittleEndian.xml", eipIO.EipParserHelper{}},
+ {"firmata", "assets/testing/protocols/firmata/ParserSerializerTestsuite.xml", firmataIO.FirmataParserHelper{}},
+ {"iec 60870-5-104", "assets/testing/protocols/iec608705104/IEC-60870-5-104.xml", iecIO.Iec608705104ParserHelper{}},
+ {"knxnetip", "assets/testing/protocols/knxnetip/ParserSerializerTestsuite.xml", knxIO.KnxnetipParserHelper{}},
+ {"modbus ascii", "assets/testing/protocols/modbus/ascii/ParserSerializerTestsuite.xml", modbusIO.ModbusParserHelper{}},
+ {"modbus rtu", "assets/testing/protocols/modbus/rtu/ParserSerializerTestsuite.xml", modbusIO.ModbusParserHelper{}},
+ {"modbus tcp", "assets/testing/protocols/modbus/tcp/ParserSerializerTestsuite.xml", modbusIO.ModbusParserHelper{}},
+ {"s7", "assets/testing/protocols/s7/ParserSerializerTestsuite.xml", s7IO.S7ParserHelper{}},
+ {"slmp", "assets/testing/protocols/slmp/ParserSerializerTestsuite.xml", slmpIO.SlmpParserHelper{}},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ testutils.RunParserTruncationTestsuite(t, test.path, test.parser)
+ })
+ }
+}