blob: 4149f181728aaf3559df9b1692d6a76fd4ff2d98 [file] [log] [blame]
/*
* 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 (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"os"
"runtime/debug"
"strconv"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/subchen/go-xmldom"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/utils"
)
type ParserSerializerTestsuite struct {
name string
protocolName string
outputFlavor string
driverParameters map[string]string
byteOrder binary.ByteOrder
parser Parser
rootTypeParser func(utils.ReadBufferByteBased) (any, error)
testcases []ParserSerializerTestcase
}
type ParserSerializerTestcase struct {
name string
rawInputText string
rootType string
referenceXml string
parserArguments []string
}
func (p *ParserSerializerTestsuite) Run(t *testing.T, testcase ParserSerializerTestcase) error {
t.Logf("running testsuite: %s test: %s", p.name, testcase.name)
// Get the raw input by decoding the hex-encoded binary input
t.Log("decoding input")
rawInput, err := hex.DecodeString(testcase.rawInputText)
if err != nil {
return errors.Wrap(err, "Error decoding test input")
}
// Create the right read buffer
t.Log("creating read buffer")
var readBuffer utils.ReadBuffer
if p.byteOrder == binary.LittleEndian {
readBuffer = utils.NewReadBufferByteBased(rawInput, utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))
} else {
readBuffer = utils.NewReadBufferByteBased(rawInput)
}
// Parse the input according to the settings of the testcase
t.Log("parsing input")
msg, err := p.parser.Parse(testcase.rootType, testcase.parserArguments, readBuffer)
if err != nil {
return errors.Wrap(err, "Error parsing input data")
}
t.Logf("Parsed Message(%T)", msg)
t.Logf("\n%s", msg)
t.Log("Try serializing")
{
// First try to use the native xml writer
serializable := msg.(utils.Serializable)
buffer := utils.NewXmlWriteBuffer()
if err := serializable.SerializeWithWriteBuffer(context.Background(), buffer); err == nil {
actualXml := buffer.GetXmlString()
if err := CompareResults(t, []byte(actualXml), []byte(testcase.referenceXml)); err != nil {
border := strings.Repeat("=", 100)
fmt.Printf(
"\n"+
// Border
"%[1]s\n"+
// Testcase name
"%[4]s\n"+
// diff detected message
"Diff detected\n"+
// Border
"%[1]s\n"+
// xml
"%[2]s\n"+
// Border
"%[1]s\n%[1]s\n"+
// Text
"Differences were found after parsing (Use the above xml in the testsuite to disable this warning).\n"+
// Diff
"%[3]s\n"+
// Double Border
"%[1]s\n%[1]s\n",
border,
actualXml,
err,
testcase.name)
assert.Equal(t, testcase.referenceXml, actualXml)
return errors.Wrap(err, "Error comparing the results")
}
}
}
// If all was ok, serialize the object again
s, ok := msg.(utils.Serializable)
if !ok {
return errors.New("Couldn't cast message to Serializable")
}
t.Log("Serializing went ok")
// Create the write buffer
t.Log("creating write buffer")
var writeBuffer utils.WriteBufferByteBased
if p.byteOrder == binary.LittleEndian {
writeBuffer = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
} else {
writeBuffer = utils.NewWriteBufferByteBased()
}
// Serialize the message
t.Log("Serialize message")
err = s.SerializeWithWriteBuffer(context.Background(), writeBuffer)
if !ok {
return errors.New("Couldn't serialize message back to byte array")
}
// Check if the output matches in size and content
t.Log("comparing output")
rawOutput := writeBuffer.GetBytes()
if len(rawInput) != len(rawOutput) {
t.Errorf("Missmatched number of bytes expected ->%d != %d<-actual\nexpected:\t%x\nactual:\t\t%x", len(rawInput), len(rawOutput), rawInput, rawOutput)
t.Errorf("Hexdumps:\n%s", utils.DiffHex(rawInput, rawOutput))
return errors.New("length doesn't match")
}
for i, val := range rawInput {
if rawOutput[i] != val {
t.Error("Raw output doesn't match input at position: " + strconv.Itoa(i))
t.Errorf("Hexdumps:\n%s", utils.DiffHex(rawInput, rawOutput))
return errors.New("index mismatch")
}
}
return nil
}
type Parser interface {
Parse(typeName string, arguments []string, io utils.ReadBuffer) (any, error)
}
func RunParserSerializerTestsuite(t *testing.T, testPath string, parser Parser, options ...options.WithOption) {
t.Log("Extract testsuite options")
var rootTypeParser func(utils.ReadBufferByteBased) (any, error)
skippedTestCasesMap := map[string]bool{}
for _, withOption := range options {
switch option := withOption.(type) {
case withRootTypeParser:
t.Logf("Using root type parser for better output")
rootTypeParser = option.rootTypeParser
case withSkippedTestCases:
t.Log("Skipping test cases:")
for _, skippedTestCase := range option.skippedTestCases {
t.Logf("Skipping %s", skippedTestCase)
skippedTestCasesMap[skippedTestCase] = true
}
}
}
// Read the test-specification as XML file
rootNode := ParseParserSerializerTestSuiteXml(t, testPath)
// Parse the contents of the test-specification
testsuite := ParseParserSerializerTestSuite(t, *rootNode, parser, rootTypeParser)
t.Logf("Running %d testcases", len(testsuite.testcases))
for _, testcase := range testsuite.testcases {
t.Run(testcase.name, func(t *testing.T) {
defer func() {
if err := recover(); err != nil {
t.Fatalf("\n-------------------------------------------------------\nPanic Failure\n%+v\n%s\n-------------------------------------------------------\n\n", err, debug.Stack())
}
}()
if skippedTestCasesMap[testcase.name] {
t.Logf("Testcase %s skipped", testcase.name)
t.Skipf("Testcase %s skipped", testcase.name)
return
}
t.Logf("Running testcase %s", testcase.name)
if err := testsuite.Run(t, testcase); err != nil {
t.Fatalf("\n-------------------------------------------------------\nFailure\n%+v\n-------------------------------------------------------\n\n", err)
}
})
}
t.Log("Done running testcases")
// Execute the tests in the testsuite
t.Log(testsuite.name)
}
func ParseParserSerializerTestSuiteXml(t *testing.T, testPath string) *xmldom.Node {
// Get the current working directory
path, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// Check if the test-file is available
dirOffset := "/../../../"
testFile := path + dirOffset + testPath
info, err := os.Stat(testFile)
if os.IsNotExist(err) {
t.Logf("Test-File %s doesn't exist", testFile)
t.Fatal(err)
}
if info.IsDir() {
t.Logf("Test-File %s refers to a directory", testFile)
t.Fatal(err)
}
// Open a reader for this file
dat, err := os.Open(testFile)
if err != nil {
t.Error("Error opening file")
}
// Read the xml
node := xmldom.Must(xmldom.Parse(dat)).Root
return node
}
func ParseParserSerializerTestSuite(t *testing.T, node xmldom.Node, parser Parser, rootTypeParser func(utils.ReadBufferByteBased) (any, error)) *ParserSerializerTestsuite {
if node.Name != "testsuite" {
t.Fatal("invalid document structure")
}
var byteOrder binary.ByteOrder
if node.GetAttributeValue("byteOrder") != "LITTLE_ENDIAN" {
byteOrder = binary.BigEndian
} else {
byteOrder = binary.LittleEndian
}
var testsuiteName string
var protocolName string
var outputFlavor string
driverParameters := make(map[string]string)
var testcases []ParserSerializerTestcase
for _, childPtr := range node.Children {
child := *childPtr
switch child.Name {
case "name":
testsuiteName = child.Text
case "protocolName":
protocolName = child.Text
case "outputFlavor":
outputFlavor = child.Text
case "driver-parameters":
parameterList := child.FindByName("parameter")
for _, parameter := range parameterList {
nameElement := parameter.FindOneByName("name")
valueElement := parameter.FindOneByName("value")
if nameElement == nil || valueElement == nil {
t.Fatal("invalid parameter found: no present")
}
name := nameElement.Text
value := valueElement.Text
if name == "" || value == "" {
t.Fatal("invalid parameter found: empty")
}
driverParameters[name] = value
}
case "testcase":
testcaseName := child.FindOneByName("name").Text
rawInputText := (*(child.FindOneByName("raw"))).Text
rootType := (*(child.FindOneByName("root-type"))).Text
parserArgumentsXml := child.FindOneByName("parser-arguments")
var parserArguments []string
if parserArgumentsXml != nil {
for _, parserArgumentXml := range parserArgumentsXml.Children {
parserArguments = append(parserArguments, parserArgumentXml.Text)
}
}
referenceXml := child.FindOneByName("xml")
normalizeXml(referenceXml)
referenceSerialized := referenceXml.FirstChild().XMLPretty()
testcase := ParserSerializerTestcase{
name: testcaseName,
rawInputText: rawInputText,
rootType: rootType,
referenceXml: referenceSerialized,
parserArguments: parserArguments,
}
testcases = append(testcases, testcase)
default:
t.Fatalf("invalid document structure. Unhandled element %s", child.Name)
}
}
t.Logf("Parsed testsuite name: %s", testsuiteName)
return &ParserSerializerTestsuite{
name: testsuiteName,
protocolName: protocolName,
outputFlavor: outputFlavor,
driverParameters: driverParameters,
byteOrder: byteOrder,
parser: parser,
rootTypeParser: rootTypeParser,
testcases: testcases,
}
}
// Mainly remove linebreaks from text content.
func normalizeXml(input *xmldom.Node) {
if len(input.Children) > 0 {
for _, child := range input.Children {
normalizeXml(child)
}
}
if len(input.Text) > 0 {
if strings.Contains(input.Text, "\n") {
input.Text = strings.Replace(input.Text, "\n", "", -1)
}
}
}