blob: d8b396c274041d18a5ad08b007f5061a768f0773 [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"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/subchen/go-xmldom"
"github.com/apache/plc4x/plc4go/pkg/api"
"github.com/apache/plc4x/plc4go/pkg/api/config"
apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
"github.com/apache/plc4x/plc4go/spi"
"github.com/apache/plc4x/plc4go/spi/options"
"github.com/apache/plc4x/plc4go/spi/options/converter"
"github.com/apache/plc4x/plc4go/spi/transports"
"github.com/apache/plc4x/plc4go/spi/transports/test"
"github.com/apache/plc4x/plc4go/spi/utils"
)
var DriverTestsuiteConnectTimeout = 30 * time.Second
type DriverTestsuite struct {
name string
protocolName string
outputFlavor string
driverName string
driverParameters map[string]string
byteOrder binary.ByteOrder
parser XmlParser
rootTypeParser func(utils.ReadBufferByteBased) (any, error)
setupSteps []DriverTestStep
teardownSteps []DriverTestStep
testcases []DriverTestcase
}
type XmlParser interface {
Parse(typeName string, xmlString string, parserArguments ...string) (any, error)
}
// WithRootTypeParser can be used to output the root type of protocol for better debugging
func WithRootTypeParser(rootTypeParser func(utils.ReadBufferByteBased) (any, error)) config.WithOption {
return withRootTypeParser{rootTypeParser: rootTypeParser}
}
type withRootTypeParser struct {
options.Option
rootTypeParser func(utils.ReadBufferByteBased) (any, error)
}
// WithSkippedTestCases can be used to skip test cases
func WithSkippedTestCases(skippedTestCases ...string) config.WithOption {
return withSkippedTestCases{skippedTestCases: skippedTestCases}
}
type withSkippedTestCases struct {
options.Option
skippedTestCases []string
}
type TestTransportInstance interface {
transports.TransportInstance
FillReadBuffer(data []byte)
GetNumDrainableBytes() uint32
DrainWriteBuffer(numBytes uint32) []byte
}
func (m DriverTestsuite) Run(t *testing.T, driverManager plc4go.PlcDriverManager, testcase DriverTestcase) error {
var driverParameters []string
for key, value := range m.driverParameters {
driverParameters = append(driverParameters, fmt.Sprintf("%s=%s", key, value))
}
optionsString := ""
if len(driverParameters) > 0 {
optionsString = "?" + strings.Join(driverParameters, "&")
}
// Get a connection
t.Log("getting a connection")
connectionChan := driverManager.GetConnection(m.driverName + ":test://hurz" + optionsString)
timer := time.NewTimer(DriverTestsuiteConnectTimeout)
var connectionResult plc4go.PlcConnectionConnectResult
select {
case connectionResult = <-connectionChan:
case <-timer.C:
t.Fatalf("timeout")
}
if connectionResult.GetErr() != nil {
return errors.Wrap(connectionResult.GetErr(), "error getting a connection")
}
connection := connectionResult.GetConnection()
t.Cleanup(func() {
timeout := time.NewTimer(30 * time.Second)
select {
case result := <-connection.Close():
assert.NoError(t, result.GetErr())
case <-timeout.C:
t.Error("timeout closing connection")
}
})
utils.NewAsciiBoxWriter()
m.LogDelimiterSection(t, "=", "Executing testcase: %s", testcase.name)
if len(m.setupSteps) > 0 {
// Run the setup steps
m.LogDelimiterSection(t, "-", "Performing setup for: %s", testcase.name)
for _, testStep := range m.setupSteps {
err := m.ExecuteStep(t, connection, &testcase, testStep)
if err != nil {
return errors.Wrap(err, "error in setup step "+testStep.name)
}
// We sleep a bit to not run too fast into the post setup steps and give connections a bit time to settle built up
time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
}
m.LogDelimiterSection(t, "-", "setup done for: %s", testcase.name)
} else {
t.Log("no setup steps")
}
if len(testcase.steps) > 0 {
// Run the actual scenario steps
m.LogDelimiterSection(t, "-", "Running testcases for: %s", testcase.name)
for _, testStep := range testcase.steps {
err := m.ExecuteStep(t, connection, &testcase, testStep)
if err != nil {
return errors.Wrap(err, "error in step "+testStep.name)
}
time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
}
m.LogDelimiterSection(t, "-", "testcases done for: %s", testcase.name)
} else {
t.Log("no testcase steps")
}
if len(m.teardownSteps) > 0 {
// Run the teardown steps
m.LogDelimiterSection(t, "-", "Performing teardown for: %s", testcase.name)
for _, testStep := range m.teardownSteps {
err := m.ExecuteStep(t, connection, &testcase, testStep)
if err != nil {
return errors.Wrap(err, "error in teardown step "+testStep.name)
}
time.Sleep(100 * time.Millisecond) // TODO: this is really bad as on CI sometimes those sleeps are not enough...
}
m.LogDelimiterSection(t, "-", "teardown done for: %s", testcase.name)
} else {
t.Log("no teardown steps")
}
m.LogDelimiterSection(t, "=", "done testcase: %s", testcase.name)
return nil
}
func (m DriverTestsuite) ExecuteStep(t *testing.T, connection plc4go.PlcConnection, testcase *DriverTestcase, step DriverTestStep) error {
mc, ok := connection.(spi.TransportInstanceExposer)
if !ok {
return errors.New("couldn't access connections transport instance")
}
testTransportInstance, ok := mc.GetTransportInstance().(TestTransportInstance)
if !ok {
return errors.New("transport must be of type TestTransport")
}
start := time.Now()
m.LogDelimiterSection(t, "┄", " - Executing step (%s): %s", step.stepType, step.name)
switch step.stepType {
case StepTypeApiRequest:
switch step.payload.Name {
case "TestReadRequest":
// Assemble a read-request according to the information in the test xml
t.Log("Assemble read request")
rrb := connection.ReadRequestBuilder()
for _, tagNode := range step.payload.GetChild("tags").GetChildren("tag") {
tagName := tagNode.GetChild("name").Text
tagAddress := tagNode.GetChild("address").Text
rrb.AddTagAddress(tagName, tagAddress)
}
readRequest, err := rrb.Build()
if err != nil {
return errors.Wrap(err, "Error creating read-request")
}
// Execute the read-request and store the response-channel in the testcase.
t.Logf("Execute read request (%T)\n%[1]s", readRequest)
if testcase.readRequestResultChannel != nil {
return errors.New("testcase read-request result channel already occupied")
}
testcase.readRequestResultChannel = readRequest.Execute()
t.Log("request executed")
case "TestWriteRequest":
t.Log("Assemble write request")
wrb := connection.WriteRequestBuilder()
for _, tagNode := range step.payload.GetChild("tags").GetChildren("tag") {
tagName := tagNode.GetChild("name").Text
tagAddress := tagNode.GetChild("address").Text
he, ok := connection.(spi.HandlerExposer)
if !ok {
return errors.New("connection is not a HandlerExposer")
}
tag, err := he.GetPlcTagHandler().ParseTag(tagAddress)
if err != nil {
return errors.Wrapf(err, "error parsing address: %s", tagAddress)
}
if len(tag.GetArrayInfo()) > 0 {
var tagValue []string
for _, valueChild := range tagNode.GetChildren("value") {
tagValue = append(tagValue, valueChild.Text)
}
wrb.AddTagAddress(tagName, tagAddress, tagValue)
} else {
tagValue := tagNode.GetChild("value").Text
wrb.AddTagAddress(tagName, tagAddress, tagValue)
}
}
writeRequest, err := wrb.Build()
if err != nil {
return errors.Wrap(err, "Error creating write-request")
}
t.Logf("Execute write request (%T)\n%[1]s", writeRequest)
if testcase.writeRequestResultChannel != nil {
return errors.New("testcase write-request result channel already occupied")
}
testcase.writeRequestResultChannel = writeRequest.Execute()
t.Log("request executed")
}
case StepTypeApiResponse:
switch step.payload.Name {
case "PlcReadResponse":
if testcase.readRequestResultChannel == nil {
return errors.New("no read response expected")
}
t.Log("Waiting for read request result")
readRequestResult := <-testcase.readRequestResultChannel
if readRequestResult.GetErr() != nil {
return errors.Wrap(readRequestResult.GetErr(), "error sending response")
}
// Serialize the response to XML
xmlWriteBuffer := utils.NewXmlWriteBuffer()
response := readRequestResult.GetResponse()
t.Logf("Got response (%T)\n%[1]s", response)
err := response.(utils.Serializable).SerializeWithWriteBuffer(context.Background(), xmlWriteBuffer)
if err != nil {
return errors.Wrap(err, "error serializing response")
}
actualResponse := xmlWriteBuffer.GetXmlString()
// Get the reference XML
referenceSerialized := step.payload.XMLPretty()
// Compare the results
err = CompareResults(t, []byte(actualResponse), []byte(referenceSerialized))
if err != nil {
return errors.Wrap(err, "Error comparing the results")
}
// Reset read channel
testcase.readRequestResultChannel = nil
case "PlcWriteResponse":
if testcase.writeRequestResultChannel == nil {
return errors.New("no write response expected")
}
t.Log("Waiting for write request result")
writeResponseResult := <-testcase.writeRequestResultChannel
if writeResponseResult.GetErr() != nil {
return errors.Wrap(writeResponseResult.GetErr(), "error sending response")
}
// Serialize the response to XML
xmlWriteBuffer := utils.NewXmlWriteBuffer()
response := writeResponseResult.GetResponse()
t.Logf("Got response (%T)\n%[1]s", response)
err := response.(utils.Serializable).SerializeWithWriteBuffer(context.Background(), xmlWriteBuffer)
if err != nil {
return errors.Wrap(err, "error serializing response")
}
actualResponse := xmlWriteBuffer.GetXmlString()
// Get the reference XML
referenceSerialized := step.payload.XMLPretty()
// Compare the results
err = CompareResults(t, []byte(actualResponse), []byte(referenceSerialized))
if err != nil {
return errors.Wrap(err, "Error comparing the results")
}
// Reset write channel
testcase.writeRequestResultChannel = nil
}
case StepTypeOutgoingPlcMessage:
typeName := step.payload.Name
payloadString := step.payload.XMLPretty()
// Parse the xml into a real model
t.Log("parsing xml")
expectedMessage, err := m.parseMessage(typeName, payloadString, step)
if err != nil {
return errors.Wrap(err, "Error parsing message")
}
t.Logf("Parsed message (%T) from xml\n%[1]s", expectedMessage)
// Serialize the model into bytes
t.Log("Serialize the model into bytes and compare output")
expectedSerializable, ok := expectedMessage.(utils.Serializable)
if !ok {
return errors.Errorf("error converting type %t into Serializable type", expectedMessage)
}
var expectedWriteBuffer utils.WriteBufferByteBased
if m.byteOrder == binary.BigEndian {
expectedWriteBuffer = utils.NewWriteBufferByteBased()
} else {
t.Log("using little endian")
expectedWriteBuffer = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
}
err = expectedSerializable.SerializeWithWriteBuffer(context.Background(), expectedWriteBuffer)
if err != nil {
return errors.Wrap(err, "error serializing expectedMessage")
}
expectedRawOutput := expectedWriteBuffer.GetBytes()
expectedRawOutputLength := uint32(len(expectedRawOutput))
startTransportPolling := time.Now()
// Read exactly this amount of bytes from the transport
t.Logf("Reading bytes from transport instance (expectedRawOutputLength %d)", expectedRawOutputLength)
for testTransportInstance.GetNumDrainableBytes() < expectedRawOutputLength {
if time.Since(startTransportPolling) > 2*time.Second {
drainableBytes := testTransportInstance.GetNumDrainableBytes()
actualRawOutput := testTransportInstance.DrainWriteBuffer(drainableBytes)
return errors.Errorf("Not enough data available: actual(%d)<expected(%d), \nactual: %#X\nexpected: %#X\nHexdumps:\n%s",
drainableBytes, expectedRawOutputLength, actualRawOutput, expectedRawOutput, utils.DiffHex(expectedRawOutput, actualRawOutput))
}
time.Sleep(2 * time.Millisecond)
}
actualRawOutput := testTransportInstance.DrainWriteBuffer(expectedRawOutputLength)
if testTransportInstance.GetNumDrainableBytes() != 0 {
t.Logf("leftover drainable bytes (%d)", testTransportInstance.GetNumDrainableBytes())
}
var bufferFactory func([]byte, ...utils.ReadBufferByteBasedOptions) utils.ReadBufferByteBased
if m.byteOrder == binary.BigEndian {
bufferFactory = utils.NewReadBufferByteBased
} else {
bufferFactory = func(bytes []byte, options ...utils.ReadBufferByteBasedOptions) utils.ReadBufferByteBased {
t.Log("using little endian")
return utils.NewReadBufferByteBased(bytes, utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))
}
}
// Compare the bytes read with the ones we expect
t.Log("Comparing outputs")
for i := range expectedRawOutput {
if expectedRawOutput[i] != actualRawOutput[i] {
if m.rootTypeParser != nil {
readBufferByteBased := bufferFactory(actualRawOutput)
actual, err := m.rootTypeParser(readBufferByteBased)
t.Logf("A readable render of expectation:\n%v\nvs actual paket\n%v\n%#v", expectedSerializable, actual, err)
}
return errors.Errorf("actual output doesn't match expected output:\nactual: %#X\nexpected: %#X\nHexdumps:\n%s", actualRawOutput, expectedRawOutput, utils.DiffHex(expectedRawOutput, actualRawOutput))
}
}
t.Log("outputs are matching")
// If there's a difference, parse the input and display it to simplify debugging
case StepTypeOutgoingPlcBytes:
// Read exactly this amount of bytes from the transport
t.Log("Reading bytes")
expectedRawInput, err := hex.DecodeString(step.payload.Text)
if err != nil {
return errors.Wrap(err, "error decoding hex-encoded byte data")
}
t.Logf("\n%s", hex.Dump(expectedRawInput))
rawInput := testTransportInstance.DrainWriteBuffer(uint32(len(expectedRawInput)))
// Compare the bytes read with the ones we expect
t.Log("Comparing bytes")
for i := range expectedRawInput {
if expectedRawInput[i] != rawInput[i] {
return errors.Errorf("actual output doesn't match expected output:\nactual: %#X\nexpected: %#X\nHexdumps:\n%s", rawInput, expectedRawInput, utils.DiffHex(expectedRawInput, rawInput))
}
}
// If there's a difference, parse the input and display it to simplify debugging
case StepTypeIncomingPlcMessage:
typeName := step.payload.Name
payloadString := step.payload.XMLPretty()
// Parse the xml into a real model
t.Log("Parsing model")
expectedMessage, err := m.parseMessage(typeName, payloadString, step)
if err != nil {
return errors.Wrap(err, "error parsing message")
}
t.Logf("Parsed message (%T)\n%[1]s", expectedMessage)
// Serialize the model into bytes
t.Log("Serializing bytes")
expectedSerializable, ok := expectedMessage.(utils.Serializable)
if !ok {
return errors.New("error converting type into Serializable type")
}
var wb utils.WriteBufferByteBased
if m.byteOrder == binary.BigEndian {
wb = utils.NewWriteBufferByteBased()
} else {
t.Log("using little endian")
wb = utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
}
err = expectedSerializable.SerializeWithWriteBuffer(context.Background(), wb)
if err != nil {
return errors.Wrap(err, "error serializing expectedMessage")
}
// Send these bytes to the transport
_bytes := wb.GetBytes()
t.Logf("Writing to transport\n%s", hex.Dump(_bytes))
testTransportInstance.FillReadBuffer(_bytes)
case StepTypeIncomingPlcBytes:
// Get the raw hex-data.
t.Log("Get hex data")
rawInput, err := hex.DecodeString(step.payload.Text)
if err != nil {
return errors.Wrap(err, "error decoding hex-encoded byte data: ")
}
// Send these bytes to the transport
t.Logf("Writing bytes to transport\n%[1]s", hex.Dump(rawInput))
testTransportInstance.FillReadBuffer(rawInput)
case StepTypeDelay:
// Get the number of milliseconds
t.Log("Getting millis")
delay, err := strconv.Atoi(step.payload.Text)
if err != nil {
return errors.Wrap(err, "invalid delay format")
}
// Sleep for that long
t.Logf("Sleeping (delay: %dms)", delay)
time.Sleep(time.Duration(delay) * time.Millisecond)
case StepTypeTerminate:
// Simply close the transport connection
t.Log("closing transport")
err := testTransportInstance.Close()
if err != nil {
return errors.Wrap(err, "error closing transport")
}
}
m.LogDelimiterSection(t, "┄", " - Finished step: %s after %s ", step.name, time.Since(start))
return nil
}
func (m DriverTestsuite) parseMessage(typeName string, payloadString string, step DriverTestStep) (any, error) {
if m.parser == nil {
return nil, errors.Errorf("Protocol name %s has no mapped parser", m.protocolName)
}
parse, err := m.parser.Parse(typeName, payloadString, step.parserArguments...)
if err != nil {
return nil, errors.Wrap(err, "error parsing xml")
}
return parse, err
}
type DriverTestcase struct {
name string
steps []DriverTestStep
readRequestResultChannel <-chan apiModel.PlcReadRequestResult
writeRequestResultChannel <-chan apiModel.PlcWriteRequestResult
}
type DriverTestStep struct {
name string
stepType StepType
parserArguments []string
payload xmldom.Node
}
type StepType uint8
//go:generate go tool stringer -type StepType
//go:generate go tool plc4xLicencer -type=StepType
const (
StepTypeOutgoingPlcMessage StepType = 0x01
StepTypeOutgoingPlcBytes StepType = 0x02
StepTypeIncomingPlcMessage StepType = 0x03
StepTypeIncomingPlcBytes StepType = 0x04
StepTypeApiRequest StepType = 0x05
StepTypeApiResponse StepType = 0x06
StepTypeDelay StepType = 0x07
StepTypeTerminate StepType = 0x08
)
func RunDriverTestsuite(t *testing.T, driver plc4go.PlcDriver, testPath string, parser XmlParser, _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
}
}
}
t.Log("Read the test-specification as XML file")
// Read the test-specification as XML file
rootNode := ParseDriverTestsuiteXml(t, testPath)
t.Log("Parse the contents of the test-specification")
// Parse the contents of the test-specification
testsuite := ParseDriverTestsuite(t, *rootNode, parser, rootTypeParser)
// We don't want to await completion of connection initialization
if connectionConnectAwaiter, ok := driver.(ConnectionConnectAwaiter); ok {
t.Log("We don't wait for setup and disconnect")
connectionConnectAwaiter.SetAwaitSetupComplete(false)
connectionConnectAwaiter.SetAwaitDisconnectComplete(false)
}
t.Log("Initialize the driver manager")
// Initialize the driver manager
driverManager := plc4go.NewPlcDriverManager(converter.WithOptionToExternal(_options...)...)
t.Cleanup(func() {
assert.NoError(t, driverManager.Close())
})
transport := test.NewTransport(_options...)
driverManager.(spi.TransportAware).RegisterTransport(transport)
driverManager.RegisterDriver(driver)
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 {
testsuite.LogDelimiterSection(t, "=", "Panic Failure\n%+v\n%s", err, debug.Stack())
t.FailNow()
}
}()
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, driverManager, testcase); err != nil {
testsuite.LogDelimiterSection(t, "=", "Failure:\n%s", err)
t.FailNow()
}
})
}
}
type ConnectionConnectAwaiter interface {
// SetAwaitSetupComplete sets a flag that the driver should await a connection completion
SetAwaitSetupComplete(awaitComplete bool)
// SetAwaitDisconnectComplete sets a flag that the driver should await a dis-connection completion
SetAwaitDisconnectComplete(awaitComplete bool)
}
func ParseDriverTestsuiteXml(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
pathOffset := "/../../../"
testSuitePath := path + pathOffset + testPath
info, err := os.Stat(testSuitePath)
if os.IsNotExist(err) {
t.Logf("Test-File %s doesn't exist", testSuitePath)
t.Fatal(err)
}
if info.IsDir() {
t.Logf("Test-File %s refers to a directory", testSuitePath)
t.Fatal(err)
}
// Open a reader for this file
dat, err := os.Open(testSuitePath)
if err != nil {
t.Log("error opening file")
t.Fatal(err)
}
// Read the xml
node := xmldom.Must(xmldom.Parse(dat)).Root
return node
}
func ParseDriverTestsuite(t *testing.T, node xmldom.Node, parser XmlParser, rootTypeParser func(utils.ReadBufferByteBased) (any, error)) *DriverTestsuite {
if node.Name != "driver-testsuite" {
t.Fatal("invalid document structure")
}
var byteOrder binary.ByteOrder
if node.GetAttributeValue("byteOrder") != "LITTLE_ENDIAN" {
byteOrder = binary.BigEndian
} else {
t.Log("using little endian")
byteOrder = binary.LittleEndian
}
var testsuiteName string
var protocolName string
var outputFlavor string
var driverName string
driverParameters := make(map[string]string)
var setupSteps []DriverTestStep
var teardownSteps []DriverTestStep
var testcases []DriverTestcase
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-name":
driverName = 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 "setup":
steps, err := ParseDriverTestsuiteSteps(t, child)
if err != nil {
t.Error("error parsing setup steps")
t.Fatal(err)
}
setupSteps = steps
case "teardown":
steps, err := ParseDriverTestsuiteSteps(t, child)
if err != nil {
t.Error("error parsing teardown steps")
t.Fatal(err)
}
teardownSteps = steps
case "testcase":
testcaseName := child.FindOneByName("name").Text
stepsNode := child.FindOneByName("steps")
steps, err := ParseDriverTestsuiteSteps(t, *stepsNode)
if err != nil {
t.Errorf("error parsing testcase %s", testcaseName)
t.Fatal(err)
}
testcase := DriverTestcase{
name: testcaseName,
steps: steps,
}
testcases = append(testcases, testcase)
default:
t.Fatalf("invalid document structure. Unhandled element %s", child.Name)
}
}
t.Logf("Parsed testsuite name: %s, driver name: %s", testsuiteName, driverName)
return &DriverTestsuite{
name: testsuiteName,
protocolName: protocolName,
outputFlavor: outputFlavor,
driverName: driverName,
driverParameters: driverParameters,
byteOrder: byteOrder,
parser: parser,
rootTypeParser: rootTypeParser,
setupSteps: setupSteps,
teardownSteps: teardownSteps,
testcases: testcases,
}
}
func ParseDriverTestsuiteSteps(t *testing.T, node xmldom.Node) ([]DriverTestStep, error) {
t.Logf("Parsing driver testsuite steps (rootElement %s)", node.Name)
var testSteps []DriverTestStep
for _, step := range node.Children {
name := step.GetAttributeValue("name")
t.Logf("Parsing step (name: %s, rootElement %s)", name, node.Name)
var stepType StepType
switch step.Name {
case "api-request":
stepType = StepTypeApiRequest
case "api-response":
stepType = StepTypeApiResponse
case "outgoing-plc-message":
stepType = StepTypeOutgoingPlcMessage
case "incoming-plc-message":
stepType = StepTypeIncomingPlcMessage
case "outgoing-plc-bytes":
stepType = StepTypeOutgoingPlcBytes
case "incoming-plc-bytes":
stepType = StepTypeIncomingPlcBytes
case "delay":
stepType = StepTypeDelay
case "terminate":
stepType = StepTypeTerminate
default:
return nil, errors.Errorf("Unknown step with name %s", step.Name)
}
var parserArguments []string
var payload *xmldom.Node
t.Logf("Looking for payload (name: %s, rootElement %s)", name, node.Name)
for _, childNode := range step.Children {
t.Logf("Found payload candidate (child node name: %s, name: %s, rootElement %s)", childNode.Name, name, node.Name)
if childNode.Name == "parser-arguments" {
for _, parserArgumentNode := range childNode.Children {
parserArguments = append(parserArguments, parserArgumentNode.Text)
}
} else if payload == nil {
payload = childNode
} else {
return nil, errors.New("test step can only contain a single payload element")
}
}
if stepType == StepTypeDelay {
payload = step
}
if payload == nil {
return nil, errors.New("missing payload element")
}
testSteps = append(testSteps, DriverTestStep{
name: name,
stepType: stepType,
parserArguments: parserArguments,
payload: *payload,
})
}
return testSteps, nil
}
func (DriverTestsuite) LogDelimiterSection(t *testing.T, delimiter, format string, a ...any) {
t.Helper()
delimiter = strings.Repeat(delimiter, 55)
if !shouldNoColor() {
delimiter = fmt.Sprintf("\x1b[%dm%v\x1b[0m", 32, delimiter)
}
message := "\n"
message += delimiter + "\n"
message += fmt.Sprintf(format, a...) + "\n"
message += delimiter
t.Log(message)
}