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)
+		})
+	}
+}